From 9d87dc638704cb0c2e2f2b3ccd4450dc2211e1f6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 27 Feb 2019 21:22:38 +0100 Subject: [PATCH] Initial commit --- index.html | 18 +++ js/components/connection.js | 103 ++++++++++++++++ js/components/self-connection.js | 75 ++++++++++++ js/components/start-connection.js | 50 ++++++++ js/components/state.js | 85 ++++++++++++++ js/main.js | 187 ++++++++++++++++++++++++++++++ js/math.js | 16 +++ 7 files changed, 534 insertions(+) create mode 100644 index.html create mode 100644 js/components/connection.js create mode 100644 js/components/self-connection.js create mode 100644 js/components/start-connection.js create mode 100644 js/components/state.js create mode 100644 js/main.js create mode 100644 js/math.js diff --git a/index.html b/index.html new file mode 100644 index 0000000..222c439 --- /dev/null +++ b/index.html @@ -0,0 +1,18 @@ + + + + + FMS Designer + + + + + + + + + + + + + diff --git a/js/components/connection.js b/js/components/connection.js new file mode 100644 index 0000000..0f99867 --- /dev/null +++ b/js/components/connection.js @@ -0,0 +1,103 @@ +class Connection { + + constructor(stateA, stateB) { + this.stateA = stateA; + this.stateB = stateB; + this.text = ''; + this.lineAngleAdjust = 0; + + this.parallelPart = 0.5; + this.perpendicularPart = 0; + } + + getAnchorPoint() { + const dx = this.stateB.x - this.stateA.x; + const dy = this.stateB.y - this.stateA.y; + const scale = Math.sqrt(dx * dx + dy * dy); + return { + x: this.stateA.x + dx * this.parallelPart - dy * this.perpendicularPart / scale, + y: this.stateA.y + dy * this.parallelPart + dx * this.perpendicularPart / scale, + }; + } + + setAnchorPoint() { + const dx = this.stateB.x - this.stateA.x; + const dy = this.stateB.y - this.stateA.y; + const scale = Math.sqrt(dx ^ 2 + dy ^ 2); + + this.parallelPart = (dx * (x - this.stateA.x) + dy * (y - this.stateA.y)) / (scale ^ 2); + this.perpendicularPart = (dx * (y - this.stateA.y) - dy * (x - this.stateA.x)) / scale; + + if(this.parallelPart > 0 && this.parallelPart < 1 && Math.abs(this.perpendicularPart) < settings.snapToPadding) { + this.lineAngleAdjust = (this.perpendicularPart < 0) * Math.PI; + this.perpendicularPart = 0; + } + } + + getEndPointsAndCircle() { + if(this.perpendicularPart === 0) { + const middleX = (this.stateA.x + this.stateB.y) / 2; + const middleY = (this.stateB.y + this.stateB.y) / 2; + const start = this.stateA.closestPointOnCircle(middleX, middleY); + const end = this.stateB.closestPointOnCircle(middleX, middleY); + + return { + isCircle: false, + start, + end + }; + } + + const anchor = this.getAnchorPoint(); + const circle = circleFromPoints( + {x: this.stateA.x, y: this.stateA.y}, + {x: this.stateB.x, y: this.stateB.y}, + anchor, + ); + + const isReversed = this.perpendicularPart > 0; + const reverseScale = isReversed ? 1 : -1; + + const startAngle = Math.atan2(this.stateA.y - circle.y, this.stateA.x - circle.x) - reverseScale * radius / circle.radius; + const endAngle = Math.atan2(this.stateB.y - circle.y, this.stateB.x - circle.y) + reverseScale * radius / circle.radius; + + const startX = circle.x + circle.radius * Math.cos(startAngle); + const startY = circle.y + circle.radius * Math.sin(startAngle); + + const endX = circle.x + circle.radius * Math.cos(endAngle); + const endY = circle.y + circle.radius * Math.sin(endAngle); + + return { + isCircle: true, + start: {x: startX, y: startY}, + end: {x: endX, y: endY}, + startAngle, + endAngle, + circle, + isReversed, + reverseScale, + }; + } + + draw() { + const stuff = this.getEndPointsAndCircle(); + + ctx.beginPath(); + + if(stuff.isCircle) { + ctx.arc(stuff.circle.x, stuff.circle.y, stuff.circle.radius, stuff.startAngle, stuff.endAngle, stuff.isReversed); + } else { + ctx.moveTo(stuff.start.x, stuff.start.y); + ctx.lineTo(stuff.end.x, stuff.end.y); + } + + ctx.stroke(); + + if(stuff.isCircle) { + ctx.drawArrow(stuff.end.x, stuff.end.y, stuff.endAngle - stuff.reverseScale * (Math.PI / 2)); + } else { + ctx.drawArrow(stuff.end.x, stuff.end.y, Math.atan2(stuff.end.y - stuff.start.y, stuff.end.x - stuff.start.x)); + } + } + +} diff --git a/js/components/self-connection.js b/js/components/self-connection.js new file mode 100644 index 0000000..d11397a --- /dev/null +++ b/js/components/self-connection.js @@ -0,0 +1,75 @@ +class SelfConnection { + + constructor(state, mouse) { + this.state = state; + this.anchorAngle = 0; + this.mouseOffsetAngle = 0; + this.text = ''; + + if(mouse) { + this.setAnchorPoint(mouse.x, mouse.y); + } + } + + setMouseStart(x, y) { + this.mouseOffsetAngle = this.anchorAngle - Math.atan2(y - this.state.y, x - this.state.x); + } + + setAnchorPoint(x, y) { + this.anchorAngle = Math.atan2(y - this.state.y, x - this.state.x) + this.mouseOffsetAngle; + + const snap = Math.round(this.anchorAngle / (Math.PI / 2)) * (Math.PI / 2); + + if(Math.abs(this.anchorAngle - snap) < .1) + this.anchorAngle = snap; + + if(this.anchorAngle < -Math.PI) + this.anchorAngle += 2 * Math.PI; + + if(this.anchorAngle > Math.PI) + this.anchorAngle -= 2 * Math.PI; + } + + getEndPointsAndCircle() { + const circle = { + x: this.state.x + 1.5 * radius * Math.cos(this.anchorAngle), + y: this.state.y + 1.5 * radius * Math.sin(this.anchorAngle), + radius: 0.75 * radius, + }; + + const startAngle = this.anchorAngle - Math.PI * .8; + const endAngle = this.anchorAngle + Math.PI * .8; + + const start = { + x: circle.x + circle.radius * Math.cos(startAngle), + y: circle.y + circle.radius * Math.sin(startAngle), + }; + + const end = { + x: circle.x + circle.radius * Math.cos(endAngle), + y: circle.y + circle.radius * Math.sin(endAngle), + }; + + return { + isCircle: true, + start, + end, + startAngle, + endAngle, + circle, + }; + } + + draw() { + const endPoints = this.getEndPointsAndCircle(); + + ctx.beginPath(); + + ctx.arc(endPoints.circle.x, endPoints.circle.y, endPoints.circle.radius, endPoints.startAngle, endPoints.endAngle); + + ctx.stroke(); + + ctx.drawArrow(endPoints.end.x, endPoints.end.y, endPoints.endAngle + Math.PI * .4); + } + +} diff --git a/js/components/start-connection.js b/js/components/start-connection.js new file mode 100644 index 0000000..e39ac8f --- /dev/null +++ b/js/components/start-connection.js @@ -0,0 +1,50 @@ +class StartConnection { + + constructor(state, start) { + this.state = state; + this.deltaX = 0; + this.deltaY = 0; + this.text = ''; + + if(start) { + this.setAnchorPoint(start.x, start.y); + } + } + + setAnchorPoint(x, y) { + this.deltaX = x - this.state.x; + this.deltaY = y - this.state.y; + + if(Math.abs(this.deltaX) < settings.snapToPadding) { + this.deltaX = 0; + } + + if(Math.abs(this.deltaY) < settings.snapToPadding) { + this.deltaY = 0; + } + } + + getEndPoints() { + const startX = this.state.x + this.deltaX; + const startY = this.state.y + this.deltaY; + + const end = this.state.closestPointOnCircle(startX, startY); + + return { + start: {x: startX, y: startY}, + end, + }; + } + + draw() { + const endPoints = this.getEndPoints(); + + ctx.beginPath(); + + ctx.moveTo(endPoints.start.x, endPoints.start.y); + ctx.lineTo(endPoints.end.x, endPoints.end.y); + + ctx.stroke(); + } + +} diff --git a/js/components/state.js b/js/components/state.js new file mode 100644 index 0000000..8aea642 --- /dev/null +++ b/js/components/state.js @@ -0,0 +1,85 @@ +class State { + + constructor(x, y, color) { + this.x = x; + this.y = y; + this.color = color; + this.text = ''; + } + + draw() { + ctx.fillStyle = this.color; + ctx.strokeStyle = '#000'; + + ctx.beginPath(); + + ctx.arc(this.x, this.y, radius, 0, 360); + ctx.fill(); + ctx.stroke(); + + ctx.closePath(); + } + + jumpTo(x, y) { + this.x = Math.min(Math.max(x, 0), width); + this.y = Math.min(Math.max(y, 0), height); + } + + moveTo(x, y) { + x = Math.min(Math.max(0, x), width); + y = Math.min(Math.max(0, y), height); + + this.goal = { + x, y + }; + + const angle = this.direction(x, y); + + this.v = { + x: settings.speed * Math.cos(angle), + y: settings.speed * Math.sin(angle) + }; + } + + moveToStep() { + this.x += this.v.x; + this.y += this.v.y; + + if ((this.v.x > 0 && this.goal.x <= this.x) || (this.v.x < 0 && this.goal.x >= this.x)) { + this.x = this.goal.x; + this.v.x = 0; + } + + if((this.v.y > 0 && this.goal.y <= this.y) || (this.v.y < 0 && this.goal.y >= this.y)) { + this.y = this.goal.y; + this.v.y = 0; + } + } + + intersection(state) { + return -Math.hypot(this.x - state.x, this.y - state.y) + radius * 2; + } + + intersects(state) { + return this.intersection(state) > 0; + } + + directionTo(state) { + return this.direction(state.x, state.y); + } + + direction(x, y) { + return Math.atan2(this.y - y, this.x - x); + } + + closestPointOnCircle(x, y) { + const dx = x - this.x; + const dy = y - this.y; + const scale = Math.sqrt(dx ^ 2 + dy ^ 2); + return { + x: this.x + dx * radius / scale, + y: this.y + dy * radius / scale, + }; + } + +} diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..e895ab3 --- /dev/null +++ b/js/main.js @@ -0,0 +1,187 @@ +const width = 600; +const height = 300; +const radius = 25; + +const settings = { + physics: true, + speed: 2, + snapToPadding: 10 +}; + +const canvas = document.getElementById('canvas'); +const ctx = canvas.getContext('2d'); + +canvas.width = width; +canvas.height = height; + +const states = []; +const connections = []; + +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(); + } + }); + + setTimeout(tick, 1000 / 30); +} + +function draw() { + ctx.clearRect(0, 0, width, height); + + connections.forEach(connection => connection.draw()); + + states.forEach(state => state.draw()); + + setTimeout(draw, 1000 / 60); +} + +function selectObject(x, y) { + for (let state of states) { + if ((x - state.x) * (x - state.x) + (y - state.y) * (y - state.y) < radius * radius) { + return state; + } + } +} + +function getStateByPos(x, y) { +} + +function checkForState(x, y) { + return !!getStateByPos(x, y); +} + +function addState(x, y) { + const state = new State(x, y, '#f0f'); + states.push(state); +} + +function addConnection(stateA, stateB, angleA, angleB) { + const connection = new Connection(stateA, stateB, angleA, angleB); + connections.push(connection); +} + +let isMouseDown = false, + isDragging = false, + lastDrag = 0, + selectedObject = null, + shiftPressed = false, + currentConnection = null, + movingObject = false; + +canvas.addEventListener('mousedown', (event) => { + selectedObject = selectObject(event.x, event.y); + + shiftPressed = event.shiftKey; + + if (!!selectedObject) { + if (shiftPressed && selectedObject instanceof State) { + currentConnection = new SelfConnection(selectedObject, event); + } else { + movingObject = true; + } + } +}); + +canvas.addEventListener('mousemove', (event) => { + if (isMouseDown && !isDragging) + isDragging = true; + + if (isMouseDown && isDragging) { + if (!shiftPressed) { + selectedObject.jumpTo(event.clientX, event.clientY); + } + + selectedObject.setAnchorPoint(event.clientX, event.clientY); + } + + if (!!currentConnection) { + let targetState = selectObject(event.x, event.y); + + if (!targetState instanceof State) { + targetState = null; + } + + if (!!selectedObject) { + if (targetState === selectedObject) { + currentConnection = new SelfConnection(selectedObject, event); + } else if(!!targetState) { + currentConnection = new Connection(selectedObject, targetState); + } else { + // currentConnection = new + } + } else { + + } + } + +}); + +canvas.addEventListener('mouseup', (event) => { + if (isDragging) { + lastDrag = Date.now() + } + + if (shiftPressed) { + const state = getStateByPos(event.clientX, event.clientY); + addConnection(selectedObject, state, Math.PI, -Math.PI); + } + + isMouseDown = false; + isDragging = false; + selectedObject = null; +}); + +canvas.addEventListener('click', (event) => { + const x = event.clientX, + y = event.clientY; + + if (Date.now() - lastDrag < 100) { + return; + } + + if (checkForState(x, y)) { + } +}); + +canvas.addEventListener('dblclick', (event) => { + const x = event.clientX, + y = event.clientY; + + addState(x, y); +}); + +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(); +}; + +tick(); +draw(); diff --git a/js/math.js b/js/math.js new file mode 100644 index 0000000..9ac4020 --- /dev/null +++ b/js/math.js @@ -0,0 +1,16 @@ +function det(a, b, c, d, e, f, g, h, i) { + return (a * e * i) + (b * f * g) + (c * d * h) - (a * f * h) - (b * d * i) - (c * e * g); +} + +function circleFromPoints(p1, p2, p3) { + const a = det(p1.x, p1.y, 1, p2.x, p2.y, 1, p3.x, p3.y, 1); + const bx = -det(p1.x * p1.x + p1.y * p1.y, p1.y, 1, p2.x * p2.x + p2.y * p2.y, p2.y, 1, p3.x * p3.x + p3.y * p3.y, p3.y, 1); + const by = det(p1.x * p1.x + p1.y * p1.y, p1.x, 1, p2.x * p2.x + p2.y * p2.y, p2.x, 1, p3.x * p3.x + p3.y * p3.y, p3.x, 1); + const c = -det(p1.x * p1.x + p1.y * p1.y, p1.x, p1.y, p2.x * p2.x + p2.y * p2.y, p2.x, p2.y, p3.x * p3.x + p3.y * p3.y, p3.x, p3.y); + + return { + x: -bx / (2 * a), + y: -by / (2 * a), + radius: Math.sqrt(bx ^ 2 + by ^ 2 - 4 * a * c) / (2 * Math.abs(a)), + }; +}