diff --git a/index.html b/index.html index b2b115f..80f265f 100644 --- a/index.html +++ b/index.html @@ -2,18 +2,20 @@ - FMS Designer + FSM Designer + + diff --git a/js/components/state.js b/js/components/state.js index 34aa6e2..b41a4ab 100644 --- a/js/components/state.js +++ b/js/components/state.js @@ -1,11 +1,14 @@ class State { constructor(x, y) { + this.id = guid(); this.x = x; this.y = y; this.mouseOffsetX = 0; this.mouseOffsetY = 0; this.color = '#f0f'; + this.isActive = false; + this.activeTime = 0; this.isAcceptState = false; this.text = ''; } @@ -21,7 +24,9 @@ class State { } draw() { - ctx.fillStyle = this.color; + const activeTimeDuration = Date.now() - this.activeTime; + + ctx.fillStyle = this.isActive && activeTimeDuration > simulationStepDuration ? '#0f0' : this.color; ctx.strokeStyle = settings.colors.getColor(this); ctx.beginPath(); @@ -32,6 +37,26 @@ class State { ctx.closePath(); + if(activeTimeDuration < simulationStepDuration) { + let size = 0; + const percent = Math.min(Math.max(activeTimeDuration / simulationStepDuration, 0), 1); + + if(this.isActive) { + size = easeInOutCubic(percent) * radius; + } else { + size = easeInOutCubic(1 - percent) * radius; + } + + ctx.fillStyle = '#0f0'; + + ctx.beginPath(); + + ctx.arc(this.x, this.y, size, 0, 2 * Math.PI); + ctx.fill(); + + ctx.closePath(); + } + ctx.fillStyle = settings.colors.getColor(this); ctx.drawText(this.text, this.x, this.y, null, selectedObject === this); diff --git a/js/export.js b/js/export.js index e69de29..f7e7e31 100644 --- a/js/export.js +++ b/js/export.js @@ -0,0 +1,65 @@ +function exportToJson() { + const _states = JSON.parse(JSON.stringify(states)); + const _connections = JSON.parse(JSON.stringify(connections)).map((conn, index) => { + conn.type = connections[index].constructor.name; + return conn; + }); + const data = { + states: _states, + connections: _connections.filter(conn => conn.type === 'Connection').map(conn => { + conn.stateA = conn.stateA.id; + conn.stateB = conn.stateB.id; + return conn; + }), + startConnections: _connections.filter(conn => conn.type === 'StartConnection').map(conn => { + conn.state = conn.state.id; + return conn; + }), + selfConnections: _connections.filter(conn => conn.type === 'SelfConnection').map(conn => { + conn.state = conn.state.id; + return conn; + }), + settings, + }; + return JSON.stringify(data); +} + +function importFromJson(json) { + const data = JSON.parse(json); + + states.push(...data.states.map(state => Object.setPrototypeOf(state, State.prototype))); + connections.push(...data.connections.map(conn => Object.setPrototypeOf(conn, Connection.prototype)).map(conn => { + conn.stateA = states.find(state => state.id === conn.stateA); + conn.stateB = states.find(state => state.id === conn.stateB); + return conn; + })); + connections.push(...data.startConnections.map(conn => Object.setPrototypeOf(conn, StartConnection.prototype)).map(conn => { + conn.state = states.find(state => state.id === conn.state); + return conn; + })); + connections.push(...data.selfConnections.map(conn => Object.setPrototypeOf(conn, SelfConnection.prototype)).map(conn => { + conn.state = states.find(state => state.id === conn.state); + return conn; + })); +} + +function exportToFile() { + const name = 'placeholder.fsm'; + const json = exportToJson(); + console.log(json); + downloadFile(name, json, 'application/json'); +} + +function downloadFile(name, content, type) { + const element = document.createElement('a'); + + element.setAttribute('href', `data:${type},charset=utf-8,${content}`); + element.setAttribute('download', name); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + document.body.removeChild(element); +} + diff --git a/js/main.js b/js/main.js index 323a65d..ddd7300 100644 --- a/js/main.js +++ b/js/main.js @@ -22,11 +22,22 @@ canvas.height = height; const states = []; const connections = []; +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; + caretVisible = true, + caretPos = 0; function resetCaret() { clearInterval(caretTimer); @@ -76,6 +87,8 @@ function draw() { currentConnection.draw(); } + animations.forEach(animation => animation.draw()); + ctx.restore(); if (!isPaused) @@ -204,20 +217,36 @@ document.addEventListener('keydown', (event) => { return true; } + console.log(key); + switch (key) { case 16: // Shift shiftPressed = true; break; case 8: // Backspace if (!!selectedObject) { - selectedObject.text = selectedObject.text.substr(0, selectedObject.text.length - 1); + selectedObject.text = selectedObject.text.substr(0, caretPos - 1) + selectedObject.text.substr(caretPos, selectedObject.text.length); + caretPos--; + caretPos = Math.max(caretPos, 0); resetCaret(); } + 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(); + return false; case 46: // Delete if (!!selectedObject) { - if(selectedObject instanceof State) { + if (selectedObject instanceof State) { states.splice(states.findIndex(state => state === selectedObject), 1); } @@ -245,18 +274,38 @@ document.addEventListener('keyup', (event) => { document.addEventListener('keypress', (event) => { const key = crossBrowserKey(event); - if(!canvasHasFocus()) { + if (!canvasHasFocus()) { return true; } - if(key >= 0x20 && key <= 0x7E && !event.metaKey && !event.altKey && !event.ctrlKey && !!selectedObject && 'text' in selectedObject) { - selectedObject.text += String.fromCharCode(key); - resetCaret(); + if (!!selectedObject && 'text' in selectedObject) { + const text = selectedObject.text; - return false; + 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(); + + return false; + } + + console.log(caretPos); + } } - if(key === 8) { + if (key === 8) { + return false; + } +}); + +window.addEventListener('keydown', (event) => { + if ((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 's') { + exportToFile(); + + event.preventDefault(); return false; } }); @@ -274,14 +323,14 @@ CanvasRenderingContext2D.prototype.drawArrow = function (x, y, angle) { this.fill(); }; -CanvasRenderingContext2D.prototype.drawText = function(originalText, x, y, angleOrNull, isSelected) { - const text = originalText; +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) { + if (!!angleOrNull) { const dx = Math.cos(angleOrNull), dy = Math.sin(angleOrNull), cornerPointX = (width / 2 + 5) * (dx > 0 ? 1 : -1), @@ -292,14 +341,14 @@ CanvasRenderingContext2D.prototype.drawText = function(originalText, x, y, angle } // Draw text and caret - if('advancedFillText' in this) { + 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 += width; + if (isSelected && caretVisible && canvasHasFocus() && document.hasFocus()) { + x += this.measureText(text.substring(0, caretPos)).width; this.beginPath(); @@ -368,3 +417,13 @@ function cbRelMousePos(event) { tick(); draw(); + +function guid() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); +} \ No newline at end of file