diff --git a/js/simulate.js b/js/simulate.js new file mode 100644 index 0000000..2b46f1b --- /dev/null +++ b/js/simulate.js @@ -0,0 +1,215 @@ +let simulationStates = []; +let singleCharMode = true; +let simulationStepDuration = 500; + +function simulate(word) { + let steps = []; + + if (singleCharMode) { + steps = word.split(''); + } else { + steps = word.split(/ /g); + } + + simulationStates.forEach(state => { + state.isActive = false; + }); + simulationStates.splice(); + + const startConnections = connections.filter(conn => conn instanceof StartConnection); + startConnections.forEach(connection => animations.push(new ConnectionAnimation(connection))); + simulationStates = startConnections.map(conn => conn.state); + + setTimeout(() => { + simulationStates.forEach(state => { + state.isActive = true; + state.activeTime = Date.now(); + }); + + let stepIndex = 0; + const stepInterval = setInterval(() => { + if (stepIndex >= steps.length) { + clearInterval(stepInterval); + return; + } + + simulationStep(steps[stepIndex]); + stepIndex++; + }, simulationStepDuration * 4); + }, simulationStepDuration); +} + +function simulationStep(input) { + const len = simulationStates.length; + for (let i = 0; i < len; i++) { + const state = simulationStates[i]; + const possibilities = getOutgoingConnections(state); + + simulationStates.splice(0, 1); + state.isActive = false; + state.activeTime = Date.now(); + + possibilities.forEach(conn => { + const conditions = conn.text.split(/, ?/g); + + console.log(conn, conditions); + + if (input.match(new RegExp(conditions.join('|'), 'g'))) { + if (conn instanceof Connection) { + animations.push(new ConnectionAnimation(conn)); + + setTimeout(() => { + simulationStates.push(conn.stateB); + conn.stateB.isActive = true; + conn.stateB.activeTime = Date.now(); + }, simulationStepDuration); + } else if (conn instanceof SelfConnection) { + animations.push(new SelfConnectionAnimation(conn)); + + setTimeout(() => { + simulationStates.push(conn.state); + conn.state.isActive = true; + conn.state.activeTime = Date.now(); + }, simulationStepDuration); + } + } + }); + } +} + +function getOutgoingConnections(state) { + return connections.filter(connection => connection instanceof SelfConnection ? connection.state === state : connection.stateA === state); +} + +class ConnectionAnimation { + + constructor(connection) { + this.connection = connection; + this.startTime = Date.now(); + } + + getSize(percent) { + if (percent < .33) { + return easeInOutCubic(percent * 3) * 10; + } else if (percent < .66) { + return 10; + } else { + return easeInOutCubic(1 - (percent - .8) * 3) * 10; + } + } + + getPosLinear(percent, points) { + + } + + getPosCircular(percent, points) { + const distanceSquared = (points.end.x - points.start.x) ** 2 + (points.end.y - points.start.y) ** 2; + let phi = Math.acos(1 - (distanceSquared / (2 * points.circle.radius ** 2))); + + if (distanceSquared > points.circle.radius ** 2) { + phi = 2 * Math.PI - phi; + } + + let deltaPhi = phi * percent; + + const startX = points.start.x - points.circle.x; + const startY = points.start.y - points.circle.y; + + if (!points.isReversed) { + deltaPhi *= -1; + } + + const x = startX * Math.cos(deltaPhi) + startY * Math.sin(deltaPhi) + points.circle.x; + const y = -startX * Math.sin(deltaPhi) + startY * Math.cos(deltaPhi) + points.circle.y; + + return {x, y}; + } + + draw() { + const deltaTime = Date.now() - this.startTime; + const percent = Math.min(Math.max(deltaTime / simulationStepDuration, 0), 1); + + let curX = 0; + let curY = 0; + + const endPoints = this.connection instanceof StartConnection ? this.connection.getEndPoints() : this.connection.getEndPointsAndCircle(); + + if (endPoints.isCircle) { + const pos = this.getPosCircular(percent, endPoints); + + curX = pos.x; + curY = pos.y; + } else { + const deltaX = endPoints.end.x - endPoints.start.x; + const deltaY = endPoints.end.y - endPoints.start.y; + + curX = endPoints.start.x + deltaX * percent; + curY = endPoints.start.y + deltaY * percent; + } + + ctx.fillStyle = '#0f0'; + + ctx.beginPath(); + + ctx.arc(curX, curY, this.getSize(percent), 0, 2 * Math.PI); + ctx.fill(); + + ctx.closePath(); + + if (percent >= 1) { + animations.splice(animations.findIndex(anim => anim === this), 1); + } + } + +} + +class SelfConnectionAnimation { + + constructor(connection) { + this.connection = connection; + this.startTime = Date.now(); + } + + draw() { + const deltaTime = Date.now() - this.startTime; + const percent = Math.min(Math.max(deltaTime / simulationStepDuration, 0), 1); + + const endPoints = this.connection.getEndPointsAndCircle(); + + const distanceSquared = (endPoints.end.x - endPoints.start.x) ** 2 + (endPoints.end.y - endPoints.start.y) ** 2; + let phi = Math.acos(1 - (distanceSquared / (2 * endPoints.circle.radius ** 2))); + + if (distanceSquared > endPoints.circle.radius ** 2) { + phi = 2 * Math.PI - phi; + } + + let deltaPhi = phi * percent; + + const startX = endPoints.start.x - endPoints.circle.x; + const startY = endPoints.start.y - endPoints.circle.y; + + if (!endPoints.isReversed) { + deltaPhi *= -1; + } + + let x = startX * Math.cos(deltaPhi) + startY * Math.sin(deltaPhi) + endPoints.circle.x; + let y = -startX * Math.sin(deltaPhi) + startY * Math.cos(deltaPhi) + endPoints.circle.y; + + ctx.fillStyle = '#0f0'; + + ctx.beginPath(); + + ctx.arc(x, y, 10, 0, 2 * Math.PI); + ctx.fill(); + + ctx.closePath(); + + if (percent >= 1) { + animations.splice(animations.findIndex(anim => anim === this), 1); + } + } +} + +function easeInOutCubic(t) { + return t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1 +} \ No newline at end of file