1var animationState = {};
2animationState.reset = function (engine) {
3    if ('string' === typeof engine) {
4        this.defaultEngine = engine;
5    }
6    this.defaults = {};
7    this.displayList = [];
8    this.displayDict = {};
9    this.start = null;
10    this.time = 0;
11    this.timeline = [];
12    this.timelineIndex = 0;
13    this.requestID = null;
14    this.paused = false;
15    this.displayEngine = 'undefined' === typeof engine ? this.defaultEngine : engine;
16}
17
18function addActions(frame, timeline) {
19    var keyframe = keyframes[frame];
20    var len = keyframe.length;
21    for (var i = 0; i < len; ++i) {
22        var action = keyframe[i];
23        loopOver(action, timeline);
24    }
25}
26
27function animateList(now) {
28    if (animationState.paused) {
29        return;
30    }
31    if (animationState.start == null) {
32        animationState.start = now - animationState.time;
33    }
34    animationState.time = now - animationState.start;
35    var stillAnimating = false;
36    for (var index = animationState.timelineIndex; index < animationState.timeline.length; ++index) {
37        var animation = animationState.timeline[index];
38        if (animation.time > animationState.time) {
39            stillAnimating = true;
40            break;
41        }
42        if (animation.time + animation.duration < animationState.time) {
43            if (animation.finalized) {
44                continue;
45            }
46            animation.finalized = true;
47        }
48        stillAnimating = true;
49        var actions = animation.actions;
50        for (var aIndex = 0; aIndex < actions.length; ++aIndex) {
51            var action = actions[aIndex];
52            var hasDraw = 'draw' in action;
53            var hasRef = 'ref' in action;
54            var displayIndex;
55            if (hasDraw) {
56                var ref = hasRef ? action.ref : "anonymous_" + index + "_" + aIndex;
57                assert('string' == typeof(ref));
58                if (ref in animationState.displayDict) {
59                    displayIndex = animationState.displayDict[ref];
60                } else {
61                    assert('string' == typeof(action.draw));
62                    var draw = (new Function("return " + action.draw))();
63                    assert('object' == typeof(draw));
64                    var paint;
65                    if ('paint' in action) {
66                        assert('string' == typeof(action.paint));
67                        paint = (new Function("return " + action.paint))();
68                        assert('object' == typeof(paint) && !isArray(paint));
69                    } else {
70                        paint = animationState.defaults.paint;
71                    }
72                    displayIndex = animationState.displayList.length;
73                    animationState.displayList.push( { "ref":ref, "draw":draw, "paint":paint,
74                        "drawSpec":action.draw, "paintSpec":action.paint,
75                        "drawCopied":false, "paintCopied":false,
76                        "drawDirty":true, "paintDirty":true, "once":false } );
77                    animationState.displayDict[ref] = displayIndex;
78                }
79            } else if (hasRef) {
80                assert('string' == typeof(action.ref));
81                displayIndex = animationState.displayDict[action.ref];
82            } else {
83                assert(actions.length == 1);
84                for (var prop in action) {
85                    if ('paint' == prop) {
86                        assert('string' == typeof(action[prop]));
87                        var obj = (new Function("return " + action[prop]))();
88                        assert('object' == typeof(obj) && !isArray(obj));
89                        animationState.defaults[prop] = obj;
90                    } else {
91                        animationState.defaults[prop] = action[prop];
92                    }
93                }
94                continue;
95            }
96            var targetSpec = 'target' in action ? action.target : animationState.defaults.target;
97            assert(targetSpec);
98            assert('string' == typeof(targetSpec));
99            assert(displayIndex < animationState.displayList.length);
100            var display = animationState.displayList[displayIndex];
101            var modDraw = targetSpec.startsWith('draw');
102            assert(modDraw || targetSpec.startsWith('paint'));
103            var modType = modDraw ? "draw" : "paint";
104            var copied = modDraw ? display.drawCopied : action.paintCopied;
105            if (!copied) {
106                var copy;
107                if (!modDraw || display.drawSpec.startsWith("text")) {
108                    copy = {};
109                    var original = modDraw ? display.draw : display.paint;
110                    for (var p in original) {
111                        copy[p] = original[p];
112                    }
113                } else if (display.drawSpec.startsWith("paths")) {
114                    copy = [];
115                    for (var i = 0; i < display.draw.length; ++i) {
116                        var curves = display.draw[i];
117                        var curve = Object.keys(curves)[0];
118                        copy[i] = {};
119                        copy[i][curve] = curves[curve].slice(0);  // clone the array of curves
120                    }
121                } else {
122                    assert(display.drawSpec.startsWith("pictures"));
123                    copy = [];
124                    for (var i = 0; i < display.draw.length; ++i) {
125                        var entry = display.draw[i];
126                        copy[i] = { "draw":entry.draw, "paint":entry.paint };
127                    }
128                }
129                display[modType] = copy;
130                display[modType + "Copied"] = true;
131            }
132            var targetField, targetObject, fieldOffset;
133            if (targetSpec.endsWith("]")) {
134                fieldOffset = targetSpec.lastIndexOf("[");
135                assert(fieldOffset >= 0);
136                targetField = targetSpec.substring(fieldOffset + 1, targetSpec.length - 1);
137                var arrayIndex = +targetField;
138                if (!isNaN(arrayIndex) && targetField.length > 0) {
139                    targetField = arrayIndex;
140                }
141
142            } else {
143                fieldOffset = targetSpec.lastIndexOf(".");
144                if (fieldOffset >= 0) {
145                    targetField = targetSpec.substring(fieldOffset + 1, targetSpec.length);
146                } else {
147                    targetObject = display;
148                    targetField = targetSpec;
149                }
150            }
151            if (fieldOffset >= 0) {
152                var sub = targetSpec.substring(0, fieldOffset);
153                targetObject = (new Function('display', "return display." + sub))(display);
154            }
155            assert(null != targetObject[targetField]);
156            if (!('start' in action) || action.start < animation.time) {
157                for (var p in animationState.defaults) {
158                    if ('draw' == p || 'paint' == p || 'ref' == p) {
159                        continue;
160                    }
161                    assert('range' == p || 'target' == p || 'formula' == p || 'params' == p);
162                    if (!(p in action)) {
163                        action[p] = animationState.defaults[p];
164                    }
165                }
166                if ('number' == typeof(action.formula)) {
167                    targetObject[targetField] = action.formula;
168                    action.once = true;
169                }
170                action.start = animation.time;
171            }
172            if (action.once) {
173                continue;
174            }
175            var value = Math.min(1, (animationState.time - animation.time) / animation.duration);
176            var scaled = action.range[0] + (action.range[1] - action.range[0]) * value;
177            if ('params' in action) {
178                if (!('func' in action)) {
179                    if (isArray(action.params)) {
180                        action.funcParams = [];
181                        var len = action.params.length;
182                        for (var i = 0; i < len; ++i) {
183                            action.funcParams[i] = 'target' == action.params[i]
184                                ? targetObject[targetField]
185                                : (new Function("return " + action.params[i]))();
186                        }
187                    } else {
188                        action.funcParams = 'target' == action.params
189                                ? targetObject[targetField]
190                                : (new Function("return " + action.params))();
191                    }
192                    assert('formula' in action && 'string' == typeof(action.formula));
193                    // evaluate inline function to get value
194                    action.func = new Function('value', 'params', "return " + action.formula);
195                }
196                scaled = action.func(scaled, action.funcParams);
197            }
198            if (targetObject[targetField] != scaled) {
199                if (modDraw) {
200                    display.drawDirty = true;
201                } else {
202                    display.paintDirty = true;
203                }
204                targetObject[targetField] = scaled;
205            }
206        }
207    }
208    displayBackend(animationState.displayEngine, animationState.displayList);
209
210    if (stillAnimating) {
211        animationState.requestID = requestAnimationFrame(animateList);
212    }
213}
214
215function flattenPaint(paint) {
216    if (!paint.paint) {
217        return;
218    }
219    var parent = paints[paint.paint];
220    flattenPaint(parent);
221    for (var prop in parent) {
222        if (!(prop in paint)) {
223            paint[prop] = parent[prop];
224        }
225    }
226    paint.paint = null;
227}
228
229function init(engine, keyframe) {
230    animationState.reset(engine);
231    setupPaint();
232    setupBackend(animationState.displayEngine);
233    keyframeInit(keyframe);
234}
235
236function keyframeInit(frame) {
237    animationState.reset();
238    addActions("_default", animationState.timeline);
239    addActions(frame, animationState.timeline);
240    for (var index = 0; index < animationState.timeline.length; ++index) {
241        animationState.timeline[index].position = index;
242    }
243    animationState.timeline.sort(function(a, b) {
244        if (a.time == b.time) {
245            return a.position - b.position;
246        }
247        return a.time - b.time;
248    });
249    keyframeBackendInit(animationState.displayEngine, animationState.displayList,
250            keyframes[frame][0]);
251    animationState.requestID = requestAnimationFrame(animateList);
252}
253
254function loopAddProp(action, propName) {
255    var funcStr = "";
256    var prop = action[propName];
257    if ('draw' != propName && isArray(prop)) {
258        funcStr += '[';
259        for (var index = 0; index < prop.length; ++index) {
260            funcStr += loopAddProp(prop, index);
261            if (index + 1 < prop.length) {
262                funcStr += ", ";
263            }
264        }
265        funcStr += ']';
266        return funcStr;
267    }
268    assert("object" != typeof(prop));
269    var useString = "string" == typeof(prop) && isAlpha(prop.charCodeAt(0));
270    if (useString) {
271        funcStr += "'";
272    }
273    funcStr += prop;
274    if (useString) {
275        funcStr += "'";
276    }
277    return funcStr;
278}
279
280function loopOver(rec, timeline) {
281    var funcStr = "";
282    if (rec.for) {
283        funcStr += "for (" + rec.for[0] + "; " + rec.for[1] + "; " + rec.for[2] + ") {\n";
284    }
285    funcStr += "    var time = " + ('time' in rec ? rec.time : 0) + ";\n";
286    funcStr += "    var duration = " + ('duration' in rec ? rec.duration : 0) + ";\n";
287    funcStr += "    var actions = [];\n";
288    var len = rec.actions.length;
289    for (var i = 0; i < len; ++i) {
290        funcStr += "    var action" + i + " = {\n";
291        var action = rec.actions[i];
292        for (var p in action) {
293            funcStr += "        '" + p + "':";
294            funcStr += loopAddProp(action, p);
295            funcStr += ",\n";
296        }
297        funcStr = funcStr.substring(0, funcStr.length - 2);
298        funcStr += "\n    };\n";
299        funcStr += "    actions.push(action" + i + ");\n";
300    }
301    funcStr += "    timeline.push( { 'time':time, 'duration':duration, 'actions':actions,"
302            + "'finalized':false } );\n";
303    if (rec.for) {
304        funcStr += "}\n";
305    }
306    var func = new Function('rec', 'timeline', funcStr);
307    func(rec, timeline);
308}
309
310function setupPaint() {
311    for (var prop in paints) {
312        flattenPaint(paints[prop]);
313    }
314}
315