runs some sample animations side by side in canvas and svg

TBR=fmalita@chromium.org

Review URL: https://codereview.chromium.org/1342523002
diff --git a/experimental/docs/animationCommon.js b/experimental/docs/animationCommon.js
new file mode 100644
index 0000000..1733ec2
--- /dev/null
+++ b/experimental/docs/animationCommon.js
@@ -0,0 +1,314 @@
+var animationState = {};
+animationState.reset = function (engine) {
+    if ('string' === typeof engine) {
+        this.defaultEngine = engine;
+    }
+    this.defaults = {};
+    this.displayList = [];
+    this.displayDict = {};
+    this.start = null;
+    this.time = 0;
+    this.timeline = [];
+    this.timelineIndex = 0;
+    this.requestID = null;
+    this.paused = false;
+    this.displayEngine = 'undefined' === typeof engine ? this.defaultEngine : engine;
+}
+
+function addActions(frame, timeline) {
+    var keyframe = keyframes[frame];
+    var len = keyframe.length;
+    for (var i = 0; i < len; ++i) {
+        var action = keyframe[i];
+        loopOver(action, timeline);
+    }
+}
+
+function animateList(now) {
+    if (animationState.paused) {
+        return;
+    }
+    if (animationState.start == null) {
+        animationState.start = now - animationState.time;
+    }
+    animationState.time = now - animationState.start;
+    var stillAnimating = false;
+    for (var index = animationState.timelineIndex; index < animationState.timeline.length; ++index) {
+        var animation = animationState.timeline[index];
+        if (animation.time > animationState.time) {
+            stillAnimating = true;
+            break;
+        }
+        if (animation.time + animation.duration < animationState.time) {
+            if (animation.finalized) {
+                continue;
+            }
+            animation.finalized = true;
+        }
+        stillAnimating = true;
+        var actions = animation.actions;
+        for (var aIndex = 0; aIndex < actions.length; ++aIndex) {
+            var action = actions[aIndex];
+            var hasDraw = 'draw' in action;
+            var hasRef = 'ref' in action;
+            var displayIndex;
+            if (hasDraw) {
+                var ref = hasRef ? action.ref : "anonymous_" + index + "_" + aIndex;
+                assert('string' == typeof(ref));
+                if (ref in animationState.displayDict) {
+                    displayIndex = animationState.displayDict[ref];
+                } else {
+                    assert('string' == typeof(action.draw));
+                    var draw = (new Function("return " + action.draw))();
+                    assert('object' == typeof(draw));
+                    var paint;
+                    if ('paint' in action) {
+                        assert('string' == typeof(action.paint));
+                        paint = (new Function("return " + action.paint))();
+                        assert('object' == typeof(paint) && !isArray(paint));
+                    } else {
+                        paint = animationState.defaults.paint;
+                    }
+                    displayIndex = animationState.displayList.length;
+                    animationState.displayList.push( { "ref":ref, "draw":draw, "paint":paint,
+                        "drawSpec":action.draw, "paintSpec":action.paint,
+                        "drawCopied":false, "paintCopied":false,
+                        "drawDirty":true, "paintDirty":true, "once":false } );
+                    animationState.displayDict[ref] = displayIndex;
+                }
+            } else if (hasRef) {
+                assert('string' == typeof(action.ref));
+                displayIndex = animationState.displayDict[action.ref];
+            } else {
+                assert(actions.length == 1);
+                for (var prop in action) {
+                    if ('paint' == prop) {
+                        assert('string' == typeof(action[prop]));
+                        var obj = (new Function("return " + action[prop]))();
+                        assert('object' == typeof(obj) && !isArray(obj));
+                        animationState.defaults[prop] = obj;
+                    } else {
+                        animationState.defaults[prop] = action[prop];
+                    }
+                }
+                continue;
+            }
+            var targetSpec = 'target' in action ? action.target : animationState.defaults.target;
+            assert(targetSpec);
+            assert('string' == typeof(targetSpec));
+            assert(displayIndex < animationState.displayList.length);
+            var display = animationState.displayList[displayIndex];
+            var modDraw = targetSpec.startsWith('draw');
+            assert(modDraw || targetSpec.startsWith('paint'));
+            var modType = modDraw ? "draw" : "paint";
+            var copied = modDraw ? display.drawCopied : action.paintCopied;
+            if (!copied) {
+                var copy;
+                if (!modDraw || display.drawSpec.startsWith("text")) {
+                    copy = {};
+                    var original = modDraw ? display.draw : display.paint;
+                    for (var p in original) {
+                        copy[p] = original[p];
+                    }
+                } else if (display.drawSpec.startsWith("paths")) {
+                    copy = [];
+                    for (var i = 0; i < display.draw.length; ++i) {
+                        var curves = display.draw[i];
+                        var curve = Object.keys(curves)[0];
+                        copy[i] = {};
+                        copy[i][curve] = curves[curve].slice(0);  // clone the array of curves
+                    }
+                } else {
+                    assert(display.drawSpec.startsWith("pictures"));
+                    copy = [];
+                    for (var i = 0; i < display.draw.length; ++i) {
+                        var entry = display.draw[i];
+                        copy[i] = { "draw":entry.draw, "paint":entry.paint };
+                    }
+                }
+                display[modType] = copy;
+                display[modType + "Copied"] = true;
+            }
+            var targetField, targetObject, fieldOffset;
+            if (targetSpec.endsWith("]")) {
+                fieldOffset = targetSpec.lastIndexOf("[");
+                assert(fieldOffset >= 0);
+                targetField = targetSpec.substring(fieldOffset + 1, targetSpec.length - 1);
+                var arrayIndex = +targetField;
+                if (!isNaN(arrayIndex) && targetField.length > 0) {
+                    targetField = arrayIndex;
+                }
+
+            } else {
+                fieldOffset = targetSpec.lastIndexOf(".");
+                if (fieldOffset >= 0) {
+                    targetField = targetSpec.substring(fieldOffset + 1, targetSpec.length);
+                } else {
+                    targetObject = display;
+                    targetField = targetSpec;
+                }
+            }
+            if (fieldOffset >= 0) {
+                var sub = targetSpec.substring(0, fieldOffset);
+                targetObject = (new Function('display', "return display." + sub))(display);
+            }
+            assert(null != targetObject[targetField]);
+            if (!('start' in action) || action.start < animation.time) {
+                for (var p in animationState.defaults) {
+                    if ('draw' == p || 'paint' == p || 'ref' == p) {
+                        continue;
+                    }
+                    assert('range' == p || 'target' == p || 'formula' == p || 'params' == p);
+                    if (!(p in action)) {
+                        action[p] = animationState.defaults[p];
+                    }
+                }
+                if ('number' == typeof(action.formula)) {
+                    targetObject[targetField] = action.formula;
+                    action.once = true;
+                }
+                action.start = animation.time;
+            }
+            if (action.once) {
+                continue;
+            }
+            var value = Math.min(1, (animationState.time - animation.time) / animation.duration);
+            var scaled = action.range[0] + (action.range[1] - action.range[0]) * value;
+            if ('params' in action) {
+                if (!('func' in action)) {
+                    if (isArray(action.params)) {
+                        action.funcParams = [];
+                        var len = action.params.length;
+                        for (var i = 0; i < len; ++i) {
+                            action.funcParams[i] = 'target' == action.params[i]
+                                ? targetObject[targetField]
+                                : (new Function("return " + action.params[i]))();
+                        }
+                    } else {
+                        action.funcParams = 'target' == action.params
+                                ? targetObject[targetField]
+                                : (new Function("return " + action.params))();
+                    }
+                    assert('formula' in action && 'string' == typeof(action.formula));
+                    // evaluate inline function to get value
+                    action.func = new Function('value', 'params', "return " + action.formula);
+                }
+                scaled = action.func(scaled, action.funcParams);
+            }
+            if (targetObject[targetField] != scaled) {
+                if (modDraw) {
+                    display.drawDirty = true;
+                } else {
+                    display.paintDirty = true;
+                }
+                targetObject[targetField] = scaled;
+            }
+        }
+    }
+    displayBackend(animationState.displayEngine, animationState.displayList);
+
+    if (stillAnimating) {
+        animationState.requestID = requestAnimationFrame(animateList);
+    }
+}
+
+function flattenPaint(paint) {
+    if (!paint.paint) {
+        return;
+    }
+    var parent = paints[paint.paint];
+    flattenPaint(parent);
+    for (var prop in parent) {
+        if (!(prop in paint)) {
+            paint[prop] = parent[prop];
+        }
+    }
+    paint.paint = null;
+}
+
+function init(engine, keyframe) {
+    animationState.reset(engine);
+    setupPaint();
+    setupBackend(animationState.displayEngine);
+    keyframeInit(keyframe);
+}
+
+function keyframeInit(frame) {
+    animationState.reset();
+    addActions("_default", animationState.timeline);
+    addActions(frame, animationState.timeline);
+    for (var index = 0; index < animationState.timeline.length; ++index) {
+        animationState.timeline[index].position = index;
+    }
+    animationState.timeline.sort(function(a, b) {
+        if (a.time == b.time) {
+            return a.position - b.position;
+        }
+        return a.time - b.time;
+    });
+    keyframeBackendInit(animationState.displayEngine, animationState.displayList,
+            keyframes[frame][0]);
+    animationState.requestID = requestAnimationFrame(animateList);
+}
+
+function loopAddProp(action, propName) {
+    var funcStr = "";
+    var prop = action[propName];
+    if ('draw' != propName && isArray(prop)) {
+        funcStr += '[';
+        for (var index = 0; index < prop.length; ++index) {
+            funcStr += loopAddProp(prop, index);
+            if (index + 1 < prop.length) {
+                funcStr += ", ";
+            }
+        }
+        funcStr += ']';
+        return funcStr;
+    }
+    assert("object" != typeof(prop));
+    var useString = "string" == typeof(prop) && isAlpha(prop.charCodeAt(0));
+    if (useString) {
+        funcStr += "'";
+    }
+    funcStr += prop;
+    if (useString) {
+        funcStr += "'";
+    }
+    return funcStr;
+}
+
+function loopOver(rec, timeline) {
+    var funcStr = "";
+    if (rec.for) {
+        funcStr += "for (" + rec.for[0] + "; " + rec.for[1] + "; " + rec.for[2] + ") {\n";
+    }
+    funcStr += "    var time = " + ('time' in rec ? rec.time : 0) + ";\n";
+    funcStr += "    var duration = " + ('duration' in rec ? rec.duration : 0) + ";\n";
+    funcStr += "    var actions = [];\n";
+    var len = rec.actions.length;
+    for (var i = 0; i < len; ++i) {
+        funcStr += "    var action" + i + " = {\n";
+        var action = rec.actions[i];
+        for (var p in action) {
+            funcStr += "        '" + p + "':";
+            funcStr += loopAddProp(action, p);
+            funcStr += ",\n";
+        }
+        funcStr = funcStr.substring(0, funcStr.length - 2);
+        funcStr += "\n    };\n";
+        funcStr += "    actions.push(action" + i + ");\n";
+    }
+    funcStr += "    timeline.push( { 'time':time, 'duration':duration, 'actions':actions,"
+            + "'finalized':false } );\n";
+    if (rec.for) {
+        funcStr += "}\n";
+    }
+    var func = new Function('rec', 'timeline', funcStr);
+    func(rec, timeline);
+}
+
+function setupPaint() {
+    for (var prop in paints) {
+        flattenPaint(paints[prop]);
+    }
+}
diff --git a/experimental/docs/backend.js b/experimental/docs/backend.js
new file mode 100644
index 0000000..efb8e52
--- /dev/null
+++ b/experimental/docs/backend.js
@@ -0,0 +1,44 @@
+function displayBackend(displayEngine, displayList) {
+    switch (displayEngine) {
+        case 'all':
+            displayCanvas(displayList);
+            displaySvg(displayList);
+            break;
+        case 'Canvas':
+            displayCanvas(displayList);
+            break;
+        case 'SVG':
+            displaySvg(displayList);
+            break;
+        default:
+            assert(0);
+    }
+}
+
+function keyframeBackendInit(displayEngine, displayList, first) {
+    switch (displayEngine) {
+        case 'all':
+        case 'Canvas':
+            keyframeCanvasInit(displayList, first);
+            break;
+        case 'SVG':
+            break;
+        default:
+            assert(0);
+    }
+}
+
+function setupBackend(displayEngine) {
+    switch (displayEngine) {
+        case 'all':
+        case 'Canvas':
+            setupCanvas();
+            setupSvg();
+            break;
+        case 'SVG':
+            setupSvg();
+            break;
+        default:
+            assert(0);
+    }
+}
diff --git a/experimental/docs/canvasBackend.js b/experimental/docs/canvasBackend.js
new file mode 100644
index 0000000..0574813
--- /dev/null
+++ b/experimental/docs/canvasBackend.js
@@ -0,0 +1,167 @@
+var canvas;
+var ctx;
+var canvasGradients = {};
+
+function canvas_rbga(color) {
+    var a = canvas_opacity(color);
+    var r = (color >> 16) & 0xFF;
+    var g = (color >>  8) & 0xFF;
+    var b = (color >>  0) & 0xFF;
+    return "rgba(" + r + "," + g + "," + b + "," + a + ")";
+}
+
+function canvas_opacity(color) {
+    var a = (color >> 24) & 0xFF;
+    return a / 255.;
+}
+
+function displayCanvas(displayList) {
+    if (displayList.clear) {
+        ctx.clearRect(0, 0, canvas.width, canvas.height);
+    }
+    for (var index = 0; index < displayList.length; ++index) {
+        drawToCanvas(displayList[index]);
+    }
+}
+
+function drawToCanvas(action) {
+    ctx.save();
+    var paint = paintToCanvas(action.paint);
+    var draw = action.draw;
+    if ('string' == typeof(draw)) {
+        draw = (new Function("return " + draw))();
+    }
+    if (isArray(draw)) {
+        assert(draw.length > 0);
+        var picture = 'draw' in draw[0];
+        if (picture) {
+            for (var index = 0; index < draw.length; ++index) {
+                drawToCanvas(draw[index]);
+            }
+            return;
+        }
+        ctx.beginPath();
+        for (var index = 0; index < draw.length; ++index) {
+            for (var prop in draw[index]) {
+                var v = draw[index][prop];
+                switch (prop) {
+                    case 'arcTo':
+                        ctx.arcTo(v[0], v[1], v[2], v[3], v[4]);
+                        break;
+                    case 'close':
+                        ctx.closePath();
+                        break;
+                    case 'cubic':
+                        ctx.moveTo(v[0], v[1]);
+                        ctx.bezierCurveTo(v[2], v[3], v[4], v[5], v[6], v[7]);
+                        break;
+                    case 'line':
+                        ctx.moveTo(v[0], v[1]);
+                        ctx.lineTo(v[2], v[3]);
+                        break;
+                    case 'quad':
+                        ctx.moveTo(v[0], v[1]);
+                        ctx.quadraticCurveTo(v[2], v[3], v[4], v[5]);
+                        break;
+                    default:
+                        assert(0);
+                }
+            }
+        }
+        if ('fill' == paint.style) {
+            ctx.fill();
+        } else {
+            assert('stroke' == paint.style);
+            ctx.stroke();
+        }
+    } else {
+        assert('string' in draw);
+        if ('fill' == paint.style) {
+            ctx.fillText(draw.string, draw.x, draw.y);
+        } else {
+            assert('stroke' == paint.style);
+            ctx.strokeText(draw.string, draw.x, draw.y);
+        }
+    }
+    ctx.restore();
+}
+
+function keyframeCanvasInit(displayList, first) {
+    if ('canvas' in first && 'clear' == first.canvas) {
+        displayList.clear = true;
+    }
+}
+
+function paintToCanvas(paint) {
+    var color;
+    var inPicture = 'string' == typeof(paint);
+    if (inPicture) {
+        paint = (new Function("return " + paint))();
+        assert('object' == typeof(paint) && !isArray(paint));
+    }
+    if ('gradient' in paint) {
+        var gradient = paint.gradient.split('.');
+        var gradName = gradient[1];
+        if (!canvasGradients[gradName]) {
+            var g = window[gradient[0]][gradient[1]];
+            var grad = ctx.createRadialGradient(g.cx, g.cy, 0, g.cx, g.cy, g.r);
+            var stopLen = g.stops.length;
+            for (var index = 0; index < stopLen; ++index) {
+                var stop = g.stops[index];
+                var color = canvas_rbga(stop.color);
+                grad.addColorStop(index, color);
+            }
+            canvasGradients[gradName] = grad;
+        }
+        color = canvasGradients[gradName];
+        if (!inPicture) {
+            ctx.globalAlpha = canvas_opacity(paint.color);
+        }
+    } else {
+        color = canvas_rbga(paint.color);
+    }
+    if ('fill' == paint.style) {
+        ctx.fillStyle = color;
+    } else if ('stroke' == paint.style) {
+        ctx.strokeStyle = color;
+    } else {
+        ctx.globalAlpha = canvas_opacity(paint.color);
+    }
+    if ('strokeWidth' in paint) {
+        ctx.lineWidth = paint.strokeWidth;
+    }
+    if ('typeface' in paint) {
+        var typeface = typefaces[paint.typeface];
+        var font = typeface.style;
+        if ('textSize' in paint) {
+            font += " " + paint.textSize;
+        }
+        if ('family' in typeface) {
+            font += " " + typeface.family;
+        }
+        ctx.font = font;
+        if ('textAlign' in paint) {
+            ctx.textAlign = paint.textAlign;
+        }
+        if ('textBaseline' in paint) {
+            ctx.textBaseline = paint.textBaseline;
+        }
+    }
+    return paint;
+}
+
+function setupCanvas() {
+    canvas = document.getElementById("canvas");
+    ctx = canvas ? canvas.getContext("2d") : null;
+    assert(ctx);
+    var resScale = window.devicePixelRatio ? window.devicePixelRatio : 1;
+    var unscaledWidth = canvas.width;
+    var unscaledHeight = canvas.height;
+    canvas.width = unscaledWidth * resScale;
+    canvas.height = unscaledHeight * resScale;
+    canvas.style.width = unscaledWidth + 'px';
+    canvas.style.height = unscaledHeight + 'px';
+    if (resScale != 1) {
+        ctx.scale(resScale, resScale);
+    }
+}
diff --git a/experimental/docs/exampleSlides.js b/experimental/docs/exampleSlides.js
new file mode 100644
index 0000000..d1a10b6
--- /dev/null
+++ b/experimental/docs/exampleSlides.js
@@ -0,0 +1,564 @@
+var circle = {
+    "center":{ "x":200, "y":200 },
+    "radius":100
+}
+
+var gradients = {
+    "grad1": { "cx":200, "cy":200, "r":300,
+        "stops": [
+            { "offset":0, "color": argb(76,0,0,255) },
+            { "offset":1, "color": argb( 0,0,0,255) }
+        ]
+    },
+    "grad2": { "cx":200, "cy":200, "r":300,
+        "stops": [
+            { "offset":0, "color": argb(76,0,255,0) },
+            { "offset":1, "color": argb( 0,0,255,0) }
+        ]
+    },
+    "grad3": { "cx":200, "cy":200, "r":300,
+        "stops": [
+            { "offset":0, "color": argb(76,255,0,0) },
+            { "offset":1, "color": argb( 0,255,0,0) }
+        ]
+    },
+    "grad4": { "cx":200, "cy":200, "r":300,
+        "stops": [
+            { "offset":0, "color": argb(76,192,63,192) },
+            { "offset":1, "color": argb( 0,192,63,192) }
+        ]
+    },
+    "grad5": { "cx":200, "cy":200, "r":300,
+        "stops": [
+            { "offset":0, "color": argb(76,127,127,0) },
+            { "offset":1, "color": argb( 0,127,127,0) }
+        ]
+    },
+    "grad6": { "cx":200, "cy":200, "r":300,
+        "stops": [
+            { "offset":0, "color": argb(76,127,0,127) },
+            { "offset":1, "color": argb( 0,127,0,127) }
+        ]
+    },
+    "grad7": { "cx":200, "cy":200, "r":300,
+        "stops": [
+            { "offset":0, "color": argb(76,0,127,127) },
+            { "offset":1, "color": argb( 0,0,127,127) }
+        ]
+    },
+    "grad8": { "cx":200, "cy":200, "r":300,
+        "stops": [
+            { "offset":0, "color": argb(76,63,192,63) },
+            { "offset":1, "color": argb( 0,63,192,63) }
+        ]
+    }
+};
+
+var paths = {
+    "cubicSegment1": [
+        { "cubic": [ 200,200, 200,200, 200,200, 200,200 ] }
+    ],
+    "cubicSegment2": [
+        { "cubic": [ 200,200, 250,200, 300,200, 300,100 ] }
+    ],
+    "curveSegment1": [
+        { "cubic": [ 200,200, 250,200, 300,150, 300,100 ] }
+    ],
+    "curveSegment2": [
+        { "cubic": [ 200,200, 250,200, 300,150, 200,100 ] }
+    ],
+    "curveSegment3": [
+        { "cubic": [ 200,200, 350,200, 250,-150, 170,300 ] }
+    ],
+    "diagSegment": [
+        { "line":  [ 200,200, 100,100 ] }
+    ],
+    "horzSegment": [
+        { "line":  [ 200,200, 341.4,200 ] }
+    ],
+    "lineSegment": [
+        { "line":  [ 200,200, 200 + circle.radius * Math.cos(-22.5 * Math.PI / 180),
+                              200 + circle.radius * Math.sin(-22.5 * Math.PI / 180) ] }
+    ],
+    "span1": [
+        { "quad":  [ 200,200, 300,300, 200,300 ] }
+    ],
+    "span2": [
+        { "cubic": [ 200,200, 100,300, 100,400, 200,300 ] }
+    ],
+    "span3": [
+        { "cubic": [ 200,200, 300,100, 100,400, 300,200 ] }
+    ],
+    "span4": [
+        { "quad":  [ 200,200, 300,300, 400,300 ] }
+    ],
+    "span5": [
+        { "quad":  [ 200,200, 280,320, 200,400 ] }
+    ],
+    "span6": [
+        { "quad":  [ 200,200, 60,340, 100,400 ] }
+    ],
+    "vertSegment": [
+        { "line":  [ 200,200, 200,341.4 ] }
+    ],
+    "wedge1": [
+        { "line":  [ 200,200, 500,500 ] },
+        { "arcTo": [ 375.74,624.36, 200,624.26, 424.26 ] },
+        { "close": null }
+    ],
+    "wedge2": [
+        { "line":  [ 200,200, 200,624.26 ] },
+        { "arcTo": [ 24.265,624.26, -100,500, 424.26 ] },
+        { "close": null }
+    ],
+    "wedge3": [
+        { "line":  [ 200,200, 500,-100 ] },
+        { "arcTo": [ 1138.22,537.70, 240,622.5, 424.26 ] },
+        { "close": null }
+    ],
+    "wedge4": [
+        { "line":  [ 200,200, 500,500 ] },
+        { "arcTo": [ 530.79,438.42, 579.47,389.74, 424.26 ] },
+        { "close": null }
+    ],
+    "wedge5": [
+        { "line":  [ 200,200, 389.74,579.47 ] },
+        { "arcTo": [ 284.94,563.441, 200,500, 424.26 ] },
+        { "close": null }
+    ],
+    "wedge6": [
+        { "line":  [ 200,200, 10.26,579.47 ] },
+        { "arcTo": [ -51.318,548.68, -100,500, 424.26 ] },
+        { "close": null }
+    ],
+    "wedgeXY1": [
+        { "line":  [ 200,200, 500,-100 ] },
+        { "arcTo": [ 624.26,24.265, 624.26,200, 424.26 ] },
+        { "close": null }
+    ],
+    "wedgeXY2": [
+        { "line":  [ 200,200, 200,-175.74 ] },
+        { "arcTo": [ 364.83,-196.61, 500,-100, 424.26 ] },
+        { "close": null }
+    ],
+    "wedgeXY3": [
+        { "line":  [ 200,200, -100,-100 ] },
+        { "arcTo": [ 35.170,-196.61, 200,-175.74, 424.26 ] },
+        { "close": null }
+    ],
+    "wedgeXY4": [
+        { "line":  [ 200,200, -175.74,200 ] },
+        { "arcTo": [ -196.61,35.170, -100,-100, 424.26 ] },
+        { "close": null }
+    ],
+    "wedgeXY5": [
+        { "line":  [ 200,200, -100,500 ] },
+        { "arcTo": [ -196.61,364.83, -175.74,200, 424.26 ] },
+        { "close": null }
+    ],
+    "wedgeXY6": [
+        { "line":  [ 200,200, -100,500 ] },
+        { "arcTo": [ 75.735,500, 200,624.26, 424.26 ] },
+        { "close": null }
+    ],
+    "wedgeXY7": [
+        { "line":  [ 200,200, 200,624.26 ] },
+        { "arcTo": [ 324.26,500, 500,500, 424.26 ] },
+        { "close": null }
+    ],
+    "wedgeXY8": [
+        { "line":  [ 200,200, 500,500 ] },
+        { "arcTo": [ 500,324.26, 624.26,200, 424.26 ] },
+        { "close": null }
+    ],
+    "xaxis": [
+        { "line":  [ 100,200, 300,200 ] }
+    ],
+    "yaxis": [
+        { "line":  [ 200,100, 200,300 ] }
+    ]
+};
+
+var text = {
+    "curve1d1": {
+        "string":"Some curves initially occupy", "x":400, "y":200
+    },
+    "curve1d2": {
+        "string":"one-dimensional sectors, then diverge.", "x":400, "y":240
+    },
+    "curveMultiple1": {
+        "string":"A curve span may cover more", "x":400, "y":200
+    },
+    "curveMultiple2": {
+        "string":"than one sector.", "x":400, "y":240
+    },
+    "line1DDest1": {
+        "string":"Some lines occupy one-dimensional", "x":400, "y":200
+    },
+    "line1DDest2": {
+        "string":"sectors.", "x":400, "y":240
+    },
+    "lineSingle": {
+        "string":"Line spans are contained by a single sector.", "x":400, "y":200
+    },
+    "sector1": {
+        "string":"A sector is a wedge of a circle", "x":400, "y":200
+    },
+    "sector2": {
+        "string":"containing a range of points.", "x":400, "y":240
+    },
+    "sectorXY1": {
+        "string":"X > 0   Y < 0   -Y < X", "x":500, "y":460
+    },
+    "sectorXY2": {
+        "string":"X > 0   Y < 0   -Y > X", "x":500, "y":460
+    },
+    "sectorXY3": {
+        "string":"X < 0   Y < 0    Y < X", "x":500, "y":460
+    },
+    "sectorXY4": {
+        "string":"X < 0   Y < 0    Y > X", "x":500, "y":460
+    },
+    "sectorXY5": {
+        "string":"X < 0   Y > 0   -Y > X", "x":500, "y":460
+    },
+    "sectorXY6": {
+        "string":"X < 0   Y > 0   -Y < X", "x":500, "y":460
+    },
+    "sectorXY7": {
+        "string":"X > 0   Y > 0    Y > X", "x":500, "y":460
+    },
+    "sectorXY8": {
+        "string":"X > 0   Y > 0    Y < X", "x":500, "y":460
+    },
+    "sectorXY9": {
+        "string":"X > 0   Y == 0", "x":500, "y":460
+    },
+    "sectorXY10": {
+        "string":"Y > 0   0 == X", "x":500, "y":460
+    },
+    "sectorXY11": {
+        "string":"X < 0   Y == X", "x":500, "y":460
+    },
+    "sectorXYA": {
+        "string":"X > 0   Y > 0    Y < X", "x":500, "y":310
+    },
+    "sectorXYB": {
+        "string":"X < 0   Y > 0   -Y < X", "x":500, "y":360
+    },
+    "sectorXYC": {
+        "string":"X < 0   Y < 0    Y < X", "x":500, "y":410
+    },
+    "spanWedge": {
+        "string":"All spans are contained by a wedge", "x":400, "y":200
+    },
+    "trivialWedge1": {
+        "string":"Wedges that don't overlap can be", "x":400, "y":200
+    },
+    "trivialWedge2": {
+        "string":"easily sorted.", "x":400, "y":240
+    },
+    "xaxis1": {
+        "string":"-X", "x":100, "y":220
+    },
+    "xaxis2": {
+        "string":"+X", "x":300, "y":220
+    },
+    "yaxis1": {
+        "string":"-Y", "x":205, "y":100
+    },
+    "yaxis2": {
+        "string":"+Y", "x":205, "y":300
+    }
+};
+
+var typefaces = {
+    "description": { "style":"normal", "family":"Helvetica,Arial" }
+};
+
+var paints = {
+    "axisStroke":    { "style":"stroke",   "color":rgb(191,191,191) },
+    "axisTextDesc":  { "paint":"textBase", "color":rgb(191,191,191) },
+    "axisTextRight": { "paint":"axisTextDesc", "textAlign":"right" },
+    "axisTextTop":   { "paint":"axisTextDesc", "textBaseline":"hanging" },
+    "diagSegment":   { "style":"stroke",    "color":rgb(127,63,127), "strokeWidth":2 },
+    "gradient1":     { "style":"fill",      "gradient":"gradients.grad1", "color":alpha(255) },
+    "gradient2":     { "paint":"gradient1", "gradient":"gradients.grad2" },
+    "gradient3":     { "paint":"gradient1", "gradient":"gradients.grad3" },
+    "gradient4":     { "paint":"gradient1", "gradient":"gradients.grad4" },
+    "gradient5":     { "paint":"gradient1", "gradient":"gradients.grad5" },
+    "gradient6":     { "paint":"gradient1", "gradient":"gradients.grad6" },
+    "gradient7":     { "paint":"gradient1", "gradient":"gradients.grad7" },
+    "gradient8":     { "paint":"gradient1", "gradient":"gradients.grad8" },
+    "horzSegment":   { "paint":"diagSegment", "color":rgb(192,92,31) },
+    "picture":       { "color":alpha(255) },
+    "sectorADesc":   { "paint":"textBase", "color":rgb(0,0,255) },
+    "sectorBDesc":   { "paint":"textBase", "color":rgb(0,127,0) },
+    "sectorCDesc":   { "paint":"textBase", "color":rgb(255,0,0) },
+    "sectorXY1":     { "paint":"textBase", "color":rgb(192,63,192) },
+    "sectorXY2":     { "paint":"textBase", "color":rgb(127,127,0) },
+    "sectorXY3":     { "paint":"textBase", "color":rgb(255,0,0) },
+    "sectorXY4":     { "paint":"textBase", "color":rgb(127,0,127) },
+    "sectorXY5":     { "paint":"textBase", "color":rgb(0,127,127) },
+    "sectorXY6":     { "paint":"textBase", "color":rgb(0,127,0) },
+    "sectorXY7":     { "paint":"textBase", "color":rgb(63,192,63) },
+    "sectorXY8":     { "paint":"textBase", "color":rgb(0,0,255) },
+    "sectorXY9":     { "paint":"textBase", "color":rgb(192,92,31) },
+    "sectorXY10":    { "paint":"textBase", "color":rgb(31,92,192) },
+    "sectorXY11":    { "paint":"textBase", "color":rgb(127,63,127) },
+
+    "stroke":        { "style":"stroke",   "color":rgb(0,0,0) },
+    "textBase":      { "style":"fill",     "color":rgb(0,0,0), "typeface":"description",
+            "textSize":"1.3rem" },
+    "vertSegment":   { "paint":"diagSegment", "color":rgb(31,92,192) },
+};
+
+var pictures = {
+     "curve1DDestText": [
+        { "draw":"text.curve1d1", "paint":"paints.textBase" },
+        { "draw":"text.curve1d2", "paint":"paints.textBase" }
+    ],
+     "curveMultipleText": [
+        { "draw":"text.curveMultiple1", "paint":"paints.textBase" },
+        { "draw":"text.curveMultiple2", "paint":"paints.textBase" }
+    ],
+    "line1DDestText": [
+        { "draw":"text.line1DDest1", "paint":"paints.textBase" },
+        { "draw":"text.line1DDest2", "paint":"paints.textBase" }
+    ],
+    "sectorXYA": [
+        { "draw":"text.sectorXYA", "paint":"paints.sectorADesc" },
+        { "draw":"paths.wedgeXY8", "paint":"paints.gradient1" }
+    ],
+    "sectorXYB": [
+        { "draw":"text.sectorXYB", "paint":"paints.sectorBDesc" },
+        { "draw":"paths.wedgeXY6", "paint":"paints.gradient2" }
+    ],
+    "sectorXYC": [
+        { "draw":"text.sectorXYC", "paint":"paints.sectorCDesc" },
+        { "draw":"paths.wedgeXY3", "paint":"paints.gradient3" }
+    ],
+    "sectorText": [
+        { "draw":"text.sector1", "paint":"paints.textBase" },
+        { "draw":"text.sector2", "paint":"paints.textBase" }
+    ],
+    "trivialWedgeSpans": [
+        { "draw":"paths.span4", "paint":"paints.stroke" },
+        { "draw":"paths.wedge4", "paint":"paints.gradient4" },
+        { "draw":"paths.span5", "paint":"paints.stroke" },
+        { "draw":"paths.wedge5", "paint":"paints.gradient5" },
+        { "draw":"paths.span6", "paint":"paints.stroke" },
+        { "draw":"paths.wedge6", "paint":"paints.gradient6" }
+    ],
+    "trivialWedgeText": [
+        { "draw":"text.trivialWedge1", "paint":"paints.textBase" },
+        { "draw":"text.trivialWedge2", "paint":"paints.textBase" }
+    ],
+    "xaxis": [
+        { "draw":"paths.xaxis", "paint":"paints.axisStroke" },
+        { "draw":"text.xaxis1", "paint":"paints.axisTextDesc" },
+        { "draw":"text.xaxis2", "paint":"paints.axisTextRight" }
+    ],
+    "yaxis": [
+        { "draw":"paths.yaxis", "paint":"paints.axisStroke" },
+        { "draw":"text.yaxis1", "paint":"paints.axisTextTop" },
+        { "draw":"text.yaxis2", "paint":"paints.axisTextDesc" }
+    ],
+    "axes": [
+        { "draw":"pictures.xaxis", "paint":"paints.picture" },
+        { "draw":"pictures.yaxis", "paint":"paints.picture" }
+    ]
+};
+
+var gradientLookup = [
+    0, 4, 5, 3, 6, 7, 2, 8, 1
+];
+
+var keyframes = {
+    "_default": [
+        { "actions": [
+            { "range":[0,255], "paint":"paints.picture", "target":"paint.color",
+                    "params":"target", "formula":"alpha(value, params)" }
+        ]}
+    ],
+    "keyframe1": [
+        { "time":   0, "duration":1000, "canvas":"clear", "actions": [
+            { "draw":"text.spanWedge", "paint":"paints.textBase" }
+        ]},
+        { "time":1000, "duration":1000, "actions": [
+            { "ref":"span1", "draw":"paths.span1", "paint":"paints.stroke" }
+        ]},
+        { "time":1500, "duration":1500, "actions": [
+            { "ref":"wedge1", "draw":"paths.wedge1", "paint":"paints.gradient1" }
+        ]},
+        { "time":3500, "duration": 500, "actions": [
+            { "ref":"span1", "range":[255,0] },
+            { "ref":"wedge1", "range":[255,0] }
+        ]},
+        { "time":4000, "duration":1000, "actions": [
+            { "ref":"span2", "draw":"paths.span2", "paint":"paints.stroke" }
+        ]},
+        { "time":4500, "duration":1500, "actions": [
+            { "ref":"wedge2", "draw":"paths.wedge2", "paint":"paints.gradient2" }
+        ]},
+        { "time":6500, "duration": 500, "actions": [
+            { "ref":"span2", "range":[255,0] },
+            { "ref":"wedge2", "range":[255,0] }
+        ]},
+        { "time":7000, "duration":1000, "actions": [
+            { "draw":"paths.span3", "paint":"paints.stroke" }
+        ]},
+        { "time":7500, "duration":1500, "actions": [
+            { "draw":"paths.wedge3", "paint":"paints.gradient3" }
+        ]}
+    ],
+    "keyframe2": [
+        { "time":   0, "duration":1000, "canvas":"clear", "actions": [
+            { "draw":"pictures.trivialWedgeText", "paint":"paints.picture" }
+        ]},
+        { "time":2000, "duration":1500, "actions": [
+            { "draw":"pictures.trivialWedgeSpans", "paint":"paints.picture" }
+        ]}
+    ],
+    "keyframe3": [
+        { "time":   0, "duration":1000, "canvas":"clear", "actions": [
+            { "draw":"pictures.sectorText" },
+            { "draw":"pictures.xaxis" }
+        ]},
+        { "time": 500, "duration":1000, "actions": [
+            { "draw":"pictures.yaxis" }
+        ]},
+        { "time":2000, "duration":1500, "actions": [
+            { "draw":"pictures.sectorXYA" }
+        ]},
+        { "time":3000, "duration":1500, "actions": [
+            { "draw":"pictures.sectorXYB" }
+        ]},
+        { "time":4000, "duration":1500, "actions": [
+            { "draw":"pictures.sectorXYC" }
+        ]}
+    ],
+    "keyframe4": [
+        { "time":   0, "duration":1000, "canvas":"clear", "actions": [
+            { "draw":"text.lineSingle", "paint":"paints.textBase" },
+            { "draw":"pictures.axes" }
+        ]},
+        { "time":1000, "duration":1000, "actions": [
+            { "ref":"line", "draw":"paths.lineSegment", "paint":"paints.stroke" }
+        ]},
+        { "time":1850, "duration":1000, "actions": [
+            { "ref":"sectorXY1", "draw":"text.sectorXY1", "paint":"paints.sectorXY1" },
+            { "ref":"sectorXY1", "target":"draw.y", "formula":260 },
+            { "ref":"wedgeXY1", "draw":"paths.wedgeXY1", "paint":"paints.gradient4" }
+        ]},
+        { "time":3000, "duration":4000, "actions": [
+            { "ref":"line", "target":"draw[0].line[2]",
+                "range":[-22.5 * Math.PI / 180, (-22.5 - 360) * Math.PI / 180], "params":"circle",
+                "formula":"params.center.x + params.radius * Math.cos(value)"
+            },
+            { "ref":"line", "target":"draw[0].line[3]",
+                "range":[-22.5 * Math.PI / 180, (-22.5 - 360) * Math.PI / 180], "params":"circle",
+                "formula":"params.center.y + params.radius * Math.sin(value)"
+            }
+        ]},
+        { "for":["i=2", "i<=8", "++i"], "time":"2250 + 500 * i", "duration":100, "actions": [
+            { "ref":"'sectorXY' + i", "draw":"'text.sectorXY' + i",
+                    "paint":"'paints.sectorXY' + i" },
+            { "ref":"'sectorXY' + i", "target":"draw.y", "formula":260 },
+            { "ref":"'wedgeXY' + i", "draw":"'paths.wedgeXY' + i",
+                    "paint":"'paints.gradient' + gradientLookup[i]" },
+            { "ref":"'sectorXY' + (i - 1)", "range":[255,0] },
+            { "ref":"'wedgeXY' + (i - 1)", "range":[255,0] }
+        ]},
+        { "time":2250 + 500 * 9, "duration":100, "actions": [
+            { "ref":"sectorXY1" },
+            { "ref":"wedgeXY1" },
+            { "ref":"sectorXY8", "range":[255,0] },
+            { "ref":"wedgeXY8", "range":[255,0] }
+        ]}
+    ],
+    "keyframe5": [
+        { "time":   0, "duration":1000, "canvas":"clear", "actions": [
+            { "draw":"pictures.curveMultipleText" },
+            { "draw":"pictures.axes" }
+        ]},
+        { "time":1000, "duration":1000, "actions": [
+            { "ref":"curve", "draw":"paths.curveSegment1", "paint":"paints.stroke" }
+        ]},
+        { "time":2000, "duration":1000, "actions": [
+            { "draw":"text.sectorXY1", "paint":"paints.sectorXY1",
+                    "target":"draw.y", "formula":260 + 1 * 25},
+            { "draw":"paths.wedgeXY1", "paint":"paints.gradient4" }
+        ]},
+        { "time":3000, "duration":1000, "actions": [
+            { "ref":"curve", "range":[0,1], "target":"draw",
+                "params":["paths.curveSegment1","paths.curveSegment2"],
+                "formula":"interp_paths(value, params)"
+            }
+        ]},
+        { "time":4000, "duration":1000, "actions": [
+            { "draw":"text.sectorXY2", "paint":"paints.sectorXY2",
+                    "target":"draw.y", "formula":260 + 2 * 25},
+            { "draw":"paths.wedgeXY2", "paint":"paints.gradient5" }
+        ]},
+        { "time":5000, "duration":1000, "actions": [
+            { "ref":"curve", "range":[0,1], "target":"draw",
+                "params":["paths.curveSegment2","paths.curveSegment3"],
+                "formula":"interp_paths(value, params)"
+            }
+        ]},
+        { "for":["i=3", "i<=6", "++i"], "time":"6000", "actions": [
+            { "ref":"'text' + i", "draw":"'text.sectorXY' + i", "paint":"'paints.sectorXY' + i",
+                    "target":"draw.y", "formula":"260 + i * 25" },
+        ]},
+        { "for":["i=3", "i<=6", "++i"], "time":"6000", "duration":1000, "actions": [
+            { "ref":"'text' + i" },
+        ]},
+        { "time":6000, "duration":1000, "actions": [
+            { "draw":"paths.wedgeXY3", "paint":"paints.gradient3" },
+            { "draw":"paths.wedgeXY4", "paint":"paints.gradient6" },
+            { "draw":"paths.wedgeXY5", "paint":"paints.gradient7" },
+            { "draw":"paths.wedgeXY6", "paint":"paints.gradient2" },
+        ]}
+    ],
+    "keyframe6": [
+        { "time":   0, "duration":1000, "canvas":"clear", "actions": [
+            { "draw":"pictures.line1DDestText" },
+            { "draw":"pictures.axes" }
+        ]},
+        { "time":2000, "duration":1000, "actions": [
+            { "ref":"xy9", "draw":"text.sectorXY9", "paint":"paints.sectorXY9" },
+            { "ref":"xy9", "target":"draw.y", "formula":260 + 25},
+            { "draw":"paths.horzSegment", "paint":"paints.horzSegment" }
+        ]},
+        { "time":3000, "duration":1000, "actions": [
+            { "ref":"xy10", "draw":"text.sectorXY10", "paint":"paints.sectorXY10" },
+            { "ref":"xy10", "target":"draw.y", "formula":260 + 50 },
+            { "draw":"paths.vertSegment", "paint":"paints.vertSegment" }
+        ]},
+        { "time":4000, "duration":1000, "actions": [
+            { "ref":"xy11", "draw":"text.sectorXY11", "paint":"paints.sectorXY11" },
+            { "ref":"xy11", "target":"draw.y", "formula":260 + 75 },
+            { "draw":"paths.diagSegment", "paint":"paints.diagSegment" }
+        ]}
+    ],
+    "keyframe7": [
+        { "time":   0, "duration":1000, "canvas":"clear", "actions": [
+            { "draw":"pictures.curve1DDestText" },
+            { "draw":"pictures.axes" }
+        ]},
+        { "time":2000, "duration":1000, "actions": [
+            { "ref":"cubic", "draw":"paths.cubicSegment1", "paint":"paints.stroke" },
+            { "ref":"cubic", "range":[0,1], "target":"draw",
+                "params":"paths.cubicSegment2", "formula":"path_partial(value, params)" },
+            { "ref":"xy9", "draw":"text.sectorXY9", "paint":"paints.sectorXY9" },
+            { "ref":"xy9", "target":"draw.y", "formula":260 + 25},
+            { "draw":"paths.horzSegment", "paint":"paints.horzSegment" }
+        ]},
+        { "time":3000, "duration":1000, "actions": [
+            { "ref":"xy1", "draw":"text.sectorXY1", "paint":"paints.sectorXY1" },
+            { "ref":"xy1", "target":"draw.y", "formula":260 + 60},
+            { "draw":"paths.wedgeXY1", "paint":"paints.gradient4" }
+        ]},
+    ]
+};
diff --git a/experimental/docs/interpolatorFunctions.js b/experimental/docs/interpolatorFunctions.js
new file mode 100644
index 0000000..d51969f
--- /dev/null
+++ b/experimental/docs/interpolatorFunctions.js
@@ -0,0 +1,84 @@
+function interp(A, B, t) {
+    return A + (B - A) * t;
+}
+
+function interp_cubic_coords(x1, x2, x3, x4, t)
+{
+    var ab = interp(x1, x2, t);
+    var bc = interp(x2, x3, t);
+    var cd = interp(x3, x4, t);
+    var abc = interp(ab, bc, t);
+    var bcd = interp(bc, cd, t);
+    var abcd = interp(abc, bcd, t);
+    return abcd;
+}
+
+// FIXME : only works for path with single cubic
+function path_partial(value, path) {
+    assert(isArray(path));
+    var out = [];
+    for (var cIndex = 0; cIndex < path.length; ++cIndex) {
+        out[cIndex] = {};
+        var curveKey = Object.keys(path[cIndex])[0];
+        var curve = path[cIndex][curveKey];
+        var outArray;
+        switch (curveKey) {
+            case "cubic":
+                var x1 = curve[0], y1 = curve[1], x2 = curve[2], y2 = curve[3];
+                var x3 = curve[4], y3 = curve[5], x4 = curve[6], y4 = curve[7];
+                var t1 = 0, t2 = value;
+                var ax = interp_cubic_coords(x1, x2, x3, x4, t1);
+                var ay = interp_cubic_coords(y1, y2, y3, y4, t1);
+                var ex = interp_cubic_coords(x1, x2, x3, x4, (t1*2+t2)/3);
+                var ey = interp_cubic_coords(y1, y2, y3, y4, (t1*2+t2)/3);
+                var fx = interp_cubic_coords(x1, x2, x3, x4, (t1+t2*2)/3);
+                var fy = interp_cubic_coords(y1, y2, y3, y4, (t1+t2*2)/3);
+                var dx = interp_cubic_coords(x1, x2, x3, x4, t2);
+                var dy = interp_cubic_coords(y1, y2, y3, y4, t2);
+                var mx = ex * 27 - ax * 8 - dx;
+                var my = ey * 27 - ay * 8 - dy;
+                var nx = fx * 27 - ax - dx * 8;
+                var ny = fy * 27 - ay - dy * 8;
+                var bx = (mx * 2 - nx) / 18;
+                var by = (my * 2 - ny) / 18;
+                var cx = (nx * 2 - mx) / 18;
+                var cy = (ny * 2 - my) / 18;
+                outArray = [
+                    ax, ay, bx, by, cx, cy, dx, dy
+                ];
+                break;
+            default:
+                assert(0);  // unimplemented
+        }
+        out[cIndex][curveKey] = outArray;
+    }
+    return out;
+}
+
+function interp_paths(value, paths) {
+    assert(isArray(paths));
+    assert(paths.length == 2);
+    var curves0 = paths[0];
+    assert(isArray(curves0));
+    var curves1 = paths[1];
+    assert(isArray(curves1));
+    assert(curves0.length == curves1.length);
+    var out = [];
+    for (var cIndex = 0; cIndex < curves0.length; ++cIndex) {
+        out[cIndex] = {};
+        var curve0Key = Object.keys(curves0[cIndex])[0];
+        var curve1Key = Object.keys(curves1[cIndex])[0];
+        assert(curve0Key == curve1Key);
+        var curve0 = curves0[cIndex][curve0Key];
+        var curve1 = curves1[cIndex][curve1Key];
+        assert(isArray(curve0));
+        assert(isArray(curve1));
+        assert(curve0.length == curve1.length);
+        var outArray = [];
+        for (var i = 0; i < curve1.length; ++i) {
+            outArray[i] = curve0[i] + (curve1[i] - curve0[i]) * value;
+        }
+        out[cIndex][curve0Key] = outArray;
+    }
+    return out;
+}
diff --git a/experimental/docs/jsonbaseddoc.htm b/experimental/docs/jsonbaseddoc.htm
new file mode 100644
index 0000000..bceabae
--- /dev/null
+++ b/experimental/docs/jsonbaseddoc.htm
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+
+<script src="utilities.js"></script>
+
+<script src="animationCommon.js"></script>
+<script src="backend.js"></script>
+<script src="canvasBackend.js"></script>
+<script src="exampleSlides.js"></script>
+<script src="interpolatorFunctions.js"></script>
+<script src="svgBackend.js"></script>
+
+<script>
+
+var frame = 1;
+
+function keypress() {
+    init('all', 'keyframe' + frame);
+    if (++frame > 7) {
+        frame = 1;
+    }
+}
+
+function onload() {
+    init('all', 'keyframe6');
+}
+
+</script>
+
+</head>
+
+<body onLoad="onload()" onKeypress="keypress()">
+
+<canvas id="canvas" width="770" height="500" ></canvas>
+
+<svg id="svg" width="770" height="500"
+    xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" />
+
+</body>
+</html>
diff --git a/experimental/docs/svgBackend.js b/experimental/docs/svgBackend.js
new file mode 100644
index 0000000..b5907f0
--- /dev/null
+++ b/experimental/docs/svgBackend.js
@@ -0,0 +1,246 @@
+var svgCache;
+var svgDefs;
+var svgGradients;
+var svgNS = "http://www.w3.org/2000/svg";
+var svgRoot;
+
+function displaySvg(displayList) {
+    for (var index = 0; index < displayList.length; ++index) {
+        drawToSvg(displayList[index]);
+    }
+}
+
+function drawToSvg(display) {
+    assert('string' == typeof(display.ref));
+    var cache;
+    if (display.ref in svgCache) {
+        cache = svgCache[display.ref];
+        if (display.drawDirty) {
+            switch (cache.spec) {
+                case "paths":
+                    svgSetPathData(cache.element, display.draw);
+                    break;
+                case "pictures":
+                    svgSetPictureData(cache.element, display.draw);
+                    break;
+                case "text":
+                    svgCreateText(cache.element, display.draw);
+                    break;
+                default:
+                    assert(0);
+            }
+        }
+    } else {
+        cache = {};
+        cache.action = display;
+        cache.spec = display.drawSpec;
+        var dot = cache.spec.indexOf(".");
+        if (dot > 0) {
+            cache.spec = cache.spec.substring(0, dot);
+        }
+        switch (cache.spec) {
+            case "paths":
+                cache.element = svgCreatePath(display.ref, display.draw);
+                break;
+            case "pictures":
+                cache.element = svgCreatePicture(display.ref, display.draw);
+                break;
+            case "text":
+                cache.element = svgCreateText(display.ref, display.draw);
+                break;
+            default:
+                assert(0);
+        }
+    }
+    display.drawDirty = false;
+    if (display.paintDirty) {
+        svgSetPaintData(cache.element, display.paint);
+        var opacity = svg_opacity(display.paint.color);
+        cache.element.setAttribute("fill-opacity", opacity);
+        cache.element.setAttribute("stroke-opacity", opacity);
+        display.paintDirty = false;
+    }
+    assert('object' == typeof(cache));
+    if (!(display.ref in svgCache)) {
+        svgRoot.appendChild(cache.element);
+        svgCache[display.ref] = cache;
+    }
+}
+
+function setupSvg() {
+    svgCache = { "paths":{}, "pictures":{}, "text":{} };
+    svgDefs = document.createElementNS(svgNS, "defs");
+    svgGradients = {};
+    svgRoot = document.getElementById("svg");
+    while (svgRoot.lastChild) {
+        svgRoot.removeChild(svgRoot.lastChild);
+    }
+    svgRoot.appendChild(svgDefs);
+}
+
+function svg_rbg(color) {
+    return "rgb(" + ((color >> 16) & 0xFF)
+            + "," + ((color >>  8) & 0xFF)
+            + "," + ((color >>  0) & 0xFF) + ")";
+}
+
+function svg_opacity(color) {
+    return ((color >> 24) & 0xFF) / 255.0;
+}
+
+function svgCreatePath(key, path) {
+    var svgPath = document.createElementNS(svgNS, "path");
+    svgPath.setAttribute("id", key);
+    svgSetPathData(svgPath, path);
+    return svgPath;
+}
+
+function svgCreatePicture(key, picture) {
+    var svgPicture = document.createElementNS(svgNS, "g");
+    svgPicture.setAttribute("id", key);
+    svgSetPictureData(svgPicture, picture);
+    return svgPicture;
+}
+
+function svgCreateRadialGradient(key) {
+    var g = gradients[key];
+    var e = document.createElementNS(svgNS, "radialGradient");
+    e.setAttribute("id", key);
+    e.setAttribute("cx", g.cx);
+    e.setAttribute("cy", g.cy);
+    e.setAttribute("r", g.r);
+    e.setAttribute("gradientUnits", "userSpaceOnUse");
+    var stopLen = g.stops.length;
+    for (var index = 0; index < stopLen; ++index) {
+        var stop = g.stops[index];
+        var color = svg_rbg(stop.color);
+        var s = document.createElementNS(svgNS, 'stop');
+        s.setAttribute("offset", stop.offset);
+        var style = "stop-color:" + svg_rbg(stop.color) + "; stop-opacity:"
+                + svg_opacity(stop.color);
+        s.setAttribute("style", style);
+        e.appendChild(s);
+    }
+    svgGradients[key] = e;
+    svgDefs.appendChild(e);
+}
+
+function svgCreateText(key, text) {
+    var svgText = document.createElementNS(svgNS, "text");
+    svgText.setAttribute("id", key);
+    var textNode = document.createTextNode(text.string);
+    svgText.appendChild(textNode);
+    svgSetTextData(svgText, text);
+    return svgText;
+}
+
+function svgSetPathData(svgPath, path) {
+    var dString = "";
+    for (var cIndex = 0; cIndex < path.length; ++cIndex) {
+        var curveKey = Object.keys(path[cIndex])[0];
+        var v = path[cIndex][curveKey];
+        switch (curveKey) {
+            case 'arcTo':
+                var clockwise = 1; // to do; work in general case
+                dString += " A" + v[4] + "," + v[4] + " 0 0," + clockwise + " "
+                        + v[2] + "," + v[3];
+                break;
+            case 'close':
+                dString += " z";
+                break;
+            case 'cubic':
+                dString += " M" + v[0] + "," + v[1];
+                dString += " C" + v[2] + "," + v[3]
+                          + " " + v[4] + "," + v[5]
+                          + " " + v[6] + "," + v[7];
+                break;
+            case 'line':
+                dString += " M" + v[0] + "," + v[1];
+                dString += " L" + v[2] + "," + v[3];
+                break;
+            case 'quad':
+                dString += " M" + v[0] + "," + v[1];
+                dString += " Q" + v[2] + "," + v[3]
+                          + " " + v[4] + "," + v[5];
+                break;
+            default:
+                assert(0);
+        }
+    }
+    svgPath.setAttribute("d", dString);
+}
+
+function svgSetPaintData(svgElement, paint) {
+    var color;
+    var inPicture = 'string' == typeof(paint);
+    if (inPicture) {
+        paint = (new Function("return " + paint))();
+        assert('object' == typeof(paint) && !isArray(paint));
+    }
+    if ('gradient' in paint) {
+        var gradient = paint.gradient.split('.');
+        var gradName = gradient[1];
+        if (!svgGradients[gradName]) {
+            svgCreateRadialGradient(gradName);
+        }
+        color = "url(#" + gradName + ")";
+    } else {
+        color = svg_rbg(paint.color);
+    }
+    svgElement.setAttribute("fill", 'fill' == paint.style ? color : "none");
+    if ('stroke' == paint.style) {
+        svgElement.setAttribute("stroke", color);
+    }
+    if ('strokeWidth' in paint) {
+        svgElement.setAttribute("stroke-width", paint.strokeWidth);
+    }
+    if ('typeface' in paint) {
+        var typeface = typefaces[paint.typeface];
+        var font = typeface.style;
+        if ('textSize' in paint) {
+            svgElement.setAttribute("font-size", paint.textSize);
+        }
+        if ('family' in typeface) {
+            svgElement.setAttribute("font-family", typeface.family);
+        }
+        if ('textAlign' in paint) {
+            svgElement.setAttribute("text-anchor", paint.textAlign == "right" ? "end" : assert(0));
+        }
+        if ('textBaseline' in paint) {
+            svgElement.setAttribute("alignment-baseline", paint.textBaseline);
+        }
+    }
+}
+
+function svgSetPictureData(svgPicture, picture) {
+    while (svgPicture.lastChild) {
+        svgPicture.removeChild(svgPicture.lastChild);
+    }
+    for (var index = 0; index < picture.length; ++index) {
+        var entry = picture[index];
+        var drawObj = (new Function("return " + entry.draw))();
+        var drawSpec = entry.draw.split('.');
+        var svgElement;
+        switch (drawSpec[0]) {
+            case 'paths':
+                svgElement = svgCreatePath(drawSpec[1], drawObj);
+                break;
+            case 'pictures':
+                svgElement = svgCreatePicture(drawSpec[1], drawObj);
+                break;
+            case 'text':
+                svgElement = svgCreateText(drawSpec[1], drawObj);
+                break;
+            default:
+                assert(0);
+        }
+        var paintObj = (new Function("return " + entry.paint))();
+        svgSetPaintData(svgElement, paintObj);
+        svgPicture.appendChild(svgElement);
+    }
+}
+
+function svgSetTextData(svgElement, text) {
+    svgElement.setAttribute('x', text.x);
+    svgElement.setAttribute('y', text.y);
+}
diff --git a/experimental/docs/svgbaseddoc.htm b/experimental/docs/svgbaseddoc.htm
new file mode 100644
index 0000000..c96edcc
--- /dev/null
+++ b/experimental/docs/svgbaseddoc.htm
@@ -0,0 +1,1712 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+
+
+<style>
+html {
+  font-family: Helvetica, Arial, sans-serif;
+  font-size: 100%;
+}
+
+.controls {
+  margin: 1em 0;
+}
+
+button {
+  display: inline-block;
+  border-radius: 3px;
+  border: none;
+  font-size: 0.9rem;
+  padding: 0.4rem 0.8em;
+  background: #69c773;
+  border-bottom: 1px solid #498b50;
+  color: white;
+  -webkit-font-smoothing: antialiased;
+  font-weight: bold;
+  margin: 0 0.25rem;
+  text-align: center;
+}
+
+button:hover, button:focus {
+  opacity: 0.75;
+  cursor: pointer;
+}
+
+button:active {
+  opacity: 1;
+  box-shadow: 0 -3px 10px rgba(0, 0, 0, 0.1) inset;
+}
+
+</style>
+
+<! set height back to 500 />
+<svg id="svg" width="800" height="500"
+    xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+
+<defs>
+    <radialGradient id="grad1" cx="200" cy="200" r="300" gradientUnits="userSpaceOnUse">
+      <stop offset="0%"   style="stop-color:rgb(0,0,255); stop-opacity:0.3" />
+      <stop offset="100%" style="stop-color:rgb(0,0,255); stop-opacity:0" />
+    </radialGradient>
+    <radialGradient id="grad2" cx="200" cy="200" r="300" gradientUnits="userSpaceOnUse">
+      <stop offset="0%"   style="stop-color:rgb(0,255,0); stop-opacity:0.3" />
+      <stop offset="100%" style="stop-color:rgb(0,255,0); stop-opacity:0" />
+    </radialGradient>
+    <radialGradient id="grad3" cx="200" cy="200" r="300" gradientUnits="userSpaceOnUse">
+      <stop offset="0%"   style="stop-color:rgb(255,0,0); stop-opacity:0.3" />
+      <stop offset="100%" style="stop-color:rgb(255,0,0); stop-opacity:0" />
+    </radialGradient>
+    <radialGradient id="grad4" cx="200" cy="200" r="300" gradientUnits="userSpaceOnUse">
+      <stop offset="0%"   style="stop-color:rgb(192,63,192); stop-opacity:0.3" />
+      <stop offset="100%" style="stop-color:rgb(192,63,192); stop-opacity:0" />
+    </radialGradient>
+    <radialGradient id="grad5" cx="200" cy="200" r="300" gradientUnits="userSpaceOnUse">
+      <stop offset="0%"   style="stop-color:rgb(127,127,0); stop-opacity:0.3" />
+      <stop offset="100%" style="stop-color:rgb(127,127,0); stop-opacity:0" />
+    </radialGradient>
+    <radialGradient id="grad6" cx="200" cy="200" r="300" gradientUnits="userSpaceOnUse">
+      <stop offset="0%"   style="stop-color:rgb(127,0,127); stop-opacity:0.3" />
+      <stop offset="100%" style="stop-color:rgb(127,0,127); stop-opacity:0" />
+    </radialGradient>
+    <radialGradient id="grad7" cx="200" cy="200" r="300" gradientUnits="userSpaceOnUse">
+      <stop offset="0%"   style="stop-color:rgb(0,127,127); stop-opacity:0.3" />
+      <stop offset="100%" style="stop-color:rgb(0,127,127); stop-opacity:0" />
+    </radialGradient>
+    <radialGradient id="grad8" cx="200" cy="200" r="300" gradientUnits="userSpaceOnUse">
+      <stop offset="0%"   style="stop-color:rgb(63,192,63); stop-opacity:0.3" />
+      <stop offset="100%" style="stop-color:rgb(63,192,63); stop-opacity:0" />
+    </radialGradient>
+</defs>
+
+<path id="circleFill" d="M300,200 A 100,100 0,0,0 300,200" fill="#777" fill-opacity="0" />
+<path id="circle" d="M300,200 A 100,100 0,0,0 300,200" fill="none" stroke="black" />
+
+<! elements for keyframe 1 />
+<text id="spanWedgeDesc" fill-opacity="0" >
+All spans are contained by a wedge.
+</text>
+<path id="span1" d="M200,200 Q300,300 200,300" fill="none" stroke="black" stroke-opacity="0"/>
+<path id="span2" d="M200,200 C100,300 100,400 200,300" fill="none" stroke="black" stroke-opacity="0"/>
+<path id="span3" d="M200,200 C300,100 100,400 300,200" fill="none" stroke="black" stroke-opacity="0"/>
+<path id="wedge1" d="M200,200 L500,500 A 424.26,424.26 0,0,1 200,624.26 z" fill="url(#grad1)" fill-opacity="0"/>
+<path id="wedge2" d="M200,200 L200,624.26 A 424.26,424.26 0,0,1 -100,500 z" fill="url(#grad2)" fill-opacity="0"/>
+<path id="wedge3" d="M200,200 L500,-100 A 424.26,424.26 0,0,1 240,622.5 z" fill="url(#grad3)" fill-opacity="0"/>
+
+<! keyframe 2 />
+<text id="trivialWedgeDesc1" fill-opacity="0" >
+Wedges that don't overlap can be
+</text>
+<text id="trivialWedgeDesc2" y="240" fill-opacity="0" >
+easily sorted.
+</text>
+<path id="span4" d="M200,200 Q300,300 400,300" fill="none" stroke="black" stroke-opacity="0"/>
+<path id="span5" d="M200,200 Q280,320 200,400" fill="none" stroke="black" stroke-opacity="0"/>
+<path id="span6" d="M200,200 Q60,340 100,400" fill="none" stroke="black" stroke-opacity="0"/>
+<path id="wedge4" d="M200,200 L500,500 A 424.26,424.26 0,0,1 579.47,389.74 z" fill="url(#grad1)" fill-opacity="0"/>
+<path id="wedge5" d="M200,200 L389.74,579.47 A 424.26,424.26 0,0,1 200,500 z" fill="url(#grad2)" fill-opacity="0"/>
+<path id="wedge6" d="M200,200 L10.26,579.47 A 424.26,424.26 0,0,1 -100,500 z" fill="url(#grad3)" fill-opacity="0"/>
+
+
+<! keyframe 3 />
+<text id="sectorDesc1" fill-opacity="0" >
+A sector is a wedge of a circle
+</text>
+<text id="sectorDesc2" y="240" fill-opacity="0" >
+containing a range of points.
+</text>
+<g id="xaxis" stroke-opacity="0" fill-opacity="0">
+    <path d="M100,200 L300,200" fill="none" stroke="rgb(191,191,191)"/>
+    <text x="100" y="220" fill="rgb(191,191,191)">-X</text>
+    <text x="300" y="220" text-anchor="end" fill="rgb(191,191,191)">+X</text>
+</g>
+<g id="yaxis" stroke-opacity="0" fill-opacity="0">
+    <path d="M200,100 L200,300" fill="none" stroke="rgb(191,191,191)"/>
+    <text x="205" y="100" alignment-baseline="hanging" fill="rgb(191,191,191)">-Y</text>
+    <text x="205" y="300" fill="rgb(191,191,191)">+Y</text>
+</g>
+<text id="sectorDescXYA" x="500" y="310" fill="rgb(0,0,255)" fill-opacity="0">
+X &gt; 0>&nbsp;&nbsp;Y &gt; 0&nbsp;&nbsp;&nbsp;&nbsp;Y &lt; X</text>
+<text id="sectorDescXYB" x="500" y="360" fill="rgb(0,127,0)" fill-opacity="0">
+X &lt; 0&nbsp;&nbsp;&nbsp;Y &gt; 0&nbsp;&nbsp;&nbsp;-Y &lt; X</text>
+<text id="sectorDescXYC" x="500" y="410" fill="rgb(255,0,0)" fill-opacity="0">
+X &lt; 0&nbsp;&nbsp;&nbsp;Y &lt; 0&nbsp;&nbsp;&nbsp;&nbsp;Y &lt; X</text>
+<path id="wedgeXY8" d="M200,200 L500,500 A 424.26,424.26 0,0,1 624.26,200 z" fill="url(#grad1)" fill-opacity="0"/>
+<path id="wedgeXY6" d="M200,200 L-100,500 A 424.26,424.26 0,0,1 200,624.26 z" fill="url(#grad2)" fill-opacity="0"/>
+<path id="wedgeXY3" d="M200,200 L-100,-100 A 424.26,424.26 0,0,1 200,-175.74 z" fill="url(#grad3)" fill-opacity="0"/>
+
+<! keyframe 4 />
+<text id="lineSingleDesc" fill-opacity="0" >
+Line spans are contained by a single sector.
+</text>
+<text id="sectorDescXY1" x="500" y="460" fill="rgb(192,63,192)" fill-opacity="0">
+X &gt; 0&nbsp;&nbsp;&nbsp;Y &lt; 0&nbsp;&nbsp;&nbsp;-Y &lt; X</text>
+<text id="sectorDescXY2" x="500" y="460" fill="rgb(127,127,0)" fill-opacity="0">
+X &gt; 0&nbsp;&nbsp;&nbsp;Y &lt; 0&nbsp;&nbsp;&nbsp;-Y &gt; X</text>
+<text id="sectorDescXY3" x="500" y="460" fill="rgb(255,0,0)" fill-opacity="0">
+X &lt; 0&nbsp;&nbsp;&nbsp;Y &lt; 0&nbsp;&nbsp;&nbsp;&nbsp;Y &lt; X</text>
+<text id="sectorDescXY4" x="500" y="460" fill="rgb(127,0,127)" fill-opacity="0">
+X &lt; 0&nbsp;&nbsp;&nbsp;Y &lt; 0&nbsp;&nbsp;&nbsp;&nbsp;Y &gt; X</text>
+<text id="sectorDescXY5" x="500" y="460" fill="rgb(0,127,127)" fill-opacity="0">
+X &lt; 0&nbsp;&nbsp;&nbsp;Y &gt; 0&nbsp;&nbsp;&nbsp;-Y &lt; X</text>
+<text id="sectorDescXY6" x="500" y="460" fill="rgb(0,127,0)" fill-opacity="0">
+X &lt; 0&nbsp;&nbsp;&nbsp;Y &gt; 0&nbsp;&nbsp;&nbsp;-Y &lt; X</text>
+<text id="sectorDescXY7" x="500" y="460" fill="rgb(63,192,63)" fill-opacity="0">
+X &gt; 0&nbsp;&nbsp;&nbsp;Y &gt; 0&nbsp;&nbsp;&nbsp;&nbsp;Y &gt; X</text>
+<text id="sectorDescXY8" x="500" y="460" fill="rgb(0,0,255)" fill-opacity="0">
+X &gt; 0&nbsp;&nbsp;&nbsp;Y &gt; 0&nbsp;&nbsp;&nbsp;&nbsp;Y &lt; X</text>
+<path id="wedgeXY1" d="M200,200 L500,-100 A 424.26,424.26 0,0,1 624.26,200 z" fill="url(#grad4)" fill-opacity="0"/>
+<path id="wedgeXY2" d="M200,200 L200,-175.74 A 424.26,424.26 0,0,1 500,-100 z" fill="url(#grad5)" fill-opacity="0"/>
+<path id="wedgeXY4" d="M200,200 L-175.74,200 A 424.26,424.26 0,0,1 -100,-100 z" fill="url(#grad6)" fill-opacity="0"/>
+<path id="wedgeXY5" d="M200,200 L-100,500 A 424.26,424.26 0,0,1 -175.74,200 z" fill="url(#grad7)" fill-opacity="0"/>
+<path id="wedgeXY7" d="M200,200 L200,624.26 A 424.26,424.26 0,0,1 500,500 z" fill="url(#grad8)" fill-opacity="0"/>
+<path id="lineSegment" d="M200,200 L200,624.26" fill="none" stroke="black" stroke-opacity="0"/>
+
+<! keyframe 5 />
+<text id="curveMultipleDesc1" fill-opacity="0" >
+A curve span may cover more
+</text>
+<text id="curveMultipleDesc2" y="240" fill-opacity="0" >
+than one sector.
+</text>
+<path id="curveSegment" d="M200,200 C250,200 300,150 300,100" fill="none" stroke="black" stroke-opacity="0"/>
+<path id="curveSegment1" d="M200,200 C250,200 300,150 300,100" fill="none"/>
+<path id="curveSegment2" d="M200,200 C250,200 300,150 200,100" fill="none"/>
+<path id="curveSegment3" d="M200,200 C350,200 250,-150 170,300" fill="none"/>
+
+<! keyframe 6 />
+<text id="line1DDest1" fill-opacity="0" >
+Some lines occupy one-dimensional
+</text>
+<text id="line1DDest2" y="240" fill-opacity="0" >
+sectors.
+</text>
+<text id="sectorDescXY9" x="500" y="460" fill="rgb(192,92,31)" fill-opacity="0">
+X &gt; 0&nbsp;&nbsp;&nbsp;Y == 0</text>
+<text id="sectorDescXY10" x="500" y="460" fill="rgb(31,92,192)" fill-opacity="0">
+Y &gt; 0&nbsp;&nbsp;&nbsp;0 == X</text>
+<text id="sectorDescXY11" x="500" y="460" fill="rgb(127,63,127)" fill-opacity="0">
+X &lt; 0&nbsp;&nbsp;&nbsp;Y == X</text>
+<path id="horzSegment" d="M200,200 L341.4,200" fill="none" stroke="rgb(192,92,31)" stroke-width="2" stroke-opacity="0"/>
+<path id="vertSegment" d="M200,200 L200,341.4" fill="none" stroke="rgb(31,92,192)" stroke-width="2" stroke-opacity="0"/>
+<path id="diagSegment" d="M200,200 L100,100"   fill="none" stroke="rgb(127,63,127)" stroke-width="2" stroke-opacity="0"/>
+
+<! keyframe 7 />
+<text id="curve1dDesc1" fill-opacity="0" >
+Some curves initially occupy
+</text>
+<text id="curve1dDesc2" y="240" fill-opacity="0" >
+one-dimensional sectors, then diverge.
+</text>
+<path id="cubicSegment" fill="none" stroke="black" />
+<path id="cubicSegment1" d="M200,200 C200,200 200,200 200,200" fill="none" />
+<path id="cubicSegment2" d="M200,200 C250,200 300,200 300,100" fill="none"/>
+
+<text id="sectorNumberDesc" fill-opacity="0" >
+Each sector is assigned a number.
+</text>
+<text id="spanSectorDesc" fill-opacity="0" >
+Each span has a bit set for one or more sectors.
+</text>
+<text id="bitOverDesc" fill-opacity="0" >
+Span sets allow rough sorting without angle computation.
+</text>
+
+</svg>
+
+<! canvas support />
+<script>
+
+var keyFrameQueue = [];
+var animationsPending = [];
+var animationsActive = [];
+var displayList = [];
+var visibleFinished = [];
+
+var animationState = {};
+animationState.reset = function () {
+    this.start = null;
+    this.time = 0;
+    this.requestID = null;
+    this.paused = false;
+    this.displayEngine = 'Canvas';
+}
+
+circle.center = { x: 200, y: 200 }
+circle.radius = 100;
+
+function assert(condition) {
+    if (!condition) debugger;
+}
+
+function CanvasGrads(ctx) {
+    var grad1 = ctx.createRadialGradient(200, 200, 0, 200, 200, 300);
+    grad1.addColorStop(0, "rgba(0,0,255, 0.3)");
+    grad1.addColorStop(1, "rgba(0,0,255, 0)");
+    var grad2 = ctx.createRadialGradient(200, 200, 0, 200, 200, 300);
+    grad2.addColorStop(0, "rgba(0,255,0, 0.3)");
+    grad2.addColorStop(1, "rgba(0,255,0, 0)");
+    var grad3 = ctx.createRadialGradient(200, 200, 0, 200, 200, 300);
+    grad3.addColorStop(0, "rgba(255,0,0, 0.3)");
+    grad3.addColorStop(1, "rgba(255,0,0, 0)");
+    var grad4 = ctx.createRadialGradient(200, 200, 0, 200, 200, 300);
+    grad4.addColorStop(0, "rgba(192,63,192, 0.3)");
+    grad4.addColorStop(1, "rgba(192,63,192, 0)");
+    var grad5 = ctx.createRadialGradient(200, 200, 0, 200, 200, 300);
+    grad5.addColorStop(0, "rgba(127,127,0, 0.3)");
+    grad5.addColorStop(1, "rgba(127,127,0, 0)");
+    var grad6 = ctx.createRadialGradient(200, 200, 0, 200, 200, 300);
+    grad6.addColorStop(0, "rgba(127,0,127, 0.3)");
+    grad6.addColorStop(1, "rgba(127,0,127, 0)");
+    var grad7 = ctx.createRadialGradient(200, 200, 0, 200, 200, 300);
+    grad7.addColorStop(0, "rgba(0,127,127, 0.3)");
+    grad7.addColorStop(1, "rgba(0,127,127, 0)");
+    var grad8 = ctx.createRadialGradient(200, 200, 0, 200, 200, 300);
+    grad8.addColorStop(0, "rgba(63,192,63, 0.3)");
+    grad8.addColorStop(1, "rgba(63,192,63, 0)");
+    var data = {
+        grad1: grad1,
+        grad2: grad2,
+        grad3: grad3,
+        grad4: grad4,
+        grad5: grad5,
+        grad6: grad6,
+        grad7: grad7,
+        grad8: grad8,
+    };
+    return data;
+}
+
+function skip_sep(data) {
+    if (!data.length) {
+        return data;
+    }
+    while (data[0] == ' ' || data[0] == ',') {
+        data = data.substring(1);
+    }
+    return data;
+}
+
+function find_points(str, value, count, isRelative, relative) {
+    var numRegEx = /-?\d+\.?\d*(?:e-?\d+)?/g;
+    var match;
+    for (var index = 0; index < count; ++index) {
+        str = skip_sep(str);
+        match = numRegEx.exec(str);
+        assert(match);
+        var x = Number(match[0]);
+        str = skip_sep(str);
+        match = numRegEx.exec(str);
+        assert(match);
+        var y = Number(match[0]);
+        value[index] = { x: x, y : y };
+    }
+    if (isRelative) {
+        for (var index = 0; index < count; index++) {
+            value[index].x += relative.x;
+            value[index].y += relative.y;
+        }
+    }
+    return str.substring(match.index + match[0].length);
+}
+
+function find_scalar(str, obj, isRelative, relative) {
+    var numRegEx = /-?\d+\.?\d*(?:e-?\d+)?/g;
+    str = skip_sep(str);
+    var match = numRegEx.exec(str);
+    obj.value = Number(match[0]);
+    if (isRelative) {
+        obj.value += relative;
+    }
+    return str.substring(match.index + match[0].length);
+}
+
+function parse_path(data) {
+    var path = "ctx.beginPath();\n";
+    var f = {x:0, y:0};
+    var c = {x:0, y:0};
+    var lastc = {x:0, y:0};
+    var points = [];
+    var op = '\0';
+    var previousOp = '\0';
+    var relative = false;
+    for (;;) {
+        data = skip_sep(data);
+        if (!data.length) {
+            break;
+        }
+        var ch = data[0];
+        if (('0' <= ch && ch <= '9') || ch == '-' || ch == '+') {
+            assert(op != '\0');
+        } else if (ch == ' ' || ch == ',') {
+            data = skip_sep(data);
+        } else {
+            op = ch;
+            relative = false;
+            if ('a' <= op && op <= 'z') {
+                op = op.toUpperCase();
+                relative = true;
+            }
+            data = data.substring(1);
+            data = skip_sep(data);
+        }
+        switch (op) {
+            case 'A':
+                var radii = [];
+                data = find_points(data, radii, 1, false, null);
+                var xaxisObj = {};
+                data = find_scalar(data, xaxisObj, false, null);
+                var largeArcObj = {};
+                data = find_scalar(data, largeArcObj, false, null);
+                var sweepObj = {};
+                data = find_scalar(data, sweepObj, false, null);
+                data = find_points(data, points, 1, relative, c);
+                var mid = { x: (c.x + points[0].x) / 2, y: (c.y + points[0].y) / 2 };
+                var midVec = { x: mid.x - c.x, y: mid.y - c.y };
+                var midLenSqr = midVec.x * midVec.x + midVec.y * midVec.y;
+                var radius = radii[0].x;
+                var scale = Math.sqrt(midLenSqr) / Math.sqrt(radius * radius - midLenSqr);
+                var tangentPt = { x: mid.x + midVec.y * scale,
+                                  y: mid.y - midVec.x * scale };
+                path += "ctx.arcTo(" + tangentPt.x + "," + tangentPt.y + ","
+                    + points[0].x + "," + points[0].y + "," + radius + ");\n";
+                c = points[0];
+                break;
+            case 'M':
+                data = find_points(data, points, 1, relative, c);
+                path += "ctx.moveTo(" + points[0].x + "," + points[0].y + ");\n";
+                op = 'L';
+                c = points[0];
+                break;
+            case 'L':
+                data = find_points(data, points, 1, relative, c);
+                path += "ctx.lineTo(" + points[0].x + "," + points[0].y + ");\n";
+                c = points[0];
+                break;
+            case 'H': {
+                var xObj = {};
+                data = find_scalar(data, xObj, relative, c.x);
+                path += "ctx.lineTo(" + xObj.value + "," + c.y + ");\n";
+                c.x = xObj.value;
+            } break;
+            case 'V': {
+                var yObj = {};
+                data = find_scalar(data, y, relative, c.y);
+                path += "ctx.lineTo(" + c.x + "," + yObj.value+ ");\n";
+                c.y = yObj.value;
+            } break;
+            case 'C':
+                data = find_points(data, points, 3, relative, c);
+                path += "ctx.bezierCurveTo(" + points[0].x + "," + points[0].y + ","
+                    + points[1].x + "," + points[1].y + ","
+                    + points[2].x + "," + points[2].y + ");\n";
+                lastc = points[1];
+                c = points[2];
+                break;
+            case 'S':
+                var pts2_3 = [];
+                data = find_points(data, pts2_3, 2, relative, c);
+                points[0] = c;
+                points[1] = pts2_3[0];
+                points[2] = pts2_3[1];
+                if (previousOp == 'C' || previousOp == 'S') {
+                    points[0].x -= lastc.x - c.x;
+                    points[0].y -= lastc.y - c.y;
+                }
+                path += "ctx.bezierCurveTo(" + points[0].x + "," + points[0].y + ","
+                    + points[1].x + "," + points[1].y + ","
+                    + points[2].x + "," + points[2].y + ");\n";
+                lastc = points[1];
+                c = points[2];
+                break;
+            case 'Q':  // Quadratic Bezier Curve
+                data = find_points(data, points, 2, relative, c);
+                path += "ctx.quadraticCurveTo(" + points[0].x + "," + points[0].y + ","
+                    + points[1].x + "," + points[1].y + ");\n";
+                lastc = points[0];
+                c = points[1];
+                break;
+            case 'T':
+                var pts2 = [];
+                data = find_points(data, pts2, 1, relative, c);
+                points[0] = pts2[0];
+                points[1] = pts2[0];
+                if (previousOp == 'Q' || previousOp == 'T') {
+                    points[0].x = c.x * 2 - lastc.x;
+                    points[0].y = c.y * 2 - lastc.y;
+                }
+                path += "ctx.quadraticCurveTo(" + points[0].x + "," + points[0].y + ","
+                    + points[1].x + "," + points[1].y + ");\n";
+                path.quadTo(points[0], points[1]);
+                lastc = points[0];
+                c = points[1];
+                break;
+            case 'Z':
+                path += "ctx.closePath();\n";
+                c = f;
+                op = '\0';
+                break;
+            case '~':
+                var args = [];
+                data = find_points(data, args, 2, false, null);
+                path += "moveTo(" + args[0].x + "," + args[0].y + ");\n";
+                path += "lineTo(" + args[1].x + "," + args[1].y + ");\n";
+                break;
+            default:
+                return false;
+        }
+        if (previousOp == 0) {
+            f = c;
+        }
+        previousOp = op;
+    }
+    return path;
+}
+
+function CanvasPaths(ctx) {
+    var svgStrs = {
+    // keyframe 1
+        span1:   "M200,200 Q300,300 200,300",
+        span2:   "M200,200 C100,300 100,400 200,300",
+        span3:   "M200,200 C300,100 100,400 300,200",
+        wedge1:  "M200,200 L500,500 A 424.26,424.26 0,0,1 200,624.26 z",
+        wedge2:  "M200,200 L200,624.26 A 424.26,424.26 0,0,1 -100,500 z",
+        wedge3:  "M200,200 L500,-100 A 424.26,424.26 0,0,1 240,622.5 z",
+    // keyframe 2
+        span4:   "M200,200 Q300,300 400,300",
+        span5:   "M200,200 Q280,320 200,400",
+        span6:   "M200,200 Q60,340 100,400",
+        wedge4:  "M200,200 L500,500 A 424.26,424.26 0,0,1 579.47,389.74 z",
+        wedge5:  "M200,200 L389.74,579.47 A 424.26,424.26 0,0,1 200,500 z",
+        wedge6:  "M200,200 L10.26,579.47 A 424.26,424.26 0,0,1 -100,500 z",
+    // keyframe 3
+        xaxis:    "M100,200 L300,200",
+        yaxis:    "M200,100 L200,300",
+        wedgeXY8: "M200,200 L500,500 A 424.26,424.26 0,0,1 624.26,200 z",
+        wedgeXY6: "M200,200 L-100,500 A 424.26,424.26 0,0,1 200,624.26 z",
+        wedgeXY3: "M200,200 L-100,-100 A 424.26,424.26 0,0,1 200,-175.74 z",
+    // keyframe 4
+        wedgeXY1: "M200,200 L500,-100 A 424.26,424.26 0,0,1 624.26,200 z",
+        wedgeXY2: "M200,200 L200,-175.74 A 424.26,424.26 0,0,1 500,-100 z",
+        wedgeXY4: "M200,200 L-175.74,200 A 424.26,424.26 0,0,1 -100,-100 z",
+        wedgeXY5: "M200,200 L-100,500 A 424.26,424.26 0,0,1 -175.74,200 z",
+        wedgeXY7: "M200,200 L200,624.26 A 424.26,424.26 0,0,1 500,500 z",
+        lineSegment: "M200,200 L200,624.26",
+    // keyframe 5
+        curveSegment:  "M200,200 C250,200 300,150 300,100",
+        curveSegment1: "M200,200 C250,200 300,150 300,100",
+        curveSegment2: "M200,200 C250,200 300,150 200,100",
+        curveSegment3: "M200,200 C350,200 250,-150 170,300",
+    // keyframe 6
+        horzSegment: "M200,200 L341.4,200",
+        vertSegment: "M200,200 L200,341.4",
+        diagSegment: "M200,200 L100,100",
+    // keyframe 7
+        cubicSegment:  "M200,200 C200,200 200,200 200,200",
+        cubicSegment1: "M200,200 C200,200 200,200 200,200",
+        cubicSegment2: "M200,200 C250,200 300,200 300,100",
+    };
+    var paths = [];
+    var keys = Object.keys(svgStrs);
+    for (var index in keys) {
+        var key = keys[index];
+        var str = svgStrs[key];
+        var path = parse_path(str);
+        var record = [];
+        paths[key] = {
+            str: str,
+            funcBody: path,
+        };
+    }
+    return paths;
+}
+
+function canvas_fill_font(record) {
+    assert(record);
+    var str = 'ctx.font = "normal 1.3rem Helvetica,Arial";\n';
+    if (record.fillStyle) {
+        str += 'ctx.fillStyle = ' + record.fillStyle + ';\n';
+    }
+    return str;
+}
+
+function canvas_fill_text(record) {
+    assert(record);
+    assert(typeof record.fillText == 'string');
+    return 'ctx.fillText("' + record.fillText + '"';
+}
+
+function canvas_xy(record) {
+    var x = typeof record.x == "number" ? record.x : 400;
+    var y = typeof record.y == "number" ? record.y : 200;
+    return ', ' + x + ', ' + y + ');\n';
+}
+
+function canvas_text_xy(record) {
+    return canvas_fill_text(record) + canvas_xy(record);
+}
+
+function add_canvas_stroke(paths, data, id, strokeStyle) {
+    var record = {};
+    record.data = paths[id].funcBody;
+    record.style = 'ctx.strokeStyle = ' + (strokeStyle ? strokeStyle : '"black"') + ';\n';
+    record.draw = 'ctx.stroke();\n';
+    record.func = new Function('ctx', record.data + record.style + record.draw);
+    return data[id] = record;
+}
+
+function add_canvas_style(record, style) {
+    record.style += style;
+    record.func = new Function('ctx', record.data + record.style + record.draw);
+}
+
+function add_canvas_fill(paths, data, id, fillStyle) {
+    var record = {};
+    record.data = paths[id].funcBody;
+    record.style = 'ctx.fillStyle = ' + (fillStyle ? fillStyle : '"black"') + ';\n';
+    record.draw = 'ctx.fill();\n';
+    record.func = new Function('ctx', record.data + record.style + record.draw);
+    return data[id] = record;
+}
+
+function add_canvas_text(data, id, params) {
+    var record = {};
+    record.style = canvas_fill_font(params);
+    record.draw = canvas_fill_text(params);
+    record.position = canvas_xy(params);
+    record.x = params.x;
+    record.y = params.y;
+    record.func = new Function('ctx', record.style + record.draw + record.position);
+    return data[id] = record;
+}
+
+function keyframe1(grads, paths) {
+    var data = [];
+    add_canvas_text(data, "spanWedgeDesc", { fillText:"All spans are contained by a wedge" } );
+    add_canvas_stroke(paths, data, "span1");
+    add_canvas_stroke(paths, data, "span2");
+    add_canvas_stroke(paths, data, "span3");
+    add_canvas_fill(paths, data, "wedge1", "grads.grad1");
+    add_canvas_fill(paths, data, "wedge2", "grads.grad2");
+    add_canvas_fill(paths, data, "wedge3", "grads.grad3");
+    return data;
+}
+
+function keyframe2(grads, paths) {
+    var data = [];
+    add_canvas_text(data, "trivialWedgeDesc1", { fillText:"Wedges that don't overlap can be" } );
+    add_canvas_text(data, "trivialWedgeDesc2", { fillText:"easily sorted.", y:240 } );
+    add_canvas_stroke(paths, data, "span4").debug = true;
+    add_canvas_stroke(paths, data, "span5");
+    add_canvas_stroke(paths, data, "span6");
+    add_canvas_fill(paths, data, "wedge4", "grads.grad1");
+    add_canvas_fill(paths, data, "wedge5", "grads.grad2");
+    add_canvas_fill(paths, data, "wedge6", "grads.grad3");
+    return data;
+}
+
+function setup_axes(paths, data) {
+    var color = '"rgb(191,191,191)"';
+    var xaxis = add_canvas_stroke(paths, data, "xaxis", color);
+    xaxis.funcBody = canvas_fill_font( { fillStyle:color } );
+    xaxis.funcBody += canvas_text_xy( { fillText:"-X", x:100, y:220 } );
+    xaxis.funcBody += "ctx.textAlign = 'right';\n";
+    xaxis.funcBody += canvas_text_xy( { fillText:"+X", x:300, y:220 } );
+    xaxis.func = new Function('ctx', xaxis.data + xaxis.style + xaxis.draw + xaxis.funcBody);
+    var yaxis = add_canvas_stroke(paths, data, "yaxis", color);
+    yaxis.funcBody = canvas_fill_font( { fillStyle:color } );
+    yaxis.funcBody += "ctx.textBaseline = 'hanging';\n";
+    yaxis.funcBody += canvas_text_xy( { fillText:"-Y", x:205, y:100 } );
+    yaxis.funcBody += "ctx.textBaseline = 'alphabetic';\n";
+    yaxis.funcBody += canvas_text_xy( { fillText:"+Y", x:205, y:300 } );
+    yaxis.func = new Function('ctx', yaxis.data + yaxis.style + yaxis.draw + yaxis.funcBody);
+}
+
+function keyframe3(grads, paths) {
+    var data = [];
+    add_canvas_text(data, "sectorDesc1", { fillText:"A sector is a wedge of a circle" } );
+    add_canvas_text(data, "sectorDesc2", { fillText:"containing a range of points.", y:240 } );
+    setup_axes(paths, data);
+    add_canvas_text(data, "sectorDescXYA",
+        { fillText:"X > 0   Y > 0    Y < X", x:500, y:310, fillStyle:'"rgb(0,0,255)"'} );
+    add_canvas_text(data, "sectorDescXYB",
+        { fillText:"X < 0   Y > 0   -Y < X", x:500, y:360, fillStyle:'"rgb(0,127,0)"'} );
+    add_canvas_text(data, "sectorDescXYC",
+        { fillText:"X < 0   Y < 0    Y < X", x:500, y:410, fillStyle:'"rgb(255,0,0)"'} );
+    add_canvas_fill(paths, data, "wedgeXY8", "grads.grad1");
+    add_canvas_fill(paths, data, "wedgeXY6", "grads.grad2");
+    add_canvas_fill(paths, data, "wedgeXY3", "grads.grad3");
+    return data;
+}
+
+function keyframe4(grads, paths) {
+    var data = [];
+    setup_axes(paths, data);
+    add_canvas_text(data, "lineSingleDesc",
+        { fillText:"Line spans are contained by a single sector." } );
+    add_canvas_text(data, "sectorDescXY1",
+        { fillText:"X > 0   Y < 0   -Y < X", x:500, y:460, fillStyle:'"rgb(192,63,192)"'} );
+    add_canvas_text(data, "sectorDescXY2",
+        { fillText:"X > 0   Y < 0   -Y > X", x:500, y:460, fillStyle:'"rgb(127,127,0)"'} );
+    add_canvas_text(data, "sectorDescXY3",
+        { fillText:"X < 0   Y < 0    Y < X", x:500, y:460, fillStyle:'"rgb(255,0,0)"'} );
+    add_canvas_text(data, "sectorDescXY4",
+        { fillText:"X < 0   Y < 0    Y > X", x:500, y:460, fillStyle:'"rgb(127,0,127)"'} );
+    add_canvas_text(data, "sectorDescXY5",
+        { fillText:"X < 0   Y > 0   -Y < X", x:500, y:460, fillStyle:'"rgb(0,127,127)"'} );
+    add_canvas_text(data, "sectorDescXY6",
+        { fillText:"X < 0   Y > 0   -Y < X", x:500, y:460, fillStyle:'"rgb(0,127,0)"'} );
+    add_canvas_text(data, "sectorDescXY7",
+        { fillText:"X > 0   Y > 0    Y > X", x:500, y:460, fillStyle:'"rgb(63,192,63)"'} );
+    add_canvas_text(data, "sectorDescXY8",
+        { fillText:"X > 0   Y > 0    Y < X", x:500, y:460, fillStyle:'"rgb(0,0,255)"'} );
+    add_canvas_fill(paths, data, "wedgeXY1", "grads.grad4");
+    add_canvas_fill(paths, data, "wedgeXY2", "grads.grad5");
+    add_canvas_fill(paths, data, "wedgeXY3", "grads.grad3");
+    add_canvas_fill(paths, data, "wedgeXY4", "grads.grad6");
+    add_canvas_fill(paths, data, "wedgeXY5", "grads.grad7");
+    add_canvas_fill(paths, data, "wedgeXY6", "grads.grad2");
+    add_canvas_fill(paths, data, "wedgeXY7", "grads.grad8");
+    add_canvas_fill(paths, data, "wedgeXY8", "grads.grad1");
+    add_canvas_stroke(paths, data, "lineSegment");
+    return data;
+}
+
+function keyframe5(grads, paths) {
+    var data = [];
+    setup_axes(paths, data);
+    add_canvas_text(data, "curveMultipleDesc1",
+        { fillText:"A curve span may cover more" } );
+    add_canvas_text(data, "curveMultipleDesc2",
+        { fillText:"than one sector.", y:240 } );
+    add_canvas_stroke(paths, data, "curveSegment");
+    add_canvas_text(data, "sectorDescXY1",
+        { fillText:"X > 0   Y < 0   -Y < X", x:500, y:460, fillStyle:'"rgb(192,63,192)"'} );
+    add_canvas_text(data, "sectorDescXY2",
+        { fillText:"X > 0   Y < 0   -Y > X", x:500, y:460, fillStyle:'"rgb(127,127,0)"'} );
+    add_canvas_text(data, "sectorDescXY3",
+        { fillText:"X < 0   Y < 0    Y < X", x:500, y:460, fillStyle:'"rgb(255,0,0)"'} );
+    add_canvas_text(data, "sectorDescXY4",
+        { fillText:"X < 0   Y < 0    Y > X", x:500, y:460, fillStyle:'"rgb(127,0,127)"'} );
+    add_canvas_text(data, "sectorDescXY5",
+        { fillText:"X < 0   Y > 0   -Y < X", x:500, y:460, fillStyle:'"rgb(0,127,127)"'} );
+    add_canvas_text(data, "sectorDescXY6",
+        { fillText:"X < 0   Y > 0   -Y < X", x:500, y:460, fillStyle:'"rgb(0,127,0)"'} );
+    add_canvas_fill(paths, data, "wedgeXY1", "grads.grad4");
+    add_canvas_fill(paths, data, "wedgeXY2", "grads.grad5");
+    add_canvas_fill(paths, data, "wedgeXY3", "grads.grad3");
+    add_canvas_fill(paths, data, "wedgeXY4", "grads.grad6");
+    add_canvas_fill(paths, data, "wedgeXY5", "grads.grad7");
+    add_canvas_fill(paths, data, "wedgeXY6", "grads.grad2");
+    return data;
+}
+
+function keyframe6(grads, paths) {
+    var data = [];
+    setup_axes(paths, data);
+
+    add_canvas_text(data, "line1DDest1",
+        { fillText:"Some lines occupy one-dimensional" } );
+    add_canvas_text(data, "line1DDest2",
+        { fillText:"sectors.", y:240 } );
+    add_canvas_text(data, "sectorDescXY9",
+        { fillText:"X > 0   Y == 0", x:500, y:460, fillStyle:'"rgb(192,92,31)"' } );
+    add_canvas_text(data, "sectorDescXY10",
+        { fillText:"Y > 0   0 == X", x:500, y:460, fillStyle:'"rgb(31,92,192)"' } );
+    add_canvas_text(data, "sectorDescXY11",
+        { fillText:"X < 0   Y == X", x:500, y:460, fillStyle:'"rgb(127,63,127)"' } );
+    var horz = add_canvas_stroke(paths, data, "horzSegment", '"rgb(192,92,31)"');
+    add_canvas_style(horz, "ctx.lineWidth = " + 2 * animationState.resScale + ";\n");
+    var vert = add_canvas_stroke(paths, data, "vertSegment", '"rgb(31,92,192)"');
+    add_canvas_style(vert, "ctx.lineWidth = " + 2 * animationState.resScale + ";\n");
+    var diag = add_canvas_stroke(paths, data, "diagSegment", '"rgb(127,63,127)"');
+    add_canvas_style(diag, "ctx.lineWidth = " + 2 * animationState.resScale + ";\n");
+    return data;
+}
+
+function keyframe7(grads, paths) {
+    var data = [];
+    setup_axes(paths, data);
+    add_canvas_text(data, "curve1dDesc1",
+        { fillText:"Some curves initially occupy" } );
+    add_canvas_text(data, "curve1dDesc2",
+        { fillText:"one-dimensional sectors, then diverge.", y:240 } );
+    add_canvas_stroke(paths, data, "cubicSegment");
+    add_canvas_text(data, "sectorDescXY1",
+        { fillText:"X > 0   Y < 0   -Y < X", x:500, y:460, fillStyle:'"rgb(192,63,192)"'} );
+    add_canvas_text(data, "sectorDescXY9",
+        { fillText:"X > 0   Y == 0", x:500, y:460, fillStyle:'"rgb(192,92,31)"' } );
+    var horz = add_canvas_stroke(paths, data, "horzSegment", '"rgb(192,92,31)"');
+    add_canvas_style(horz, "ctx.lineWidth = " + 2 * animationState.resScale + ";\n");
+    add_canvas_fill(paths, data, "wedgeXY1", "grads.grad4");
+    return data;
+}
+
+var canvasData = null;
+
+function CanvasInit(keyframe) {
+    canvasData = window[keyframe](grads, paths);
+}
+
+</script>
+
+<script>
+
+function interp(A, B, t) {
+    return A + (B - A) * t;
+}
+
+function interp_cubic_coords(x1, x2, x3, x4, t)
+{
+    var ab = interp(x1, x2, t);
+    var bc = interp(x2, x3, t);
+    var cd = interp(x3, x4, t);
+    var abc = interp(ab, bc, t);
+    var bcd = interp(bc, cd, t);
+    var abcd = interp(abc, bcd, t);
+    return abcd;
+}
+
+function cubic_partial(value, p) {
+    var x1 = p[0], y1 = p[1], x2 = p[2], y2 = p[3];
+    var x3 = p[4], y3 = p[5], x4 = p[6], y4 = p[7];
+    var t1 = 0, t2 = value;
+    var ax = interp_cubic_coords(x1, x2, x3, x4, t1);
+    var ay = interp_cubic_coords(y1, y2, y3, y4, t1);
+    var ex = interp_cubic_coords(x1, x2, x3, x4, (t1*2+t2)/3);
+    var ey = interp_cubic_coords(y1, y2, y3, y4, (t1*2+t2)/3);
+    var fx = interp_cubic_coords(x1, x2, x3, x4, (t1+t2*2)/3);
+    var fy = interp_cubic_coords(y1, y2, y3, y4, (t1+t2*2)/3);
+    var dx = interp_cubic_coords(x1, x2, x3, x4, t2);
+    var dy = interp_cubic_coords(y1, y2, y3, y4, t2);
+    var mx = ex * 27 - ax * 8 - dx;
+    var my = ey * 27 - ay * 8 - dy;
+    var nx = fx * 27 - ax - dx * 8;
+    var ny = fy * 27 - ay - dy * 8;
+    var bx = (mx * 2 - nx) / 18;
+    var by = (my * 2 - ny) / 18;
+    var cx = (nx * 2 - mx) / 18;
+    var cy = (ny * 2 - my) / 18;
+    var array = [
+        ax, ay, bx, by, cx, cy, dx, dy
+    ];
+    return array;
+}
+
+function evaluate_at(value, p) {
+    var array = [];
+    for (var index = 0; index < p.length; ++index) {
+        var func = new Function('value', 'return ' + p[index] + ';');
+        array[index] = func(value);
+    }
+    return array;
+}
+
+function interpolate_at(value, p) {
+    var array = [];
+    var start = p[0];
+    var end = p[1];
+    assert(typeof end == typeof start);
+    switch (typeof start) {
+        case 'object':
+            for (var index = 0; index < start.length; ++index) {
+                array[index] = interp(start[index], end[index], value);
+            }
+            break;
+        case 'number':
+            array[index] = interp(start, end, value);
+            break;
+        default:
+            debugger;
+    }
+    return array;
+}
+
+function AnimationAddCommon(timing, range, attr, inParams) {
+    var animation = {
+        timing: timing,
+        range: range,
+        attr: attr,
+        inParams: inParams,
+        duration: timing[1] - timing[0],
+        remaining: timing[1] - timing[0],
+        firstStep: true,
+    }
+    animationsPending.push(animation);
+    return animation;
+}
+
+function AnimationAddSVG(timing, element, range, attr, inParams) {
+    var animation = AnimationAddCommon(timing, range, attr, inParams);
+    animation.element = element;
+    return animation;
+}
+
+function AnimationAddCanvas(timing, element, range, attr, inParams) {
+    var animation = AnimationAddCommon(timing, range, attr, inParams);
+    animation.element = canvasData[element];
+    assert(animation.element);
+    animation.firstElement = null;
+    return animation;
+}
+
+function AnimationAdd(timing, e, range, attr, funct, inParams) {
+    if (!range) {
+        range = [0, 1];
+    }
+    if (!attr) {
+        attr = 'opacity';
+    }
+    if (!funct) {
+        funct = interpolate_at;
+    }
+    var element;
+    switch (animationState.displayEngine) {
+        case 'SVG':
+            element = typeof e == 'string' ? document.getElementById(e) : e;
+            break;
+        case 'Canvas':
+            element = typeof e == 'string' ? e : e.id;
+            break;
+        default:
+            debugger;
+    }
+    assert(element);
+    switch (attr) {
+        case 'path':
+            if (!inParams) {
+                inParams = PathDataArray(element);
+            }
+            break;
+        case 'opacity':
+            if (!inParams) {
+                inParams = [0, 1];
+            }
+            break;
+        default:
+            debugger;
+    }
+    var funcBody = 'var outParams = '  + funct.name + '(value, inParams);\n';
+    switch (animationState.displayEngine) {
+        case 'SVG':
+            switch (attr) {
+                case 'path':
+                    var verbArray = PathVerbArray(element);
+                    funcBody += 'return ';
+                    for (var index = 0; index < inParams.length; ++index) {
+                        funcBody += '"' + verbArray[index] + '"';
+                        funcBody += 'outParams[' + index + '];\n';
+                    }
+                    if (verbArray.length > inParams.length) {
+                        funcBody += '"' + verbArray[verbArray.length - 1] + '"';
+                    }
+                    funcBody += ';\n';
+                    var animation = AnimationAddSVG(timing, element, range, "d", inParams);
+                    animation.func = new Function('value', 'inParams', funcBody);
+                    break;
+               case 'opacity':
+                    if (animation.element.getAttribute("stroke-opacity")) {
+                        animation = AnimationAddSVG(timing, element, range, "stroke-opacity", inParams);
+                    }
+                    if (animation.element.getAttribute("fill-opacity")) {
+                        animation = AnimationAddSVG(timing, element, range, "fill-opacity", inParams);
+                    }
+                    break;
+                default:
+                    debugger;
+            }
+        case 'Canvas':
+            switch (attr) {
+                case 'path':
+                    var verbArray = PathVerbArray(element);
+                    for (var index = 0; index < inParams.length; ++index) {
+                        funcBody += verbArray[index];
+                        funcBody += 'outParams[' + index + ']';
+                    }
+                    if (verbArray.length > inParams.length) {
+                        funcBody += verbArray[verbArray.length - 1];
+                    }
+                    animation = AnimationAddCanvas(timing, element, range, attr, inParams);
+                    funcBody += animation.element.style + animation.element.draw;
+                    animation.func = new Function('ctx', 'value', 'inParams', funcBody);
+                    break;
+               case 'opacity':
+                    animation = AnimationAddCanvas(timing, element, range, attr, inParams);
+                    break;
+                default:
+                    debugger;
+            }
+            break;
+        default:
+            debugger;
+    }
+    return animation;
+}
+
+function path_data_common(element, getValues) {
+    var numRegEx = /-?\d+\.?\d*(?:e-?\d+)?/g;
+    var data = [];
+    var match;
+    var path;
+    switch (animationState.displayEngine) {
+        case 'SVG': path = element.getAttribute("d"); break;
+        case 'Canvas': path = paths[element].funcBody; break;
+        default: debugger;
+    }
+    if (getValues) {
+        while ((match = numRegEx.exec(path))) {
+            data.push(Number(match[0]));
+        }
+    } else {
+        var sIndex = 0;
+        while ((match = numRegEx.exec(path))) {
+            if (sIndex < match.index) {
+                data.push(path.substring(sIndex, match.index));
+            }
+            sIndex = match.index + match[0].length;
+        }
+        if (sIndex < path.length) {
+            data.push(path.substring(sIndex, path.length));
+        }
+    }
+    return data;
+}
+
+function PathDataArray(element) {
+    return path_data_common(element, true);
+}
+
+function PathVerbArray(element) {
+    return path_data_common(element, false);
+}
+
+function PathSet(element, funct, value, params) {
+    var pathVerbs = PathVerbArray(element);
+    if (funct) {
+        params = funct(value, params);
+    }
+   var setValue = '';
+    for (var index = 0; index < params.length; ++index) {
+        setValue += pathVerbs[index];
+        setValue += params[index];
+    }
+    if (pathVerbs.length > params.length) {
+        setValue += pathVerbs[pathVerbs.length - 1];
+    }
+    switch (animationState.displayEngine) {
+        case 'SVG':
+            element.setAttribute('d', setValue);
+            break;
+        case 'Canvas':
+            element.func = new Function('ctx', setValue + element.style + element.draw);
+            break;
+        default:
+            debugger;
+    }
+}
+
+function RemoveFromArray(array, element) {
+    for (var index in array) {
+        var record = array[index];
+        if (record.element == element) {
+            array.splice(index, 1);
+            break;
+        }
+    }
+}
+
+function EndAnimationCanvas(animation, visibleFinished) {
+    var changeAlpha = "opacity" == animation.attr;
+    if (!changeAlpha || animation.range[1] > 0) {
+        if (changeAlpha) {
+            ctx.save();
+            ctx.globalAlpha = animation.range[1];
+        }
+        if (animation.func) {
+            animation.func(ctx, animation.range[animation.range.length - 1], animation.inParams);
+        } else {
+            animation.element.func(ctx);
+        }
+        if (changeAlpha) {
+            ctx.restore();
+        }
+//        if (visibleFinished) {
+//            visibleFinished.push(animation);
+//        }
+    } else {
+ //       if (visibleFinished) {
+ //           RemoveFromArray(visibleFinished, animation.element);
+ //       }
+    }
+}
+
+/* start here
+canvas:
+
+display list :
+    for each element (canvas)
+        save
+        set global alpha (override)
+        create geometry (override)
+        create style (override)
+        draw
+        restore
+        
+maybe each action should have an override slot
+animations write to the slot
+each element in display list then iterates overrides once the animations complete the frame
+
+so, first -- 
+    active animations update the display list
+    
+next --
+    active animations install themselves in override slots
+    
+finally
+    display list is iterated, calling override slots
+
+----------------
+    
+svg:
+    display list is implicit
+    
+    active animations write element attributes
+ */
+
+function EndAnimationSVG(animation, visibleFinished) {
+    switch (animation.attr) {
+        case "opacity":
+            animation.element.setAttribute(animation.attribute, animation.range[1]);
+            if (animation.range[1] > 0) {
+                visibleFinished.push(animation);
+            } else {
+                RemoveFromArray(visibleFinished, animation.element);
+            }
+            break;
+        case "path":
+            var attrStr = animation.func(animation.range[1], animation.inParams);
+            animation.element.setAttribute(animation.attribute, attrStr);
+            break;
+        default:
+            debugger;
+    }
+}
+
+function StepAnimationCanvas(animation, value) {
+    var endValue = animation.range[animation.range.length - 1];
+    var interp = animation.range[0] + (endValue - animation.range[0]) * (1 - value);
+    if (animation.firstStep) {
+        RemoveFromArray(visibleFinished, animation.element);
+        animation.firstStep = false;
+    }
+    var changeAlpha = "opacity" == animation.attr;
+    if (changeAlpha) {
+        ctx.save();
+        ctx.globalAlpha = interp;
+    }
+    if (animation.func) {
+        animation.func(ctx, interp, animation.inParams);
+    } else {
+        animation.element.func(ctx);
+    }
+    if (changeAlpha) {
+        ctx.restore();
+    }
+}
+
+function StepAnimationSVG(animation, value) {
+    var interp = animation.range[0] + (animation.range[1] - animation.range[0]) * (1 - value);
+    switch (animation.attr) {
+        case "opacity":
+            animation.element.setAttribute(animation.attribute, interp);
+            break;
+        case "path":
+            var attrStr = animation.func(interp, animation.inParams);
+            animation.element.setAttribute(animation.attribute, attrStr);
+            break;
+        default:
+            debugger;
+    }
+}
+
+var animate_frame = 0;
+
+function AnimateList(now) {
+    ++animate_frame;
+    if (animationState.paused) {
+        return;
+    }
+    if (animationState.start == null) {
+        animationState.start = now - animationState.time;
+    }
+    animationState.time = now - animationState.start;
+    var stillPending = [];
+    for (var index in animationsPending) {
+        var animation = animationsPending[index];
+        var interval = animationState.time - animation.timing[0];
+        if (interval <= 0) {
+            stillPending.push(animation);
+            continue;
+        }
+        animationsActive.push(animation);
+        var inList = false;
+        for (var dlIndex in displayList) {
+            var displayable = displayList[dlIndex];
+            if (displayable == animation.element) {
+                inList = true;
+                break;
+            }
+        }
+        if (!inList) {
+            displayList.push(animation.element);
+        }
+    }
+    animationsPending = stillPending;
+    var stillAnimating = [];
+    if ('Canvas' == animationState.displayEngine) {
+        ctx.clearRect(0, 0, canvas.width, canvas.height);
+//        for (var index in visibleFinished) {
+//           var animation = visibleFinished[index];
+//           animation.endAnimation = false;
+//        }
+    }
+    for (var index in animationsActive) {
+        var animation = animationsActive[index];
+        var interval = animationState.time - animation.timing[0];
+        animation.remaining = animation.duration > interval ? animation.duration - interval : 0;
+        animation.endAnimation = animation.remaining <= 0;
+        if (animation.endAnimation) {
+            switch (animationState.displayEngine) {
+                case 'SVG':  EndAnimationSVG(animation, visibleFinished); break;
+                case 'Canvas': EndAnimationCanvas(animation, visibleFinished); break;
+                default: debugger;
+            }
+            continue;
+        }
+        var value = animation.remaining / animation.duration;
+        switch (animationState.displayEngine) {
+            case 'SVG': StepAnimationSVG(animation, value); break;
+            case 'Canvas':
+                if (!animation.firstElement || !animation.firstElement.endAnimation) {
+                    StepAnimationCanvas(animation, value);
+                }
+            break;
+            default: debugger;
+        }
+        stillAnimating.push(animation);
+    }
+    if ('Canvas' == animationState.displayEngine) {
+        for (var index in visibleFinished) {
+            var animation = visibleFinished[index];
+            if (!animation.endAnimation) {
+                EndAnimationCanvas(animation, null);
+            }
+        }
+    }
+    animationsActive = stillAnimating;
+    if (animationsPending.length || animationsActive.length) {
+        animationState.requestID = requestAnimationFrame(AnimateList);
+    }
+}
+
+function CancelAnimate(now) {
+    if (animationState.start == null) {
+        animationState.start = now;
+    }
+    var time = now - animationState.start;
+    var stillAnimating = [];
+    for (var index in animationsActive) {
+        var animation = animationsActive[index];
+        var remaining = animation.remaining - time;
+        var value = remaining / animation.duration;
+        switch (animationState.displayEngine) {
+            case 'SVG': animation.element.setAttribute(animation.attribute, value); break;
+            case 'Canvas': break;
+        }
+        if (remaining <= 0) {
+            continue;
+        }
+        stillAnimating.push(animation);
+    }
+    animationsActive = stillAnimating;
+    if (animationsActive.length) {
+        animationState.requestID = requestAnimationFrame(CancelAnimate);
+        return;
+    }
+    animationsPending = [];
+    animationState.reset();
+    if (keyFrameQueue.length > 0) {
+        var animationFunc = keyFrameQueue.pop();
+        animationFunc();
+    }
+}
+
+function CancelAnimation() {
+    cancelAnimationFrame(animationState.requestID);
+    for (var index in animationsActive) {
+        var animation = animationsActive[index];
+        switch (animation.attr) {
+            case "opacity":
+                var tmp = animation.range[0]; animation.range[0] = animation.range[1]; animation[1] = tmp;
+                animation.remaining = animation.duration - animation.remaining;
+                animation.remaining /= animation.duration / 1000;
+                animation.duration = 1000;
+                break;
+            case "fadeOut":
+                RemoveFromArray(visibleFinished, animation.element);
+                break;
+            case "path":
+                break;
+            default:
+                debugger;
+
+        }
+    }
+    for (var index in visibleFinished) {
+        var animation = visibleFinished[index];
+        animation.action = "fadeOut";
+        animation.remaining = animation.duration = 1000;
+        animationsActive.push(animation);
+    }
+    visibleFinished = [];
+    animationState.reset();
+    animationState.requestID = requestAnimationFrame(CancelAnimate);
+}
+
+function PauseAnimation() {
+    animationState.paused = true;
+}
+
+function QueueAnimation(animationFunc) {
+    if (null == animationState.requestID) {
+        animationFunc();
+        return;
+    }
+    keyFrameQueue.push(animationFunc);
+}
+
+function UnpauseAnimation() {
+    animationState.paused = false;
+    animationState.start = performance.now() - animationState.time;
+    animationState.requestID = requestAnimationFrame(AnimateList);
+}
+
+function SetupTextSVG(t, x, y) {
+    var text;
+    if (typeof t == "string") {
+        text = document.getElementById(t);
+    } else {
+        text = t;
+    }
+    text.setAttribute("font-family", "Helvetica,Arial");
+    text.setAttribute("font-size", "1.3rem");
+    if (typeof x == 'number') {
+        text.setAttribute("x", x);
+    } else if (null == text.getAttribute("x")) {
+        text.setAttribute("x", 400);
+    }
+    if (typeof y == 'number') {
+        text.setAttribute("y", y);
+    } else if (null == text.getAttribute("y")) {
+        text.setAttribute("y", 200);
+    }
+}
+
+function SetupTextCanvas(t, x, y) {
+    var text = typeof t == 'string' ? t : t.id;
+    var record = canvasData[text];
+    if (typeof x == 'number') {
+        record.x = x;
+    }
+    if (typeof y == 'number') {
+        record.y = y;
+    }
+    record.position = canvas_xy(record);
+    record.func = new Function('ctx', record.style + record.draw + record.position);
+}
+
+function SetupText(t, x, y) {
+    switch (animationState.displayEngine) {
+        case 'SVG':
+            SetupTextSVG(t, x, y);
+            break;
+        case 'Canvas':
+            SetupTextCanvas(t, x, y);
+            break;
+        default:
+            debugger;
+    }
+}
+
+function FirstText(text) {
+    SetupText(text);
+    AnimationAdd([0, 1000], text);
+}
+
+
+function EngineInit(keyframe) {
+    displayList = [];
+    switch (animationState.displayEngine) {
+        case 'SVG': break;
+        case 'Canvas': CanvasInit(keyframe); break;
+        default: debugger;
+    }
+}
+
+function EngineStart() {
+    switch (animationState.displayEngine) {
+        case 'SVG': break;
+        case 'Canvas':
+            // associate fadeIn and fadeOut
+            for (var outerIndex in animationsPending) {
+                var outer = animationsPending[outerIndex];
+                for (var innerIndex in animationsPending) {
+                    if (outerIndex == innerIndex) {
+                        continue;
+                    }
+                    var inner = animationsPending[innerIndex];
+                    if (inner.element == outer.element) {
+                        inner.firstElement = outer;
+                        continue;
+                    }
+                }
+            }
+            break;
+        default: debugger;
+    }
+    animationState.reset();
+    animationState.requestID = requestAnimationFrame(AnimateList);
+}
+
+function AnimateSpanWedge() {
+    EngineInit('keyframe1');
+    FirstText(spanWedgeDesc);
+    AnimationAdd([1000, 2000], span1);
+    AnimationAdd([1500, 3000], wedge1);
+    AnimationAdd([3500, 4000], span1,  [1, 0]);
+    AnimationAdd([3500, 4000], wedge1, [1, 0]);
+    AnimationAdd([4000, 5000], span2);
+    AnimationAdd([4500, 6000], wedge2);
+    AnimationAdd([6500, 7000], span2,  [1, 0]);
+    AnimationAdd([6500, 7000], wedge2, [1, 0]);
+    AnimationAdd([7000, 8000], span3);
+    AnimationAdd([7500, 9000], wedge3);
+    EngineStart();
+}
+
+function AnimateTrivialWedge() {
+    EngineInit('keyframe2');
+    FirstText(trivialWedgeDesc1);
+    FirstText(trivialWedgeDesc2);
+    AnimationAdd([2000, 3500], span4);
+    AnimationAdd([2000, 3500], wedge4);
+    AnimationAdd([2000, 3500], span5);
+    AnimationAdd([2000, 3500], wedge5);
+    AnimationAdd([2000, 3500], span6);
+    AnimationAdd([2000, 3500], wedge6);
+    EngineStart();
+}
+
+function AnimateSectorDesc() {
+    EngineInit('keyframe3');
+    FirstText(sectorDesc1);
+    FirstText(sectorDesc2);
+    AnimationAdd([   0, 1000], xaxis);
+    AnimationAdd([ 500, 1500], yaxis);
+    AnimationAdd([2000, 3500], sectorDescXYA);
+    AnimationAdd([2000, 3500], wedgeXY8);
+    AnimationAdd([3000, 4500], sectorDescXYB);
+    AnimationAdd([3000, 4500], wedgeXY6);
+    AnimationAdd([4000, 5500], sectorDescXYC);
+    AnimationAdd([4000, 5500], wedgeXY3);
+    EngineStart();
+}
+
+function AnimateLineSingle() {
+    EngineInit('keyframe4');
+    FirstText(lineSingleDesc);
+    for (var i = 1; i <= 8; ++i) {
+        SetupText("sectorDescXY" + i, 500, 260);
+    }
+    AnimationAdd([   0, 1000], xaxis);
+    AnimationAdd([   0, 1000], yaxis);
+    AnimationAdd([1000, 2000], lineSegment);
+    AnimationAdd([1000, 3000], lineSegment, [-22.5 * Math.PI / 180], "path", evaluate_at,
+            [ circle.center.x, circle.center.y,
+              circle.center.x + " + " + circle.radius + " * Math.cos(value)",
+              circle.center.y + " + " + circle.radius + " * Math.sin(value)",
+            ]);
+    AnimationAdd([2000, 3000], sectorDescXY1);
+    AnimationAdd([2000, 3000], wedgeXY1);
+    AnimationAdd([3000, 7000], lineSegment, [-22.5 * Math.PI / 180, (-22.5 - 360) * Math.PI / 180],
+            "path", evaluate_at,
+            [ circle.center.x, circle.center.y,
+              circle.center.x + " + " + circle.radius + " * Math.cos(value)",
+              circle.center.y + " + " + circle.radius + " * Math.sin(value)",
+            ]);
+    for (var i = 1; i < 8; ++i) {
+        AnimationAdd([2500 + 500 * i, 3000 + 500 * i], "sectorDescXY" + (i + 1));
+        AnimationAdd([2500 + 500 * i, 3000 + 500 * i], "wedgeXY" + (i + 1));
+        AnimationAdd([2500 + 500 * i, 3000 + 500 * i], "sectorDescXY" + i,       [1, 0]);
+        AnimationAdd([2500 + 500 * i, 3000 + 500 * i], "wedgeXY" + i,            [1, 0]);
+    }
+    AnimationAdd([2500 + 500 * 8, 3000 + 500 * 8], sectorDescXY1);
+    AnimationAdd([2500 + 500 * 8, 3000 + 500 * 8], wedgeXY1);
+    AnimationAdd([2500 + 500 * 8, 3000 + 500 * 8], sectorDescXY8, [1, 0]);
+    AnimationAdd([2500 + 500 * 8, 3000 + 500 * 8], wedgeXY8,      [1, 0]);
+    EngineStart();
+}
+
+function AnimateCurveMultiple() {
+    EngineInit('keyframe5');
+    var cubicStart = PathDataArray(curveSegment1);
+    var cubicMid = PathDataArray(curveSegment2);
+    var cubicEnd = PathDataArray(curveSegment3);
+    FirstText(curveMultipleDesc1);
+    FirstText(curveMultipleDesc2);
+    for (var i = 1; i <= 6; ++i) {
+        SetupText("sectorDescXY" + i, 500, 260 + i * 25);
+    }
+    AnimationAdd([   0, 1000], xaxis);
+    AnimationAdd([   0, 1000], yaxis);
+    AnimationAdd([1000, 2000], curveSegment);
+    AnimationAdd([2000, 3000], sectorDescXY1);
+    AnimationAdd([2000, 3000], wedgeXY1);
+    AnimationAdd([3000, 4000], curveSegment, [0, 1], "path", interpolate_at, [cubicStart, cubicMid]);
+    AnimationAdd([4000, 5000], sectorDescXY2);
+    AnimationAdd([4000, 5000], wedgeXY2);
+    AnimationAdd([5000, 6000], curveSegment, [0, 1], "path", interpolate_at, [cubicMid, cubicEnd]);
+    AnimationAdd([6000, 7000], sectorDescXY3);
+    AnimationAdd([6000, 7000], wedgeXY3);
+    AnimationAdd([6000, 7000], sectorDescXY4);
+    AnimationAdd([6000, 7000], wedgeXY4);
+    AnimationAdd([6000, 7000], sectorDescXY5);
+    AnimationAdd([6000, 7000], wedgeXY5);
+    AnimationAdd([6000, 7000], sectorDescXY6);
+    AnimationAdd([6000, 7000], wedgeXY6);
+    EngineStart();
+}
+
+function AnimateOneDLines() {
+    EngineInit('keyframe6');
+    FirstText(line1DDest1);
+    FirstText(line1DDest2);
+    for (var i = 9; i <= 11; ++i) {
+        SetupText("sectorDescXY" + i, 500, 260 + (i - 8) * 25);
+    }
+    AnimationAdd([   0, 1000], xaxis);
+    AnimationAdd([   0, 1000], yaxis);
+    AnimationAdd([2000, 3000], sectorDescXY9);
+    AnimationAdd([2000, 3000], horzSegment);
+    AnimationAdd([3000, 4000], sectorDescXY10);
+    AnimationAdd([3000, 4000], vertSegment);
+    AnimationAdd([4000, 5000], sectorDescXY11);
+    AnimationAdd([4000, 5000], diagSegment);
+    EngineStart();
+}
+
+function AnimateDiverging() {
+    EngineInit('keyframe7');
+    var cubicData = PathDataArray(cubicSegment2);
+    FirstText(curve1dDesc1);
+    FirstText(curve1dDesc2);
+    SetupText("sectorDescXY9", 500, 285);
+    SetupText("sectorDescXY1", 500, 320);
+    AnimationAdd([   0, 1000], xaxis);
+    AnimationAdd([   0, 1000], yaxis);
+    AnimationAdd([1900, 1900], cubicSegment);
+    AnimationAdd([2000, 3000], cubicSegment, [0, 1], "path", cubic_partial, cubicData);
+    AnimationAdd([2000, 3000], sectorDescXY9);
+    AnimationAdd([2000, 3000], horzSegment);
+    AnimationAdd([3000, 4000], sectorDescXY1);
+    AnimationAdd([3000, 4000], wedgeXY1);
+    EngineStart();
+}
+
+circle.animate = AnimateCircle;
+circle.start = null;
+
+function AngleToPt(center, radius, degrees) {
+    var radians = degrees * Math.PI / 180.0;
+    return {
+        x: center.x + (radius * Math.cos(radians)),
+        y: center.y - (radius * Math.sin(radians))
+    };
+}
+
+function PtsToSweep(pt1, pt2, center) {  // unused
+    return {
+     start: 180 / Math.PI * Math.atan2(pt1.y - center.y, pt1.x - center.x),
+     end:   180 / Math.PI * Math.atan2(pt2.y - center.y, pt2.x - center.x)
+    };
+}
+
+
+function ArcStr(center, radius, startAngle, endAngle) {
+    var endPt = AngleToPt(center, radius, endAngle);
+    var arcSweep = endAngle - startAngle <= 180 ? "0" : "1";
+    return ["A", radius, radius, 0, arcSweep, 0, endPt.x, endPt.y].join(" ");
+}
+
+function ArcStart(center, radius, startAngle, endAngle) {
+    var startPt = AngleToPt(center, radius, startAngle);
+    return [ startPt.x, startPt.y, ArcStr(center, radius, startAngle, endAngle) ].join(" ");
+}
+
+function MakeArc(arcStart) {
+    return "M" + arcStart;
+}
+
+function MakeWedge(center, arcStart) {
+    return ["M", center.x, center.y, "L", arcStart, "z"].join(" ");
+}
+
+function Animate(path, now, dur) {
+    if (path.start == null) {
+        path.start = now;
+//        console.log("start=" + now);
+    }
+    if (now - path.start < dur) {
+        requestAnimationFrame(path.animate);
+        return true;
+    }
+    return false;
+}
+
+function AnimateCircle(now) {
+    if (circle.start == null) {
+        circleFill.setAttribute("fill-opacity", "0.3");
+    }
+    var dur = 2 * 1000;
+    var animating = Animate(circle, now, dur);
+//    console.log("now=" + now + "circle.start=" + circle.start )
+    var pathStr = ArcStart(circle.center, circle.radius, 0, (now - circle.start) / (dur / 359.9));
+
+    circle.setAttribute("d", MakeArc(pathStr));
+    circleFill.setAttribute("d", MakeWedge(circle.center, pathStr));
+    if (!animating) {
+        var delay = dur - (now - circle.start);
+        setTimeout(CircleFinal, delay);
+    }
+}
+
+function CircleFinal() {
+    var firstHalf = ArcStart(circle.center, circle.radius, 0, 180);
+    var secondHalf = ArcStr(circle.center, circle.radius, 180, 360);
+    circle.setAttribute("d", "M" + firstHalf + secondHalf + "z");
+    circleFill.setAttribute("d", "M" + firstHalf + secondHalf + "z");
+}
+
+var svgNS = "http://www.w3.org/2000/svg";
+
+function CreateTextLabels()
+{
+    for (var i = 0; i < 32; ++i) {
+        var text = document.createElementNS(svgNS, "text");
+        var pt = AngleToPt(circle.center, circle.radius + 80, i * 360 / 32);
+        text.setAttribute("id", "t" + i);
+        text.setAttribute("x", pt.x);
+        text.setAttribute("y", pt.y);
+        text.setAttribute("text-anchor", "middle");
+        text.setAttribute("alignment-baseline", "mathematical");
+        var textNode = document.createTextNode(i);
+        text.appendChild(textNode);
+        document.getElementById("svg").appendChild(text);
+    }
+}
+
+// CreateTextLabels();
+
+var keyframeArray = [
+    AnimateSpanWedge,
+    AnimateTrivialWedge,
+    AnimateSectorDesc,
+    AnimateLineSingle,
+    AnimateCurveMultiple,
+    AnimateOneDLines,
+    AnimateDiverging,
+];
+
+var keyframeIndex = 3; // keyframeArray.length - 1;  // normally 0 ; set to debug a particular frame
+
+function QueueKeyframe() {
+    QueueAnimation(keyframeArray[keyframeIndex]);
+    if (keyframeIndex < keyframeArray.length - 1) {
+        ++keyframeIndex;
+    }
+}
+
+var grads;
+var paths;
+var canvas;
+var ctx;
+
+function canvasSetup() {
+    canvas = document.getElementById("canvas");
+    ctx = canvas ? canvas.getContext("2d") : null;
+    assert(ctx);
+    var resScale = animationState.resScale = window.devicePixelRatio ? window.devicePixelRatio : 1;
+    var unscaledWidth = canvas.width;
+    var unscaledHeight = canvas.height;
+    canvas.width = unscaledWidth * resScale;
+    canvas.height = unscaledHeight * resScale;
+    canvas.style.width = unscaledWidth + 'px';
+    canvas.style.height = unscaledHeight + 'px';
+    if (resScale != 1) {
+        ctx.scale(resScale, resScale);
+    }
+
+    grads = CanvasGrads(ctx);
+    paths = CanvasPaths(ctx);
+}
+
+function Onload() {
+    canvasSetup();
+    var startBtn = document.getElementById('startBtn');
+    var stopBtn = document.getElementById('stopBtn');
+    var resetBtn = document.getElementById('resetBtn');
+
+    startBtn.addEventListener('click', function(e) {
+        e.preventDefault();
+        e.srcElement.innerText = "Next";
+        CancelAnimation();
+        QueueKeyframe();
+    });
+
+    stopBtn.addEventListener('click', function(e) {
+      e.preventDefault();
+
+      if (!animationState.paused) {
+        PauseAnimation();
+        e.srcElement.innerText = "Resume";
+      } else {
+        UnpauseAnimation();
+        e.srcElement.innerText = "Pause";
+      }
+    });
+
+    resetBtn.addEventListener('click', function(e) {
+        e.preventDefault();
+        CancelAnimation();
+        keyframeIndex = 0;
+        startBtn.innerText = "Start";
+        QueueKeyframe();
+    });
+}
+
+</script>
+
+</head>
+
+<body onLoad="Onload()">
+
+<div class="controls">
+      <button type="button" id="startBtn">Start</button>
+      <button type="button" id="stopBtn">Pause</button>
+      <button type="button" id="resetBtn">Restart</button>
+</div>
+
+<canvas id="canvas" width="800" height="500" />
+
+</body>
+</html>
\ No newline at end of file
diff --git a/experimental/docs/utilities.js b/experimental/docs/utilities.js
new file mode 100644
index 0000000..e3261c6
--- /dev/null
+++ b/experimental/docs/utilities.js
@@ -0,0 +1,24 @@
+function alpha(value, color) {
+    return value << 24 | (color & 0x00FFFFFF);
+}
+
+function argb(a, r, g, b) {
+    return a << 24 | r << 16 | g << 8 | b;
+}
+
+function assert(condition) {
+    if (!condition) debugger;
+}
+
+function isAlpha(code) {
+    return (code > 64 && code < 91) // upper alpha (A-Z)
+        || (code > 96 && code < 123); // lower alpha (a-z)
+}
+
+function isArray(a) {
+    return a.constructor === Array;
+}
+
+function rgb(r, g, b) {
+    return 0xFF << 24 | r << 16 | g << 8 | b;
+}