diff --git a/css/style.css b/css/style.css index eb7e513..3113ff5 100644 --- a/css/style.css +++ b/css/style.css @@ -25,7 +25,7 @@ html, body { .sorter { background:#f4f4f4; border:1px solid #bbb; - height:270px; + height:260px; margin:20px auto; padding:10px; position:relative; @@ -33,7 +33,7 @@ html, body { } .sorter-sidebar { - height:230px; + height:220px; position:absolute; right:10px; top:10px; @@ -43,7 +43,7 @@ html, body { .sorter-svg { background:#fff; border:1px solid #ccc; - height:200px; + height:190px; left:10px; position:absolute; top:10px; @@ -63,7 +63,7 @@ html, body { height:40px; left:10px; position:absolute; - top:220px; + top:210px; width:770px; } diff --git a/index.html b/index.html index f320a13..ef5e39e 100644 --- a/index.html +++ b/index.html @@ -81,8 +81,8 @@ - - + + diff --git a/js/itemgroup.js b/js/itemgroup.js new file mode 100644 index 0000000..b030141 --- /dev/null +++ b/js/itemgroup.js @@ -0,0 +1,146 @@ +/** + * + */ +var Itemgroup = { + /** + * + */ + items: function items(group, delay, n) { + var g; + var all = []; + + // Items start with no background by default. + for (var i = 0; i < n; i++) { + g = group.append('g') + .attr('class', `i${i}`) + .attr('transform', function transform(d) { + return `translate(${i * (Visualizer.itemW + Visualizer.spacerW) + Visualizer.padding}, ${Visualizer.padding})`; + });; + + g.append('rect') + .attr('class', 'item') + .attr('height', Visualizer.itemH) + .attr('width', Visualizer.itemW) + .attr('fill', 'transparent'); + + // Item labels + g.append('text') + .attr('fill', '#aaa') + .attr('font-size', 10) + .attr('font-family', 'sans-serif') + .attr('transform', function transform(d) { + return `rotate(90 0,0), translate(5, -3)`; + }); + + all.push(g); + }; + + return all; + }, + + /** + * + */ + swap: function swap(group, delay, indexA, indexB) { + var x, a, b; + var len = group.selectAll('g')[0].length; + + // NOTE pitfalls in the swapping problem. + // NOTE Two way binding here between dataset and function parameter? + // NOTE swapping will not reorder index and i parameter will be off + // NOTE discuss chained transitions: http://bl.ocks.org/mbostock/1125997 + + // Animate a node swap which will be quietly undone after completion. + group.selectAll('g').transition().duration(delay - 100) + .attr('transform', function transform(d, i) { + x = i * (Visualizer.itemW + Visualizer.spacerW) + Visualizer.padding; + + if (i === indexA) { + x = indexB * (Visualizer.itemW + Visualizer.spacerW) + Visualizer.padding; + a = d3.select(this).select('text').text(); + } + else if (i === indexB) { + x = indexA * (Visualizer.itemW + Visualizer.spacerW) + Visualizer.padding; + b = d3.select(this).select('text').text(); + + } + + return `translate(${x}, ${Visualizer.padding})`; + }) + .each('end', function(d, i) { + if (i !== len - 1) { + return; + } + + console.log(`indexA: ${indexA}, ${a}, indexB: ${indexB}, ${b}`); + + // Undo the animation by restoring the original positions and swapping the values. + group.selectAll('g') + .attr('transform', function transform(d, i) { + x = i * (Visualizer.itemW + Visualizer.spacerW) + Visualizer.padding; + return `translate(${x}, ${Visualizer.padding})`; + }) + .each(function(d, i) { + if (i === indexA) { + d3.select(this).select('text').text(b); + } + else if (i === indexB) { + d3.select(this).select('text').text(a); + } + }); + + console.warn(`indexA: ${indexA}, ${a}, indexB: ${indexB}, ${b}`); + }); + }, + + /** + * + */ + background: function background(group, delay, start, end, color) { + group.selectAll('g').each(function(d, i) { + if (i >= start && i <= end) { + d3.select(this).select('rect').attr('fill', color); + } + }); + }, + + /** + * + */ + foreground: function foreground(group, delay, start, end, color) { + group.selectAll('g').each(function(d, i) { + if (i >= start && i <= end) { + d3.select(this).select('text').attr('fill', color); + } + }); + }, + + /** + * + */ + text: function text(group, delay, which, text) { + // NOTE http://stackoverflow.com/questions/28390754/get-one-element-from-d3js-selection-by-index + group.selectAll('g') + .filter(function filter(d, i) { return i === which; }) + .select('text').text(text); + }, + + /** + * + */ + opacity: function opacity(group, delay, start, end, opacity) { + group.selectAll('g').each(function(d, i) { + if (i >= start && i <= end) { + d3.select(this).attr('opacity', opacity); + } + }); + }, + + /** + * Message updates. + */ + message: function message(group, delay, which, msg) { + var msg = msg || ' '; + this.parent.querySelector(`.message:nth-child(${which})`).innerHTML = msg; + }, +}; diff --git a/js/mergesort.js b/js/mergesort.js index 952b140..14258f2 100644 --- a/js/mergesort.js +++ b/js/mergesort.js @@ -1,96 +1,120 @@ /** * */ -var MergeSort = function(VisualizerInstance) { +var MergeSort = function() { //===== Inits. - this.V = VisualizerInstance; + this.actions = []; this.comparisons = 0; //===== Action management. // - this.initSort = function(arr, start, end) { - this.V - .instruct(this.V.unhighlight, 0) - .instruct(this.V.unfade, 0) - .instruct(this.V.fade, 0, -1, start - 1) - .instruct(this.V.fade, 0, end + 1, arr.length) - .instruct(this.V.message, 0, 1, `Comparisons: ${this.comparisons}`) - .instruct(this.V.message, 0, 2, '') - .instruct(this.V.message, 0, 3, '') - .instruct(this.V.message, 0, 4, '') - .instruct(this.V.message, 0, 5, ''); + this.reset = function(arr, start, end) { + var len = arr.length; + + this + .instruct(Itemgroup.background, 0, 0, '#f00', 0, len) + + .instruct(Itemgroup.opacity, 0, 0, 0, len, 1) + .instruct(Itemgroup.opacity, 0, 0, -1, start - 1, 0.2) + .instruct(Itemgroup.opacity, 0, 0, end + 1, len, 0.2) + + .instruct(Itemgroup.message, 0, 0, 1, `Comparisons: ${this.comparisons}`) + .instruct(Itemgroup.message, 0, 0, 2, '') + .instruct(Itemgroup.message, 0, 0, 3, '') + .instruct(Itemgroup.message, 0, 0, 4, '') + .instruct(Itemgroup.message, 0, 0, 5, ''); }; // this.splitSingle = function(index) { - this.V.instruct(this.V.message, 100, 2, `Single element [${index}]`); + this + .instruct(Itemgroup.message, 0, 100, 2, `Single element [${index}]`); }; // this.preSort = function(start, mid, end) { - this.V - .instruct(this.V.message, 0, 2, `Sorting [${start}] - [${end}]`) - .instruct(this.V.message, 0, 3, 'Slicing and recursing:') - .instruct(this.V.message, 100, 4, `[${start}]-[${mid}] and [${mid + 1}]-[${end}]`); + this + .instruct(Itemgroup.message, 0, 0, 2, `Sorting [${start}] - [${end}]`) + .instruct(Itemgroup.message, 0, 0, 3, 'Slicing and recursing:') + .instruct(Itemgroup.message, 0, 100, 4, `[${start}]-[${mid}] and [${mid + 1}]-[${end}]`); }; // - this.preMerge = function(arr1, arr2, start, mid, end) { - var i, j, x, y, v; - var len1 = arr1.length; - var len2 = arr2.length; - - for (var i = 0; i < len1; i++) { - x = Visualizer.padding + (i + start) * (Visualizer.itemW + Visualizer.spacerW); - y = Visualizer.padding * 2 + Visualizer.itemH; - v = arr1[i].value; - this.V.instruct(this.V.item, 0, 'secondary', x, y, v, '#05350D') - } - - for (var j = 0; j < len2; j++) { - x = Visualizer.padding + (j + len1 + start) * (Visualizer.itemW + Visualizer.spacerW); - y = Visualizer.padding * 2 + Visualizer.itemH; - v = arr2[j].value; - this.V.instruct(this.V.item, 0, 'secondary', x, y, v, '#028E2D') - } - - this.V - // .instruct(this.V.fade, 0, 0, arr.length) - .instruct(this.V.message, 0, 2, ``) - .instruct(this.V.message, 0, 3, 'Merging slices:') - .instruct(this.V.message, 0, 4, `[${start}]-[${mid}] and [${mid + 1}]-[${end}]`) - .instruct(this.V.removeTertiary, 0); + this.preMerge = function(start, mid, end, len) { + this + .instruct(Itemgroup.message, 0, 0, 2, ``) + .instruct(Itemgroup.message, 0, 0, 3, 'Merging slices:') + .instruct(Itemgroup.message, 0, 0, 4, `[${start}]-[${mid}] and [${mid + 1}]-[${end}]`) + + .instruct(Itemgroup.opacity, 1, 0, 0, len, 0) + .instruct(Itemgroup.opacity, 1, 0, start, end, 1) + .instruct(Itemgroup.opacity, 2, 0, 0, len, 0) }; // - this.postMerge = function() { - this.V.instruct(this.V.removeSecondary, 0); + this.postMerge = function(len) { + this + .instruct(Itemgroup.opacity, 1, 0, 0, len, 0) }; // this.midMerge = function(index, value, message) { - var x = Visualizer.padding + index * (Visualizer.itemW + Visualizer.spacerW); - var y = Visualizer.padding * 3 + Visualizer.itemH * 2 - - this.V - .instruct(this.V.item, 0, 'tertiary', x, y, value, '#8E5500') - .instruct(this.V.message, 0, 4, message) - .instruct(this.V.message, 100, 5, `Pushing ${value} to sub-result.`); + this + .instruct(Itemgroup.opacity, 1, 0, index, index, 1) + .instruct(Itemgroup.text, 1, 0, index, value) + .instruct(Itemgroup.message, 0, 100, 5, `Pushing ${value} to sub-result.`); }; // this.updateComparisons = function() { - this.V.instruct(this.V.message, 0, 1, `Comparisons: ${this.comparisons}`); + this.instruct(Itemgroup.message, 0, 0, 1, `Comparisons: ${this.comparisons}`); }; }; MergeSort.prototype = Object.create(Sorter.prototype); +/** + * + */ +MergeSort.prototype.init = function() { + var len = this.shuffled.length; + + this + .instruct(Itemgroup.items, 0, 0, len) + .instruct(Itemgroup.items, 1, 0, len) + .instruct(Itemgroup.items, 2, 0, len) + + for (var i = 0; i < len; i++) { + this.instruct(Itemgroup.text, 0, 0, i, this.shuffled[i]); + } + + this + .instruct(Itemgroup.foreground, 0, 0, 0, len, Visualizer.fg0) + .instruct(Itemgroup.background, 0, 0, 0, len, Visualizer.bg0) + + .instruct(Itemgroup.opacity, 1, 0, 0, 0, len) + .instruct(Itemgroup.foreground, 1, 0, 0, len, Visualizer.fg1) + .instruct(Itemgroup.background, 1, 0, 0, len, Visualizer.bg1) + + .instruct(Itemgroup.opacity, 2, 0, 0, 0, len) + .instruct(Itemgroup.foreground, 2, 0, 0, len, Visualizer.fg2) + .instruct(Itemgroup.background, 2, 100, 0, len, Visualizer.bg2) + + + this + .instruct(Itemgroup.swap, 0, 1000, 0, 1) + .instruct(Itemgroup.swap, 0, 1000, 1, 2) + .instruct(Itemgroup.swap, 0, 1000, 2, 3) + .instruct(Itemgroup.swap, 0, 1000, 3, 2) + .instruct(Itemgroup.swap, 0, 1000, 2, 1) + .instruct(Itemgroup.swap, 0, 1000, 1, 0) +}; + /** * */ MergeSort.prototype.sort = function(arr, start, end) { - this.initSort(arr, start, end); + this.reset(arr, start, end); if (arr.length === 0) { return arr; @@ -98,7 +122,7 @@ MergeSort.prototype.sort = function(arr, start, end) { if (start === end) { this.splitSingle(start); - return new Array(arr[start]); + return [arr[start]]; } @@ -108,10 +132,10 @@ MergeSort.prototype.sort = function(arr, start, end) { var arr1 = this.sort(arr, start, mid); var arr2 = this.sort(arr, mid + 1, end); - this.preMerge(arr1, arr2, start, mid, end); + this.preMerge(start, mid, end, arr.length); var result = this.merge(arr1, arr2); + this.postMerge(arr.length); - this.postMerge(); return result; }; @@ -120,28 +144,28 @@ MergeSort.prototype.sort = function(arr, start, end) { */ MergeSort.prototype.merge = function(arr1, arr2) { var result = []; - var e; + var n; while (arr1.length > 0 || arr2.length > 0) { if (arr1.length === 0) { - e = arr2.shift(); - result.push(e); - this.midMerge(result.length, e.value, 'One element left to merge.'); + n = arr2.shift(); + result.push(n); + this.midMerge(result.length, n, 'One element left to merge.'); } else if (arr2.length === 0) { - e = arr1.shift(); - result.push(e); - this.midMerge(result.length, e.value, 'One element left to merge.'); + n = arr1.shift(); + result.push(n); + this.midMerge(result.length, n, 'One element left to merge.'); } - else if (arr1[0].value <= arr2[0].value) { - e = arr1.shift() - result.push(e); - this.midMerge(result.length, e.value, `${e.value} <= ${arr2[0].value}`); + else if (arr1[0] <= arr2[0]) { + n = arr1.shift() + result.push(n); + this.midMerge(result.length, n, `${n} <= ${arr2[0]}`); } else { - e = arr2.shift(); - result.push(e); - this.midMerge(result.length, e.value, `${arr1[0].value} > ${e.value}`); + n = arr2.shift(); + result.push(n); + this.midMerge(result.length, n, `${arr1[0]} > ${n}`); } this.comparisons++; diff --git a/js/sorter.js b/js/sorter.js index 03b3c9c..292173c 100644 --- a/js/sorter.js +++ b/js/sorter.js @@ -1,7 +1,14 @@ /** * */ -var Sorter = function() {}; +var Sorter = function() { + this.data = []; + this.shuffled = []; + this.ordered = []; + + this.actions = []; + this.comparisons = []; +}; // NOTE fisher-yates, http://bost.ocks.org/mike/algorithms/ /** @@ -22,40 +29,49 @@ Sorter.prototype.shuffle = function(arr) { /** * */ -Sorter.prototype.generate = function(n) { - var arr = []; - var v; - for (var i = 0; i < n; i++) { - v = Math.floor(i * 255 / n); - arr.push({ - value: v, - fill: `rgb(0, 0, ${v})` - }); - }; +Sorter.prototype.swap = function(arr, i, j) { + var tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; +}; - return arr; +/** + * + */ +Sorter.prototype.sort = function() { + throw new Error('Sorter.sort() method override required.'); }; /** * */ -Sorter.prototype.swap = function(arr, i, j) { - // console.info(`swapping ${arr[i].value} and ${arr[j].value}`) - var tmp = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp; +Sorter.prototype.init = function() { + throw new Error('Sorter.init() method override required.'); }; /** * */ -Sorter.prototype.sort = function(instruction) { - throw new Error('Sorter.sort() method override required.'); +Sorter.prototype.instruct = function() { + this.actions.push(arguments); + return this; }; /** * */ -Sorter.prototype.addInstruction = function(instruction) { - throw new Error('Sorter.addInstruction() method override required.'); +Sorter.prototype.generate = function(n) { + this.data = []; + + var upper = Math.floor(Math.random() * 300 + n); + for (var i = 0; i < n; i++) { + this.data.push(Math.floor(i * upper / n)); + }; + + this.shuffled = this.shuffle(this.data); + this.ordered = this.shuffled.slice(); + this.init(); + // this.sort(this.ordered, 0, this.ordered.length - 1); + + return this.actions; }; diff --git a/js/visualizer-actions.js b/js/visualizer-actions.js deleted file mode 100644 index 06b9287..0000000 --- a/js/visualizer-actions.js +++ /dev/null @@ -1,122 +0,0 @@ -/** - * - */ -Visualizer.prototype.swap = function(delay, indexA, indexB) { - // NOTE Two way binding here between dataset and function parameter? - // NOTE swapping will not reorder index and i parameter will be off - // NOTE discuss chained transitions: http://bl.ocks.org/mbostock/1125997 - this.groups - .transition().duration(delay) - .attr('transform', function doTransform(d) { - if (d.index === indexA) { - d.index = indexB; - } - else if (d.index === indexB) { - d.index = indexA; - } - - return `translate(${Visualizer.calculateX(d.index)}, ${Visualizer.itemY})`; - }); -}; - -/** - * Highlights a range of indices with a color. End index and color optional. - */ -Visualizer.prototype.highlight = function(delay, startIndex, endIndex, color) { - if (endIndex === undefined) { - endIndex = startIndex; - } - - if (color === undefined) { - color = 'orangered'; - } - - this.groups.each(function(d, i) { - if (d.index >= startIndex && d.index <= endIndex) { - d3.select(this).select('rect').attr('fill', color); - } - }); -}; - -/** - * Un-highlights an index. - */ -Visualizer.prototype.unhighlight = function() { - // this.svg.selectAll('.item').attr('fill', function(d) { return d.fill; }); - this.svg.selectAll('.item').attr('fill', '#1A45AC'); -}; - -/** - * Greys out an item. - */ -Visualizer.prototype.fade = function(delay, startIndex, endIndex) { - this.groups.each(function(d) { - if (d.index >= startIndex && d.index <= endIndex) { - d3.select(this).style('opacity', '0.2'); - } - }); -}; - -/** - * Restores all items to un-greyed state. - */ -Visualizer.prototype.unfade = function() { - this.groups.each(function(d) { - d3.select(this).style('opacity', 1); - }); -}; - -/** - * Message updates. - */ -Visualizer.prototype.message = function(delay, which, msg) { - var msg = msg || ' '; - this.parent.querySelector(`.message:nth-child(${which})`).innerHTML = msg; -}; - -/** - * - */ -Visualizer.prototype.item = function(delay, classname, x, y, text, color) { - var g = this.svg.append('g') - .attr('class', classname) - .attr('transform', `translate(${x}, ${y})`); - - g.append('rect').attr('width', Visualizer.itemW) - .attr('height', Visualizer.itemH) - .attr('fill', color); - - g.append('text').text(text) - .attr('fill', '#aaa') - .attr('font-size', 10) - .attr('font-family', 'sans-serif') - .attr('transform', function doTransform(d) { - return `rotate(90 0,0), translate(5, -3)`; - }); -}; - -/** - * - */ -Visualizer.prototype.removeSecondary = function() { - this.svg.selectAll('.secondary').remove(); -}; - -/** - * - */ -Visualizer.prototype.removeTertiary = function() { - this.svg.selectAll('.tertiary').remove(); -}; - -/** - * - */ -Visualizer.prototype.remove = function(delay, id) { - this.svg.select(`#${id}`).remove(); -}; - -/** - * - */ -// Visualizer.prototype.move = function() diff --git a/js/visualizer-inits.js b/js/visualizer-dom.js similarity index 80% rename from js/visualizer-inits.js rename to js/visualizer-dom.js index d7dce74..a327dc0 100644 --- a/js/visualizer-inits.js +++ b/js/visualizer-dom.js @@ -3,51 +3,23 @@ /** * */ -Visualizer.prototype.initItems = function(n) { - var data = this.sorter.generate(n); - var shuffled = this.sorter.shuffle(data); - var ordered = Object.create(shuffled); - ordered = this.sorter.sort(ordered, 0, ordered.length - 1); - - // A swap on the dataset will not take effect until after transition is complete, so custom index is required. - var n = 0; - for (var i in shuffled) { - shuffled[i].index = n++; - } - - if (this.svg !== undefined) { - this.svg.remove(); - } - - this.actionIndex = 0; - this.svg = d3.select(this.parent).append('svg') - .attr('class', 'sorter-svg'); - - // Items - this.groups = this.svg.selectAll('g').data(shuffled).enter().insert('g') - .attr('transform', `translate(0, ${Visualizer.padding})`); - - this.groups.append('rect') - .attr('class', 'item') - .attr('height', Visualizer.itemH) - .attr('width', Visualizer.itemW) - // .attr('fill', function doFill(d) { return d.fill; }); - .attr('fill', '#1A45AC'); - - this.groups.transition(500) - .attr('transform', function doTransform(d, i) { - return `translate(${i * (Visualizer.itemW + Visualizer.spacerW) + Visualizer.padding}, ${Visualizer.padding})`; - }); - - // Item labels - this.groups.append('text') - .text(function t(d) { return d.value; }) - .attr('fill', '#aaa') - .attr('font-size', 10) - .attr('font-family', 'sans-serif') - .attr('transform', function doTransform(d) { - return `rotate(90 0,0), translate(5, -3)`; - }); +Visualizer.prototype.initSvg = function() { + var svg = d3.select(this.parent).append('svg').attr('class', 'sorter-svg'); + var groups = []; + + groups.push(svg.append('g') + .attr('transform', `translate(0, 0)`) + ); + + groups.push(svg.append('g') + .attr('transform', `translate(0, ${Visualizer.padding + Visualizer.itemH})`) + ); + + groups.push(svg.append('g') + .attr('transform', `translate(0, ${Visualizer.padding * 2 + Visualizer.itemH * 2})`) + ); + + return groups; }; /** diff --git a/js/visualizer.js b/js/visualizer.js index 420aa7f..0ecda9f 100644 --- a/js/visualizer.js +++ b/js/visualizer.js @@ -3,9 +3,11 @@ */ function Visualizer(parent) { this.actions = []; + this.actionIndex = 0; this.parent = parent; this.sorter = null; this.paused = true; + this.groups = this.initSvg(); var sorterSidebarContainer = document.createElement('div'); sorterSidebarContainer.className = 'sorter-sidebar'; @@ -25,60 +27,51 @@ function Visualizer(parent) { switch(parent.attributes['data-algorithm'].value) { case 'quick': - this.sorter = new QuickSort(this); + this.sorter = new QuickSort(); break; case 'merge': - this.sorter = new MergeSort(this); + this.sorter = new MergeSort(); break; case 'selection': - this.sorter = new SelectionSort(this); + this.sorter = new SelectionSort(); break; case 'bubble': - this.sorter = new BubbleSort(this); + this.sorter = new BubbleSort(); break; case 'insertion': - this.sorter = new InsertionSort(this); + this.sorter = new InsertionSort(); break; case 'shell': - this.sorter = new ShellSort(this); + this.sorter = new ShellSort(); break; case 'radix': - this.sorter = new RadixSort(this); + this.sorter = new RadixSort(); break; default: throw new Error('Unrecognized sort type.'); } - this.initItems(10); + this.actions = this.sorter.generate(10); }; -// Static properties (mutable) +// Static properties (global, mutable) Visualizer.spacerW = 5; Visualizer.itemW = 14; Visualizer.itemH = 50; Visualizer.padding = 10; - -/** - * Static. - */ -Visualizer.calculateX = function(index) { - return Visualizer.spacerW + index * (Visualizer.itemW + Visualizer.spacerW) -}; - -/** - * - */ -Visualizer.prototype.instruct = function() { - this.actions.push(arguments); - return this; -}; +Visualizer.bg0 = '#284A8F'; +Visualizer.bg1 = '#C25C49'; +Visualizer.bg2 = '#CCCC53'; +Visualizer.fg0 = '#e7e7e7'; +Visualizer.fg1 = '#e7e7e7'; +Visualizer.fg2 = '#000000'; /** * Instructions contain a string with the name of a function in this object which is called to perform an action. @@ -89,30 +82,33 @@ Visualizer.prototype.go = function() { } var obj = this.actions[this.actionIndex]; - var instruction = new Array(); + var action = new Array(); for (var key in obj) { if (obj[key].hasOwnProperty) { - instruction.push(obj[key]); + action.push(obj[key]); } } - var delay = instruction[1]; - var args = instruction.slice(1); + action[1] = this.groups[action[1]]; + var delay = action[2]; + var args = action.slice(1); + + action[0].apply(this, args); // TODO add tabs for best/worst cases // TODO add links to stats // TODO fix init slider // TODO heap sort // TODO extra memory - // TODO width and height updates + // TODO width update + // TODO disable next button if no further actions and during action + // NOTE functional programming discussion // NOTE interesting (anti?)pattern here. // NOTE use of call() vs apply() (apply only delivered first array item as string) // if (typeof operation === 'function') { - // operation.call(this, instruction); - - instruction[0].apply(this, args); + // operation.call(this, action); if (delay === 0) { this.actionIndex++;