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