57 | Check if string matches regular expression:
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | Automatic animations
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
88 |
89 |
90 | Help menu
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
Regular expressions
104 |
A regular expression is a sequence of characters that define a search pattern.
105 |
106 | The regular expressions supported by this tool may contain:
107 |
108 |
Letters (a-z, A-Z)
109 |
The quantifiers *, meaning 0 or more of the previous token, and +, meaning 1 or more of the previous token
110 |
Alternation |, meaning either the expression to the left or to the right of the | has to match.
111 |
Parenthesis (), to group things together
112 |
The number 0, meaning the empty string
113 |
Concatenation
114 |
115 |
116 |
An example regular expression using these symbols is (ab|cd)+(e|0).
117 | The strings that match this regular expression are exactly the strings that start with 1 or more instances of the strings ab or cd, and after that either the character e, or nothing.
118 | Some examples of strings that match this regular expression are ab, cde, abcde, cdcdabcd.
119 |
For more information about regular expressions, click here (some different notation and way more possibilities, but the same concept)
120 |
121 |
122 |
Finite state machines
123 |
124 | A finite state machine (also called a finite automaton) is a finite set of states N, together with a transition function.
125 | This transition function takes a state and a symbol as input, and returns a set of states, a subset of N.
126 | These returned states are the states that can be reached by starting from the given input state and reading the given input symbol.
127 | The set of symbols an automaton contains is called the alphabet.
128 |
129 |
130 | A finite state machine also has one starting state, the state we are in after reading no input symbols.
131 | Finally, any of the states in N can be accepting/final. If we end up in an accepting state after reading a string, we say that string is accepted by the machine.
132 | This tool will create finite state machines, such that the accepted strings are those that exactly match the given regular expression.
133 |
134 | Here states will be represented as circles, accepting states as double circles.
135 | The transition function will be represented as directed edges between these states, meaning you can go from one state to another using the symbol in the edge's label.
136 | The starting state will have an incoming edge from nowhere.
137 |
138 |
139 |
140 |
Types of finite state machines
141 |
142 | We will define 4 different types of finite state machines, each with a more strict definition than the previous.
143 |
144 |
145 |
Nondeterministic finite automaton with λ-transitions (NFA-λ): This is the most general, least strict type of state machine. Here, the transition function may return multiple states for any input state and symbol.
146 | This means that in some cases we have a choice what state to go to next, given an input state and symbol (nondeterminism). In the visual representation used here, this means there are multiple outgoing edges from the input state with the same symbol. The transition function is also allowed to return no states, in which case reading the input symbol from the input state is not possible. In the visual representation used here, this means there are no outgoing edges from the input state with the input symbol.
147 | The state machine may also contain λ-transitions. These transitions (edges) are labeled λ and allow you to freely move from one state to another without reading any input symbols.
148 |
Nondeterministic finite automaton (NFA): This type is the same as the NFA-λ, except that λ-transitions are not allowed.
149 |
Deterministic finite automaton (DFA): In deterministic finite automata, the transition function has to return exactly one state for each input state and symbol in the alphabet.
150 | This means that for every string of characters in the alphabet, there is one and at most one path you can follow. In the visual representation used here, this means that every state has exactly one outgoing edge for each symbol in the alphabet.
151 |
Minimal deterministic finite automaton (DFAm): We call a DFA minimal if there is no other DFA with less states that accepts the exact same strings as the original DFA.
152 | It can be proven that for each DFA, there exists a unique DFAm, up to the naming of the states.
153 |
154 |
155 |
156 |
Algorithms
157 |
158 | To convert a regular expression to a DFAm, we use 4 different algorithms, each resulting in the next type from the previous section.
159 | Here follows a brief overview of all the algorithms, but for more in depth explanations, I used the book "Introduction to Languages and the Theory of Computation" by John C. Martin.
160 |
161 |
162 | The first algorithm converts a regular expression to a NFA-λ. This is done using a bottom-up construction.
163 | We start with finite state machines for a single character, and 3 template state machines for the 3 main operations (alternation, concatenation and the quantifiers).
164 | We then substitute the simple state machines into the template machine we need, until we have a finite state machine for the entire regular expression.
165 | Finally, some basic simplification is done to make the state machine more readable.
166 |
167 |
168 | The second algorithm converts a NFA-λ to a NFA by replacing the λ-transitions with normal transitions.
169 | We start off by determining the λ-closure of each state: The set of states that can be reached from that state using only λ-transitions. Next, we make any state whose λ-closure contains an accepting state also accepting.
170 | Then, for every state p and for every state r in the λ-closure of p, we duplicate all the normal outgoing edges from r, and make their starting point p.
171 | Finally, we remove all the λ-transitions, and if during this process any of the states have become unreachable from the starting state, we remove them.
172 |
173 |
174 | The third algorithm converts a NFA to a DFA using an algorithm called "subset construction".
175 | We start with a new starting state that corresponds to the old starting state. We then determine for each of the symbols in the alphabet, the set of states can be reached from the starting state using that symbol.
176 | If there is not yet a new state corresponding to this old set of states, we create this new state and add an edge from the starting state to this new state with the given symbol.
177 | If there is already a new state corresponding to this old set of states, we simply add an edge from the starting state to this state with the given symbol.
178 | We continue this process for each of the newly created states, until all of the new states have an outgoing edge for every symbol in the alphabet.
179 | A new state in this DFA is accepting, if any of the corresponding old states were accepting in the NFA.
180 |
181 |
182 | The last algorithm converts a DFA to a DFAm by minimizing it.
183 | This is done by determining which of the states of the DFA are equivalent, and merging them. Two states are called equivalent,
184 | if the strings that cause you to end up in an accepting state starting from that state, are exactly the same for both states.
185 | To determine which states are equivalent, we start off by marking each pair of states as equivalent, and step by step mark pairs as not equivalent if we find proof they are not.
186 | In the first step, we mark every pair of states, where one of the states is accepting and the other one is non-accepting, as non-equivalent, since for example the empty string causes one of them to end up in an accepting state, but not the other one.
187 | In each of the next steps, we take a pair of states that is marked equivalent, and for each symbol in the alphabet, we check if the new pair of states that we get from following the edge with that symbol from the starting pair, is marked as equivalent.
188 | If this new pair is marked as not-equivalent, we have found a symbol that leads the original pair of states to two non-equivalent states. This implies that the original pair of states is also not equivalent, and so we mark it as such.
189 | We do this until we cannot mark any more pairs as non-equivalent. Finally, we merge the states that have been found to be equivalent, and we are done.
190 |
191 |
192 |
193 |
Features
194 |
195 |
Convert a regular expression into a DFAm, using custom animations
196 |
This gives a very visual way to see what strings match the regular expression and what strings don't
197 |
Smooth automatic animations, or animations in steps, with information about the current step
198 |
Hover over any of the states, to see an infobox with some example strings that can end up in that state
199 |
Pan and zoom the state machine around by dragging the screen and using your mousewheel, to get a better look at large and complicated state machines
200 |
A semi-in-depth help menu you are looking at right now!
201 |
202 |
203 |
204 |
205 |
206 |
207 |
Welcome!
208 |
This tool is used to visualize which strings exactly match a regular expression and which don't, using finite state machines
209 |
You can start playing around now by pressing "Example expression" or by typing in your own regular expression
210 |
Press the "help" button at any time for more information about the application
211 |
235 |
236 |
237 |
238 |
239 |
--------------------------------------------------------------------------------
/js/animation.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /*
4 | Wrapper function for all main animations. Takes a animation function in as argument
5 | and keeps a queue containing the next animation steps, that either get triggered
6 | automatically, or with the animation step button if auto_animation is off
7 | */
8 | function animationWrap(animationFunction, args, step) {
9 | in_animation = true;
10 | var queue = []; // Queue containing the next animation steps that are ready to go
11 | queue.notify = function(message) { // Custom queue function to notify queue when a new element gets added
12 | if (queue == null) // Animation was skipped during this step, stop
13 | return;
14 | if (auto_animation)
15 | (queue.shift())(); // Execute the first animation in the queue
16 | else { // Not auto animnation, wait for click
17 | if (message != undefined)
18 | $("#step_message").html("Next step: " + message);
19 | $("#options_button").one("click", function() {
20 | $("#step_message").html("");
21 | (queue.shift())()
22 | });
23 | }
24 | }
25 | queue.done = $.Deferred(); // Gets resolved when last step of animation is done
26 | $.when(queue.done).then(function() {
27 | in_animation = false;
28 | $("#FA animate").remove(); // Delete the used animation objects
29 | $("#FA animateTransform").remove();
30 | if (step <= 4)
31 | $("#step_message").html("");
32 | $("#skip_button").unbind(); // Remove old event handlers
33 | $("#options_button").unbind();
34 | $("#skip_button").css("visibility", "hidden");
35 | queue = null;
36 | })
37 | $("#step_message").html("");
38 | args.push(queue);
39 | animationFunction.apply(this, args); // Start animation function
40 | $("#skip_button").css("visibility", "visible");
41 | $("#skip_button").click(function() {
42 | skipAnimation(step, queue);
43 | })
44 | return queue.done;
45 | }
46 |
47 | function skipAnimation(step, queue) {
48 | if (step == 5 || step == 6) {
49 | $("#graphdouble").remove();
50 | if (step == 5) {
51 | doubleGraph($("#graph0"), "#31eb37", false); // Green
52 | $("#step_message").html("The string matches the regular expression!");
53 | }
54 | else {
55 | doubleGraph($("#graph0"), "#e61515", false); // Red
56 | $("#step_message").html("The string doesn't match the regular expression!");
57 | }
58 | queue.done.resolve();
59 | return;
60 | }
61 | $.when(instance.FAstrings[step]).then(function(element) {
62 | var FA = step == 0 ? instance.NFAl
63 | : step == 2 ? instance.NFA
64 | : step == 3 ? instance.DFA
65 | : step == 4 ? instance.DFAm
66 | : undefined;
67 | instance.FAobj[step] = FAToHTML(FA, element);
68 | $("#FA").html(instance.FAobj[step].svg).promise().then(function() {
69 | updateScale($("#FA").children()[0]);
70 | })
71 | queue.done.resolve();
72 | })
73 | }
74 |
75 | // Small intro animation (moving header up, not start popup path)
76 | function introAnimation() {
77 | $(".helpbutton2, .examplebutton").css("border", "none")
78 | $(".beginbuttons").animate({
79 | 'height': '0',
80 | 'opacity': '0'
81 | }, 700).promise().then(function() {
82 | $(".beginbuttons").css("display", "none");
83 | $("#convert, #regex").css("margin", "10px auto");
84 | })
85 | return $("#header").animate({
86 | 'height': '110px'
87 | }, 700).promise().then(function() {
88 | $("#line").css({
89 | display: 'block'
90 | });
91 | $("#FA").css({
92 | display: 'flex'
93 | });
94 | }).then(function() {
95 | $("#line").animate({
96 | 'opacity': '1'
97 | }, 200);
98 | $("#help").animate({
99 | 'opacity': '1'
100 | }, 200);
101 | $("#options").animate({
102 | 'opacity': '1'
103 | }, 200);
104 | $("#next_step").animate({
105 | 'opacity': '1'
106 | }, 200);
107 | return $("#previous_step").animate({
108 | 'opacity': '1'
109 | }, 200).promise();
110 | });
111 | }
112 |
113 | function startPopupAnimation() { // Start popup animation 1, 2, start
114 | $("#pa1").animate({
115 | "stroke-dashoffset": "0"
116 | }, 600).promise().then(function() {
117 | $("#an1")[0].beginElement();
118 | $("#popupanim").delay(200).promise().then(function() {
119 | $("#cp11, #cp12").animate({
120 | "stroke-dashoffset": "0"
121 | }, 400).promise().then(function() {
122 | $("#t1").animate({"opacity": 1}, 300);
123 | $("#pa2").animate({
124 | "stroke-dashoffset": "0"
125 | }, 600).promise().then(function() {
126 | $("#an2")[0].beginElement();
127 | $("#popupanim").delay(200).promise().then(function() {
128 | $("#cp21, #cp22").animate({
129 | "stroke-dashoffset": "0"
130 | }, 400).promise().then(function() {
131 | $("#t2").animate({"opacity": 1}, 300);
132 | $("#pa3").animate({
133 | "stroke-dashoffset": "0"
134 | }, 600).promise().then(function() {
135 | $("#an3")[0].beginElement();
136 | $("#popupanim").delay(200).promise().then(function() {
137 | $("#cp31, #cp32").animate({
138 | "stroke-dashoffset": "0"
139 | }, 700).promise().then(function() {
140 | $("#t3").animate({"opacity": 1}, 300);
141 | $("#popupanim").css("pointer-events", "all");
142 | })
143 | })
144 | })
145 | })
146 | })
147 | })
148 | })
149 | })
150 | })
151 | }
152 |
153 |
154 | /*
155 | Functions for showing an FA (animating in)
156 | */
157 | function showFA(FAobj, queue) {
158 | var timeper = Math.max(Math.min(4000 / FAobj.states.length, 500), 250); // Animation time between 250 and 500 ms
159 |
160 | return showEdge(FAobj.start[0], timeper).then(function() { // Show starting edge
161 | queue.push(function() {
162 | showFAParts(FAobj, [FAobj.start[1].toString()], [], [$("polygon", FAobj.start[0])[0].points[1]], queue)
163 | });
164 | queue.notify("Show state(s) " + FAobj.start[1] + " and outgoing edges" );
165 | });
166 | }
167 |
168 | function showFAParts(FAobj, curstates, visited, entrypoints, queue) { // Show the states in curstates and their outgoing edges
169 | // Entrypoints are the coordinates at the tips of arrow of the edges that made
170 | // curstates. These are needed for the precise animation of the circles
171 | var timeper = Math.max(Math.min(4000 / FAobj.states.length, 500), 250); // Animation time between 250 and 500 ms
172 | var nextentrypoints = [];
173 | var nextstates = [];
174 | var p1, p2, p3;
175 |
176 | for (var i in curstates) {
177 | p1 = showState(FAobj.states[curstates[i]], entrypoints[i], timeper);
178 | }
179 | p2 = $.when(p1).then(function() {
180 | for (var i in curstates) {
181 | for (var to in FAobj.edges[curstates[i]]) {
182 | p3 = showEdge(FAobj.edges[curstates[i]][to][1], timeper);
183 | if (!curstates.includes(to) && !visited.includes(to) && !nextstates.includes(to)) {
184 | nextstates.push(to);
185 | nextentrypoints.push($("polygon", FAobj.edges[curstates[i]][to][1])[0].points[1]);
186 | }
187 | }
188 | /*for (var symbol in FAobj.edges[curstates[i]]) {
189 | FAobj.edges[curstates[i]][symbol].forEach(function(val) {
190 | p2 = showEdge(val[1], timeper);
191 | if (!curstates.includes(val[0]) && !visited.includes(val[0]) && !nextstates.includes(val[0])) {
192 | nextstates.push(val[0]);
193 | nextentrypoints.push($("polygon", val[1])[0].points[1]);
194 | }
195 | });
196 | }*/
197 | visited.push(curstates[i]);
198 | }
199 | return p3;
200 | });
201 |
202 | return $.when(p2).then(function() {
203 | if (nextstates.length == 0) {
204 | queue.done.resolve(); // Notify queue that the animation is finished
205 | return;
206 | }
207 | queue.push(function() {
208 | showFAParts(FAobj, nextstates, visited, nextentrypoints, queue)
209 | });
210 | queue.notify("Show state(s) " + nextstates.join(",") + " and outgoing edges");
211 | });
212 | }
213 |
214 | function showState(state, entrypoint, time) {
215 | var circles = $("ellipse", state);
216 | var cx = parseFloat(circles.attr("cx"));
217 | var cy = parseFloat(circles.attr("cy"));
218 | var distance = Math.sqrt(Math.pow(cx - entrypoint.x, 2) + Math.pow(cy - entrypoint.y, 2)); // Distance from center to entrypoint
219 | var startx, starty, endx, endy;
220 | var color = circles.attr("stroke");
221 | var strokewidth = circles.attr("stroke-width");
222 | var p1;
223 | for (var i = 0; i < circles.length; i++) {
224 | var r = circles[i].rx.baseVal.value;
225 | // Arrows slightly stick out into the states, calculate actual entrypoint and endpoint
226 | startx = cx - (cx - entrypoint.x) * r / distance;
227 | starty = cy - (cy - entrypoint.y) * r / distance;
228 | endx = 2 * cx - startx;
229 | endy = 2 * cy - starty;
230 |
231 | var curve1 = document.createElementNS('http://www.w3.org/2000/svg', "path");
232 | var curve2 = document.createElementNS('http://www.w3.org/2000/svg', "path");
233 | curve1.setAttribute("fill", "none");
234 | curve2.setAttribute("fill", "none");
235 | curve1.setAttribute("stroke", color);
236 | curve2.setAttribute("stroke", color);
237 | curve1.setAttribute("stroke-width", strokewidth);
238 | curve2.setAttribute("stroke-width", strokewidth);
239 | curve1.setAttribute("d", "M" + startx + "," + starty + " A" + r + " " + r + " 0 0 0 " + endx + " " + endy);
240 | curve2.setAttribute("d", "M" + startx + "," + starty + " A" + r + " " + r + " 0 1 1 " + endx + " " + endy);
241 | curve1.setAttribute("class", "ellipsepath");
242 | curve2.setAttribute("class", "ellipsepath");
243 |
244 | var len = curve1.getTotalLength();
245 | $(curve1).css({
246 | 'stroke-dasharray': len,
247 | 'stroke-dashoffset': len
248 | });
249 | len = curve2.getTotalLength();
250 | $(curve2).css({
251 | 'stroke-dasharray': len,
252 | 'stroke-dashoffset': len
253 | });
254 | state[0].appendChild(curve1);
255 | state[0].appendChild(curve2);
256 | $(curve1).animate({
257 | 'stroke-dashoffset': 0
258 | }, time);
259 | p1 = $(curve2).animate({
260 | 'stroke-dashoffset': 0
261 | }, time).promise();
262 | }
263 | $.when(p1).then(function(path) { // Remove the animation paths and replace them with the actual circle
264 | $("ellipse", path.parent()).attr("visibility", "visible");
265 | $("text", path.parent()).animate({
266 | "opacity": 1
267 | }, 220);
268 | $(".ellipsepath", path.parent()).remove();
269 | $("animate", state).remove(); // Remove old animation objects
270 | })
271 | return p1;
272 | }
273 |
274 | function unshowState(state, entrypoint, time) { // Opposite of showState
275 | // (in this case entrypoint is actually the exit point for the next edge)
276 | $("ellipse", state).attr("visibility", "hidden"); // Hide ellipses and replace them with animated paths
277 | var circles = $("ellipse", state);
278 | var cx = parseFloat(circles.attr("cx"));
279 | var cy = parseFloat(circles.attr("cy"));
280 | var distance = Math.sqrt(Math.pow(cx - entrypoint.x, 2) + Math.pow(cy - entrypoint.y, 2)); // Distance from center to entrypoint
281 | var startx, starty, endx, endy;
282 | var color = circles.attr("stroke");
283 | var strokewidth = circles.attr("stroke-width");
284 | var p1;
285 | for (var i = 0; i < circles.length; i++) {
286 | var r = circles[i].rx.baseVal.value;
287 | // Arrows slightly stick out into the states, calculate actual entrypoint and endpoint
288 | startx = cx + (cx - entrypoint.x) * r / distance;
289 | starty = cy + (cy - entrypoint.y) * r / distance;
290 | endx = 2 * cx - startx;
291 | endy = 2 * cy - starty;
292 |
293 | var curve1 = document.createElementNS('http://www.w3.org/2000/svg', "path");
294 | var curve2 = document.createElementNS('http://www.w3.org/2000/svg', "path");
295 | curve1.setAttribute("fill", "none");
296 | curve2.setAttribute("fill", "none");
297 | curve1.setAttribute("stroke", color);
298 | curve2.setAttribute("stroke", color);
299 | curve1.setAttribute("stroke-width", strokewidth);
300 | curve2.setAttribute("stroke-width", strokewidth);
301 | curve1.setAttribute("d", "M" + startx + "," + starty + " A" + r + " " + r + " 0 0 0 " + endx + " " + endy);
302 | curve2.setAttribute("d", "M" + startx + "," + starty + " A" + r + " " + r + " 0 1 1 " + endx + " " + endy);
303 | curve1.setAttribute("class", "ellipsepath");
304 | curve2.setAttribute("class", "ellipsepath");
305 |
306 | var len = curve1.getTotalLength();
307 | $(curve1).css({
308 | 'stroke-dasharray': len,
309 | 'stroke-dashoffset': 0
310 | });
311 | len = curve2.getTotalLength();
312 | $(curve2).css({
313 | 'stroke-dasharray': len,
314 | 'stroke-dashoffset': 0
315 | });
316 | state[0].appendChild(curve1);
317 | state[0].appendChild(curve2);
318 | $(curve1).animate({
319 | 'stroke-dashoffset': -len
320 | }, time);
321 | p1 = $(curve2).animate({
322 | 'stroke-dashoffset': -len
323 | }, time).promise();
324 | }
325 | $.when(p1).then(function(path) { // Remove the animation paths
326 | $(".ellipsepath", path.parent()).remove();
327 | $("animate", state).remove(); // Remove old animation objects
328 | })
329 | return p1;
330 | }
331 |
332 | function showEdge(edge, time) {
333 | var path = $("path", edge);
334 | var pol = $("polygon", edge);
335 | var length = path[0].getTotalLength();
336 | return path.css("stroke-dashoffset", length).animate({
337 | 'stroke-dashoffset': 0
338 | }, length / (length + 10) * time).promise().then(function() {
339 | var points = pol[0].points;
340 | var anim = document.createElementNS('http://www.w3.org/2000/svg', "animate");
341 | var attrs = {
342 | attributeName: "points",
343 | attributeType: "XML",
344 | begin: "indefinite",
345 | dur: (10 / (length + 10) * time) + "ms",
346 | fill: "freeze",
347 | from: points[0].x + " " + points[0].y + " " + points[0].x + " " + points[0].y + " " + points[2].x + " " + points[2].y + " " + points[2].x + " " + points[2].y + " " + points[0].x + " " + points[0].y + " ",
348 | to: points[0].x + " " + points[0].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[2].x + " " + points[2].y + " " + points[0].x + " " + points[0].y + " "
349 | };
350 | for (var k in attrs)
351 | anim.setAttribute(k, attrs[k]);
352 | pol.attr("visibility", "visible");
353 | pol[0].appendChild(anim);
354 | $("animate", pol)[0].beginElement();
355 | return edge.delay(10 / (length + 10) * time).promise().then(function() { // Artifical delay, same time as animation time
356 | $("text", edge).animate({"opacity": 1}, 220);
357 | $("animate", edge).remove(); // Remove old animation objects
358 | })
359 | })
360 | }
361 |
362 | function showEdge2(edge, time) { // Show edge the other way around (first polygon then path)
363 | var path = $("path", edge);
364 | var pol = $("polygon", edge);
365 | var length = path[0].lengthsaved;
366 | var points = pol[0].points;
367 | var anim = document.createElementNS('http://www.w3.org/2000/svg', "animate");
368 | var attrs = {
369 | attributeName: "points",
370 | attributeType: "XML",
371 | begin: "indefinite",
372 | dur: (10 / (length + 10) * time) + "ms",
373 | fill: "freeze",
374 | from: points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " ",
375 | to: points[0].x + " " + points[0].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[2].x + " " + points[2].y + " " + points[0].x + " " + points[0].y + " "
376 | };
377 | for (var k in attrs)
378 | anim.setAttribute(k, attrs[k]);
379 | pol.attr("visibility", "visible");
380 | pol[0].appendChild(anim);
381 | $("animate", pol)[0].beginElement();
382 | return edge.delay(10 / (length + 10) * time).promise().then(function() { // Artifical delay, same time as animation time
383 | return path.css("stroke-dashoffset", -length).animate({
384 | 'stroke-dashoffset': 0
385 | }, length / (length + 10) * time).promise().then(function() {
386 | $("text", edge).animate({"opacity": 1}, 220);
387 | $("animate", edge).remove(); // Remove old animation objects
388 | });
389 | })
390 | }
391 |
392 | function unshowEdge(edge, time) { // Opposite of showEdge. First animate out polygon, then path
393 | var path = $("path", edge);
394 | var pol = $("polygon", edge);
395 | var length = path[0].lengthsaved;
396 | var points = pol[0].points;
397 | var anim = document.createElementNS('http://www.w3.org/2000/svg', "animate");
398 | var attrs = {
399 | attributeName: "points",
400 | attributeType: "XML",
401 | begin: "indefinite",
402 | dur: (10 / (length + 10) * time) + "ms",
403 | fill: "freeze",
404 | from: points[0].x + " " + points[0].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[2].x + " " + points[2].y + " " + points[0].x + " " + points[0].y + " ",
405 | to: points[0].x + " " + points[0].y + " " + points[0].x + " " + points[0].y + " " + points[2].x + " " + points[2].y + " " + points[2].x + " " + points[2].y + " " + points[0].x + " " + points[0].y + " "
406 | };
407 | for (var k in attrs)
408 | anim.setAttribute(k, attrs[k]);
409 | pol[0].appendChild(anim);
410 | $("animate", pol)[0].beginElement();
411 | return edge.delay(10 / (length + 10) * time).promise().then(function() { // Artifical delay, same time as animation time
412 | pol.attr("visibility", "hidden");
413 | return path.css("stroke-dashoffset", 0).animate({
414 | 'stroke-dashoffset': length
415 | }, length / (length + 10) * time).promise().then(function() {
416 | $("text", edge).animate({"opacity": 0}, 220);
417 | $("animate", edge).remove(); // Remove old animation objects
418 | });
419 | })
420 | }
421 |
422 | function unshowEdge2(edge, time) { // Opposite of showEdge. First animate out path, then polygon
423 | var path = $("path", edge);
424 | var pol = $("polygon", edge);
425 | var length = path[0].getTotalLength();
426 | return path.css("stroke-dashoffset", 0).animate({
427 | 'stroke-dashoffset': -length
428 | }, length / (length + 10) * time).promise().then(function() {
429 | var points = pol[0].points;
430 | var anim = document.createElementNS('http://www.w3.org/2000/svg', "animate");
431 | var attrs = {
432 | attributeName: "points",
433 | attributeType: "XML",
434 | begin: "indefinite",
435 | dur: (10 / (length + 10) * time) + "ms",
436 | fill: "freeze",
437 | from: points[0].x + " " + points[0].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[2].x + " " + points[2].y + " " + points[0].x + " " + points[0].y + " ",
438 | to: points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " "
439 | };
440 | for (var k in attrs)
441 | anim.setAttribute(k, attrs[k]);
442 | pol[0].appendChild(anim);
443 | $("animate", pol)[0].beginElement();
444 | return edge.delay(10 / (length + 10) * time).promise().then(function() { // Artifical delay, same time as animation time
445 | pol.attr("visibility", "hidden");
446 | $("animate", edge).remove(); // Remove old animation objects
447 | })
448 | })
449 | }
450 |
451 | /*
452 | Convert NFAl to NFA by removing l-transitions
453 | (and updating accepting states and removing unreachable states)
454 | */
455 | function animToNFAStart(FAobjs, queue) { // Starting animations: Rearrange and add accepting states
456 | if (FAobjs[1] == undefined || FAobjs[1].newedges == undefined) {
457 | $("#step_message").html("The automaton doesn't contain any λ-transitions. Continuing..");
458 | FAobjs[2].svg.attr("viewBox", FAobjs[0].svg.attr("viewBox"));
459 | FAobjs[2].svg.children().attr("transform", FAobjs[0].svg.children().attr("transform"));
460 | $("#FA").html(FAobjs[2].svg).promise().then(function() {
461 | updateScale($("#FA").children()[0]);
462 | })
463 | $("#FA").delay(1000).promise().then(function() {
464 | queue.done.resolve();
465 | })
466 | return;
467 | }
468 |
469 | var newedges = false;
470 | for (var from in FAobjs[1].newedges) {
471 | for (var to in FAobjs[1].newedges[from]) {
472 | if (FAobjs[1].newedges[from][to].length != 0)
473 | newedges = true;
474 | if (newedges) break
475 | }
476 | if (newedges) break
477 | }
478 |
479 | var fMoveo = function() {
480 | return moveo(FAobjs[0], FAobjs[1]);
481 | }
482 | var fAccepting = function() {
483 | var p1;
484 | for (var i = 0; i < FAobjs[1].newaccepting.length; i++)
485 | p1 = $($("ellipse",FAobjs[1].newaccepting[i][1])[1]).animate({"opacity": "1"}, 400).promise();
486 | return p1;
487 | }
488 | var fReplace = function() {
489 | replaceEdges(FAobjs, 0, queue);
490 | }
491 |
492 | if (FAobjs[1].newaccepting.length == 0) { // No new accepting states, skip that step
493 | if (!newedges) { // No new edges, no need to rearrange
494 | FAobjs[1].svg.attr("viewBox", FAobjs[0].svg.attr("viewBox"));
495 | FAobjs[1].svg.children().attr("transform", FAobjs[0].svg.children().attr("transform"));
496 | $("#FA").html(FAobjs[1].svg).promise().then(function() {
497 | updateScale($("#FA").children()[0]);
498 | })
499 | fReplace();
500 | }
501 | else { // Has new edges, rearrange first
502 | queue.push(function() {
503 | fMoveo().then(fReplace);
504 | })
505 | queue.notify("Rearrange graph to make space for the new edges");
506 | }
507 | }
508 | else { // Does have new accepting states
509 | if (!newedges) { // No new edges, no need to rearrange
510 | queue.push(function() {
511 | FAobjs[1].svg.attr("viewBox", FAobjs[0].svg.attr("viewBox"));
512 | FAobjs[1].svg.children().attr("transform", FAobjs[0].svg.children().attr("transform"));
513 | $("#FA").html(FAobjs[1].svg).promise().then(function() {
514 | updateScale($("#FA").children()[0]);
515 | })
516 | fAccepting().then(fReplace);
517 | })
518 | var str = "";
519 | for (var i = 0; i < FAobjs[1].newaccepting.length-1; i++)
520 | str += FAobjs[1].newaccepting[i][0] + ", ";
521 | str += FAobjs[1].newaccepting[FAobjs[1].newaccepting.length-1][0];
522 | queue.notify("Mark all states that can reach an accepting state using only λ-transitions as accepting (states " + str + ")");
523 | }
524 | else { // Has new edges, rearrange first
525 | queue.push(function() {
526 | fMoveo().then(function() {
527 | queue.push(function() {
528 | fAccepting().then(fReplace);
529 | })
530 | var str = "";
531 | for (var i = 0; i < FAobjs[1].newaccepting.length-1; i++)
532 | str += FAobjs[1].newaccepting[i][0] + ", ";
533 | str += FAobjs[1].newaccepting[FAobjs[1].newaccepting.length-1][0];
534 | queue.notify("Mark all states that can reach an accepting state using only λ-transitions as accepting (states " + str + ")");
535 | })
536 | })
537 | queue.notify("Rearrange graph to make space for the new edges");
538 | }
539 | }
540 | }
541 |
542 | function animToNFAFinish(FAobjs, queue) { // Finishing animations: Remove unreachable states and rearrange
543 | if (FAobjs[1].unreachable.length == 0) { // No unreachable states, skip that step
544 | queue.push(function() {
545 | moveo(FAobjs[1], FAobjs[2]).then(function() {
546 | queue.done.resolve();
547 | })
548 | })
549 | queue.notify("Rearrange for clarity")
550 | }
551 | else {
552 | queue.push(function() {
553 | var p1;
554 | for (var from in FAobjs[1].edges) {
555 | for (var to in FAobjs[1].edges[from]) {
556 | for (var i = 0; i < FAobjs[1].unreachable.length; i++) {
557 | if (from == FAobjs[1].unreachable[i][0] || to == FAobjs[1].unreachable[i][0]) {
558 | unshowEdge(FAobjs[1].edges[from][to][1],200);
559 | break;
560 | }
561 | }
562 | }
563 | }
564 | for (var i = 0; i < FAobjs[1].unreachable.length; i++) {
565 | p1 = FAobjs[1].unreachable[i][1].delay(200).promise().then(function() {
566 | return this.animate({"opacity": 0}, 300).promise();
567 | })
568 | }
569 | $.when(p1).then(function() {
570 | queue.push(function() {
571 | moveo(FAobjs[1], FAobjs[2], FAobjs[1].unreachable).then(function() {
572 | queue.done.resolve();
573 | })
574 | })
575 | queue.notify("Rearrange for clarity")
576 | })
577 | })
578 |
579 | var str = "";
580 | for (var i = 0; i < FAobjs[1].unreachable.length-1; i++)
581 | str += FAobjs[1].unreachable[i][0] + ", ";
582 | str += FAobjs[1].unreachable[FAobjs[1].unreachable.length-1][0];
583 | queue.notify("Remove unreachable states from the starting state (remove states " + str + ")")
584 | }
585 | }
586 |
587 | function replaceEdges(FAobjs, state, queue) { // Main animations: Replace the lambda transitions
588 | var FAobj = FAobjs[1];
589 | if (FAobj.states.length == state) { // Reached last state, play finishing animation
590 | animToNFAFinish(FAobjs, queue);
591 | return;
592 | }
593 | var hasledges = false;
594 | for (var to in FAobj.edges[state]) {
595 | if (FAobj.edges[state][to][0].includes("0")) {
596 | hasledges = true;
597 | break;
598 | }
599 | }
600 | if (!hasledges) {
601 | replaceEdges(FAobjs, state+1, queue); // Doesnt have ledges, go to next state
602 | return;
603 | }
604 | for (var to in FAobj.edges[state]) {
605 | if (FAobj.edges[state][to][0].includes("0")) {
606 | var edge = FAobj.edges[state][to][1]; // Mark lambda edges red to be removed in the next step
607 | var path = $("path", edge);
608 | var pol = $("polygon", edge);
609 | path.attr("stroke", "#ff0000");
610 | pol.attr("fill", "#ff0000");
611 | pol.attr("stroke", "#ff0000");
612 | $("text", edge).attr("fill", "#ff0000");
613 | }
614 | }
615 | queue.push(function() {
616 | var p1;
617 | for (var to in FAobj.edges[state]) {
618 | if (FAobj.edges[state][to][0].includes("0")) {
619 | var edge = FAobj.edges[state][to];
620 | edge[0].splice(edge[0].indexOf("0"), 1);
621 | if (edge[0].length == 0) { // Transition only consists of lambda, can remove it entirely
622 | p1 = unshowEdge(edge[1], 1000); // Unshow the edges
623 | }
624 | else { // Transition also contains other symbols, just change text
625 | $("text", edge[1]).text(edge[0].join(","));
626 | var te = $("text", edge[1])[0];
627 | var index = te.innerHTML.indexOf("0");
628 | if (index != -1)
629 | te.innerHTML = te.innerHTML.substr(0, index) + "λ" + te.innerHTML.substr(index+1);
630 | }
631 | }
632 | }
633 | var showEdgeAndText = function(edge) {
634 | return showEdge(edge[1], 1000).then(function() {
635 | $("text", edge[1]).text(edge[0].join(",")); // Set new text of edge
636 | var te = $("text", edge[1])[0];
637 | var index = te.innerHTML.indexOf("0");
638 | if (index != -1)
639 | te.innerHTML = te.innerHTML.substr(0, index) + "λ" + te.innerHTML.substr(index+1);
640 | });
641 | }
642 |
643 | $.when(p1).then(function() {
644 | var p2;
645 | for (var to in FAobj.newedges[state]) {
646 | if (FAobj.newedges[state][to].length == 0)
647 | continue;
648 | var edge = FAobj.edges[state][to];
649 | var path = $("path", edge[1]); // Make edge black again if previously colored red
650 | var pol = $("polygon", edge[1]);
651 | path.attr("stroke", "#000000");
652 | pol.attr("fill", "#000000");
653 | pol.attr("stroke", "#000000");
654 | $("text", edge[1]).attr("fill", "#000000");
655 | edge[0] = edge[0].concat(FAobj.newedges[state][to]);
656 | if (edge[0].length == FAobj.newedges[state][to].length) { // Transition didnt exist yet, show it
657 | p2 = showEdgeAndText(edge);
658 | }
659 | }
660 | return p2;
661 | }).then(function() {
662 | replaceEdges(FAobjs, state+1, queue);
663 | })
664 | })
665 | queue.notify("Replace λ-transitions from state " + state + " with corresponding non-λ-transitions")
666 | }
667 |
668 | function moveo(FAold, FAnew, unreachable) { // Animate an automaton to another automaton with the same states and edges, possibly with shifted states
669 | // Compute which old state numbers correspond to which new state numbers using a list of the unreachable states from the old automaton
670 | var stepdown = [];
671 | for (var i = 0; i < FAold.states.length; i++)
672 | stepdown[i] = 0
673 | if (unreachable != undefined) {
674 | for (var i = 0; i < unreachable.length; i++) {
675 | for (var j = unreachable[i][0]; j < FAold.states.length; j++) {
676 | stepdown[j]++;
677 | }
678 | }
679 | }
680 | var newtoold = []
681 | for (var i = 0; i < FAnew.states.length; i++) {
682 | var j = 0;
683 | for (; j < FAold.states.length; j++) {
684 | if (j-stepdown[j]==i) {
685 | newtoold[i] = j
686 | break;
687 | }
688 | }
689 | }
690 |
691 | $("#graph0 text", FAnew.svg).attr("visibility", "hidden"); // Hide all text temporarily
692 | var edges = $(".edge", FAnew);
693 | var states = $(".node", FAnew);
694 | $("text", FAold.svg).finish(); // Finish all ongoing text animations instantly
695 | return $("#graph0 text", FAold.svg).animate({
696 | "opacity": 0
697 | }, 300).promise().then(function() {
698 | $("#FA").html(FAnew.svg).promise().then(function() { // Replace with actual DFAm
699 | updateScale($("#FA").children()[0]);
700 | })
701 | // Animate transform translate to new value
702 | var anim = document.createElementNS('http://www.w3.org/2000/svg', "animateTransform");
703 | var attrs = {
704 | attributeName: "transform",
705 | attributeType: "XML",
706 | type: "translate",
707 | begin: "indefinite",
708 | fill: "freeze",
709 | from: FAold.svg.children().eq(0).attr("transform").match(/translate\((.*)\)/)[1],
710 | to: FAnew.svg.children().eq(0).attr("transform").match(/translate\((.*)\)/)[1],
711 | dur: "1s"
712 | }
713 | for (var k in attrs)
714 | anim.setAttribute(k, attrs[k]);
715 | FAnew.svg.children()[0].appendChild(anim);
716 | anim.beginElement();
717 | // Animate viewbox to new value
718 | var anim2 = document.createElementNS('http://www.w3.org/2000/svg', "animate");
719 | var attrs2 = {
720 | attributeName: "viewBox",
721 | attributeType: "XML",
722 | begin: "indefinite",
723 | fill: "freeze",
724 | from: FAold.svg.attr("viewBox"),
725 | to: FAnew.svg.attr("viewBox"),
726 | dur: "1s"
727 | }
728 | for (var k in attrs2)
729 | anim2.setAttribute(k, attrs2[k]);
730 | FAnew.svg[0].appendChild(anim2);
731 | anim2.beginElement();
732 | $("text", FAold.svg).css("opacity", 1)
733 | for (var i = 0; i < FAnew.states.length; i++) {
734 | var oldi = newtoold[i];
735 | moveState(FAnew.states[i],
736 | [$("ellipse", FAold.states[oldi]).attr("cx"), $("ellipse", FAold.states[oldi]).attr("cy")],
737 | [$("ellipse", FAnew.states[i]).attr("cx"), $("ellipse", FAnew.states[i]).attr("cy")], 1000);
738 | for (var to in FAnew.edges[i]) {
739 | var edgeNew = FAnew.edges[i][to];
740 | var edgeOld = FAold.edges[oldi][newtoold[to]];
741 | if (edgeOld != undefined && edgeNew != undefined && ArrayEquals(edgeOld[0], edgeNew[0]))
742 | moveEdge(edgeNew[1],
743 | [$("path", edgeOld[1]).attr("d"), $("polygon", edgeOld[1]).attr("points")],
744 | [$("path", edgeNew[1]).attr("d"), $("polygon", edgeNew[1]).attr("points")], 1000);
745 | }
746 | }
747 | moveEdge(FAnew.start[0],
748 | [$("path", FAold.start[0]).attr("d"), $("polygon", FAold.start[0]).attr("points")],
749 | [$("path", FAnew.start[0]).attr("d"), $("polygon", FAnew.start[0]).attr("points")], 1000);
750 | return $("#FA svg").delay(1000).promise().then(function() {
751 | $("#graph0 text", FAnew.svg).attr("visibility", "visible"); // Reshow text
752 | for (var i = 0; i < FAnew.edges.length; i++) { // Animate in text
753 | for (var to in FAnew.edges[i])
754 | if (FAnew.edges[i][to][0].length != 0)
755 | $("text", FAnew.edges[i][to][1]).css("opacity",0).animate({"opacity": 1}, 300);
756 | $("text", FAnew.states[i]).css("opacity",0).animate({"opacity": 1}, 300);
757 | }
758 | $("#FA animate").remove(); // Delete the used animation objects
759 | });
760 | })
761 | }
762 | function moveEdge(edge, from, to, time) { // Animate edge from one position to another. From and to contain the d and points attributes
763 | $("path", edge).css("stroke-dasharray", 0); // Set stroke-dasharray to 0 while animating to avoid invisible parts
764 | // Animating path requires both paths to have same amount of segments. Check difference and add empty segments if necessary
765 | var diff = from[0].split(/[\s,CMc]+/).length - to[0].split(/[\s,CMc]+/).length;
766 | if (diff % 6 != 0)
767 | console.log("??");
768 | if (diff > 0)
769 | to[0] += "c0,0 0,0 0,0".repeat(diff / 6);
770 | else
771 | from[0] += "c0,0 0,0 0,0".repeat(-diff / 6);
772 | var anim = document.createElementNS('http://www.w3.org/2000/svg', "animate");
773 | var attrs = {
774 | attributeName: "d",
775 | attributeType: "XML",
776 | begin: "indefinite",
777 | dur: time + "ms",
778 | fill: "freeze",
779 | from: from[0],
780 | to: to[0]
781 | };
782 | for (var k in attrs)
783 | anim.setAttribute(k, attrs[k]);
784 | $("path", edge)[0].appendChild(anim);
785 | anim.beginElement();
786 | // Animate path polygons (arrow points) from old to new position
787 | var anim2 = document.createElementNS('http://www.w3.org/2000/svg', "animate");
788 | var attrs2 = {
789 | attributeName: "points",
790 | attributeType: "XML",
791 | begin: "indefinite",
792 | dur: time + "ms",
793 | fill: "freeze",
794 | from: from[1],
795 | to: to[1]
796 | };
797 | for (var k in attrs)
798 | anim2.setAttribute(k, attrs2[k]);
799 | $("polygon", edge)[0].appendChild(anim2);
800 | anim2.beginElement();
801 | $("path", edge).delay(time).promise().then(function() {
802 | this[0].lengthsaved = this[0].getTotalLength();
803 | this.css("stroke-dasharray", this[0].lengthsaved); // Set stroke-dasharray back to original value
804 | })
805 | return $(edge).delay(time).promise();
806 | }
807 |
808 | function moveState(state, from, to, time) { // Animate state from one position to another. From and to contain the cx and cy attributes
809 | for (var j = 0; j < $("ellipse", state).length; j++) {
810 | // Animate ellipses from old to new position
811 | var anim = document.createElementNS('http://www.w3.org/2000/svg', "animate");
812 | var attrs = {
813 | attributeName: "cx",
814 | attributeType: "XML",
815 | begin: "indefinite",
816 | dur: time + "ms",
817 | fill: "freeze",
818 | from: from[0],
819 | to: to[0]
820 | };
821 | var anim2 = document.createElementNS('http://www.w3.org/2000/svg', "animate");
822 | var attrs2 = {
823 | attributeName: "cy",
824 | attributeType: "XML",
825 | begin: "indefinite",
826 | dur: time + "ms",
827 | fill: "freeze",
828 | from: from[1],
829 | to: to[1]
830 | };
831 | for (var k in attrs)
832 | anim.setAttribute(k, attrs[k]);
833 | for (var k in attrs2)
834 | anim2.setAttribute(k, attrs2[k]);
835 | $("ellipse", state)[j].appendChild(anim);
836 | $("ellipse", state)[j].appendChild(anim2);
837 | anim.beginElement();
838 | anim2.beginElement();
839 | }
840 | return $(state).delay(time).promise();
841 | }
842 |
843 | /*
844 | Convert NFA to DFA using subset construction
845 | (Make a splitscreen, and state by state build the DFA)
846 | */
847 | function animToDFAStart(instance, queue) {
848 | $("#FA svg").animate({"width": "40%"}, 600).promise().then(function() {
849 | updateScale($("#FA svg")[0]);
850 | //$("#FA").append("