Total conversion to FF2.
[featherfall2.git] / src / pwl6.js
diff --git a/src/pwl6.js b/src/pwl6.js
deleted file mode 100644 (file)
index dc50206..0000000
+++ /dev/null
@@ -1,2149 +0,0 @@
-"use strict";
-
-var storage;
-var SIGILS;
-var BOOK;
-var handScene;
-var circleScene;
-var NOISY_BLOCKS;
-
-var sounds;
-
-var TOP = 0;
-var LEFT = 1;
-var BOTTOM = 2;
-var RIGHT = 3;
-
-var MenuScene, CircleScene, HandScene, BookScene, GridScene;
-
-yuu.Texture.DEFAULTS.magFilter = yuu.Texture.DEFAULTS.minFilter = "nearest";
-
-var TICK_ROT = quat.rotateZ(quat.create(), quat.create(), Math.PI / 30);
-var TICK_REV = quat.invert(quat.create(), TICK_ROT);
-var TICK_ROT2 = quat.rotateZ(quat.create(), quat.create(), Math.PI / 60);
-
-function sawtooth (p) {
-    /** Sawtooth wave, Û = 1, T = 2π, f(0) = 0, f′(0) > 0 */
-    var _2PI = 2 * Math.PI;
-    return 2 * (p / _2PI - Math.floor(0.5 + p / _2PI));
-}
-
-function triangle (p) {
-    /** Triangle wave, Û = 1, T = 2π, f(0) = 0, f′(0) > 0 */
-    return 2 * Math.abs(sawtooth(p + Math.PI / 2)) - 1;
-}
-
-function waveshift (period, peak, xoffset, yoffset) {
-    period /= 2 * Math.PI;
-    xoffset = xoffset || 0;
-    yoffset = yoffset || 0;
-    return function (f) {
-        return function (p) {
-            return yoffset + peak * f.call(this, (p + xoffset) / period);
-        };
-    };
-}
-
-function cycler (scale) {
-    var f = waveshift(scale, 0.5, -Date.now(), 0.5)(triangle);
-    return function () { return f(Date.now()); };
-}
-
-function load () {
-    storage = ystorage.getStorage();
-    yuu.audio.storage = storage;
-
-    NOISY_BLOCKS = new yuu.ShaderProgram(null, ["@noise.glsl", "@noisyblocks"]);
-
-    SIGILS = new yuu.Material("@sigils");
-    BOOK = new yuu.Material("@book", NOISY_BLOCKS);
-    BOOK.uniforms.cut = yf.volatile(cycler(20000));
-    BOOK.uniforms.range = 0.06;
-    BOOK.texture.ready.then(function (texture) {
-        BOOK.uniforms.resolution = new Float32Array(
-            [texture.width / 4, texture.height / 4]);
-    });
-
-    sounds = {
-        tick: new yuu.Instrument("@tick"),
-        tock: new yuu.Instrument("@tock"),
-        regear: new yuu.Instrument("@regear"),
-        winding: new yuu.Instrument("@winding"),
-        slam: new yuu.Instrument("@slam"),
-        switch: new yuu.Instrument("@switch"),
-        clicking: new yuu.Instrument("@clicking"),
-        bookAppear: new yuu.Instrument("@book-appear"),
-        switchBroke: new yuu.Instrument({
-            sample: { "@switch": { duration: 0.27, offset: 0.1 } } }),
-        switchOn: new yuu.Instrument({
-            sample: { "@switch": { duration: 0.2 } } }),
-        switchOff: new yuu.Instrument({
-            sample: { "@switch": { offset: 0.2 } } }),
-        chime: new yuu.Instrument({
-            envelope: { "0": 1, "0.7": 0.2, "3": 0 },
-            modulator: {
-                envelope: { "0": 1, "0.7": 0.2, "3": 0 },
-                frequency: "x1.5",
-            }
-        }),
-    };
-
-    yuu.director.pushScene(circleScene = new CircleScene());
-    yuu.director.pushScene(handScene = new HandScene());
-    yuu.director.pushScene(new MenuScene());
-    if (!storage.getFlag("instructions")) {
-        yuu.director.entity0.attach(new yuu.Ticker(function () {
-            yuu.director.pushScene(new BookScene());
-        }, 60));
-    }
-
-    return yuu.ready(
-        [SIGILS, BOOK]
-        .concat(yf.map(yf.getter.bind(sounds), Object.keys(sounds)))
-    );
-}
-
-function start () {
-    yuu.director.start();
-}
-
-window.addEventListener("load", function() {
-    yuu.registerInitHook(load);
-    yuu.init({ backgroundColor: [0, 0, 0, 1], antialias: false }).then(start);
-});
-
-var PALETTE = [[ 0.76, 0.13, 0.13 ],
-               [ 0.33, 0.49, 0.71 ],
-               [ 0.45, 0.68, 0.32 ],
-               [ 0.51, 0.32, 0.63 ],
-               [ 0.89, 0.49, 0.11 ],
-               [ 1.00, 1.00, 0.30 ]];
-
-var LEVELS = [
-    { name: "12345654321",
-      randomSlammer: [3, 5],
-      deps: "50040@easy 53535@easy 44404@easy 1302@easy 12321@easy 20124@easy"
-    },
-    
-    { slammer: [1, 1], sets: "tutorial",
-      scramble: { easy: "01", hard: "0122" } },
-    { slammer: [1, 1, 1], deps: "tutorial",
-      scramble: { easy: "11", hard: "1212" } },
-    { slammer: [2, 1], deps: "tutorial", sets: "asymmetric",
-      scramble: { easy: "32", hard: "3321" } },
-    { slammer: [1, 2, 1], deps: "tutorial", sets: "unequal",
-      scramble: { easy: "112", hard: "3210" } },
-    { slammer: [2, 0], deps: "asymmetric", sets: "zero",
-      scramble: { easy: "23", hard: "032" } },
-    { slammer: [2, 0, 2], deps: "zero",
-      scramble: { easy: "11", hard: "2211" } },
-    { slammer: [1, 1, 1, 1], deps: "tutorial" },
-    { slammer: [2, 1, 1], deps: "asymmetric" },
-    { slammer: [1, 2, 1, 2], deps: "asymmetric",
-      scramble: { easy: "012" } },
-    { slammer: [1, 2, 3, 4], deps: "asymmetric", sets: "solid",
-      scramble: { easy: "110" } },
-    { slammer: [5, 0, 0, 4, 0], deps: "unequal zero",
-      scramble: { easy: "112" } },
-    { slammer: [5, 3, 5, 3, 5], deps: "unequal solid",
-      scramble: { easy: "3232" } },
-    { slammer: [4, 4, 4, 0, 4], deps: "solid zero",
-      scramble: { easy: "0321" } },
-    { slammer: [1, 3, 0, 2], deps: "unequal zero" },
-    { slammer: [1, 2, 3, 2, 1], deps: "unequal",
-      scramble: { easy: "3333" } },
-    { slammer: [2, 0, 1, 2, 4], deps: "unequal zero" },
-];
-
-function levelName (level) {
-    return (level.name || level.slammer.join("")).trim();
-}
-
-function wonLevel (level, difficulty) {
-    if (level.sets)
-        storage.setFlag(level.sets);
-    storage.setFlag(levelName(level) + "@" + difficulty);
-}
-
-function hasBeaten (level, difficulty) {
-    return storage.getFlag(levelName(level) + "@" + difficulty);
-}
-
-function scrambleForLevel (rnd, level, difficulty) {
-    var c = difficulty === "easy" ? 0 : 1;
-    if (difficulty === "random")
-        c = rnd.randrange(2, 5);
-    var length = level.slammer.length;
-    return rnd.randrange(length * c, length * (c + 1)) + 2;
-}
-
-function difficultyForLevel (level) {
-    if (level.deps && !level.deps.split(" ").every(storage.getFlag, storage))
-        return null;
-    if (hasBeaten(level, "hard"))
-        return "random";
-    if (hasBeaten(level, "easy"))
-        return "hard";
-    else
-        return "easy";
-}
-
-function levelRandom (level, difficulty) {
-    if (difficulty === "random")
-        return yuu.random;
-    else
-        return new yuu.Random(yuu.createLCG(+level.slammer.join("")));
-}
-
-function generateBoard (rnd, level) {
-    var size = level.length;
-    var board = new Array(size);
-    for (var i = 0; i < size; ++i)
-        board[i] = yf.repeat(i % PALETTE.length + 1, size);
-    if (rnd.randbool())
-        yuu.transpose2d(board);
-    return board;
-}
-
-function generateSlammer (rnd, level) {
-    var s = new Array(level.length);
-    for (var i = 0; i < s.length; ++i)
-        s[i] = yf.repeat(0, level[i]);
-    if (rnd.randbool())
-        s.reverse();
-    return s;
-}
-
-var AnimationQueue = yT(yuu.C, {
-    constructor: function () {
-        this._queue = [];
-    },
-
-    attached: function () {
-        this._queue = [];
-    },
-
-    _runNext: function () {
-        var next = this._queue[0];
-        if (next && this.entity)
-            this.entity.attach(new yuu.Animation(
-                next.timeline, next.params, this._complete.bind(this)));
-    },
-
-    _complete: function () {
-        var next = this._queue.shift();
-        next.resolve();
-        this._runNext();
-    },
-
-    enqueue: function (timeline, params) {
-        return new Promise(function (resolve) {
-            this._queue.push({
-                timeline: timeline,
-                params: params,
-                resolve: resolve
-            });
-            // FIXME: Simply chaining the promise doesn't work here
-            // because the tick between the two handlers is often long
-            // enough to render a frame, and that frame will have some
-            // undesirable intermediate state.
-            if (this._queue.length === 1)
-                this._runNext();
-        }.bind(this));
-    },
-
-    SLOTS: ["animationQueue"]
-});
-
-var SLAMMER_ROTATE = {
-    0: { tween1: { yaw: "yaw" }, duration: 10 }
-};
-
-var ROTATE_ALL = {
-    0: { tweenAll: { yaw: "yaws" }, duration: 10 }
-};
-
-var SLAMMER_BOUNCE = {
-    0: { tween1: { y: 0.5 }, duration: 5, repeat: -1 }
-};
-
-var SLIDE_BLOCKS = {
-    0: { tweenAll: { position: "positions" },
-         duration: 8, easing: "linear" },
-};
-
-var SLAMMER_SLAM = {
-    0:  { tween1: { y: -1.5 }, easing: "linear", duration: 6 },
-    6:  { event: "slideBoardBlocks" },
-    15: { event: "slam",
-          tween1: { y: 0 }, easing: "linear", duration: 8 }
-};
-     
-var GRID_DISMISS = {
-    0: { tween1: { yaw: 2 * Math.PI, x: "x", y: "y", scale: [0.3, 0.3, 1] },
-         duration: 45 }
-};
-
-var GRID_FINISHED = {
-    0: { tween: { arm: { scale: [0, 0, 1], yaw: "armYaw", y: "armY" },
-                  board: { y: "boardY" } },
-         duration: 45 }
-};
-
-function rotateCw (d) { return (--d + 4) % 4; }
-function rotateCcw (d) { return ++d % 4; }
-function opposite (d) { return (d + 2) % 4; }
-
-var FlagSet = yT({
-    /** Manage a set of semaphore-like counting flags. */
-
-    constructor: function () {
-        /** Construct a flag set for the provided flags.
-
-            Flags are initialized to 0 by default.
-        */
-        this._counts = {};
-        for (var i = 0; i < arguments.length; ++i)
-            this._counts[arguments[i]] = 0;
-    },
-
-    increment: function () {
-        /** Increment the provided flags. */
-        for (var i = 0; i < arguments.length; ++i)
-            this._counts[arguments[i]]++;
-    },
-
-    decrement: function () {
-        /** Decrement the provided flags.
-
-            No underflow checks are performed. A flag with a negative
-            value is considered set exactly as a flag with a positive
-            value.
-        */
-        for (var i = 0; i < arguments.length; ++i)
-            this._counts[arguments[i]]--;
-    },
-
-    some: function () {
-        /** Return true if any of the provided flags are set. */
-        return yf.some.call(this._counts, yf.getter, arguments);
-    },
-
-    every: function () {
-        /** Return true if all of the provided flags are set. */
-        return yf.every.call(this._counts, yf.getter, arguments);
-    },
-
-    none: function () {
-        /** Return true if none of the provided flags are set. */
-        return !this.some.apply(this, arguments);
-    },
-
-    incrementer: function () {
-        /** Provide a bound 0-ary function to increment the provided flags.
-
-            Useful for wrapps around context-free callbacks.
-        */
-        var that = this, args = arguments;
-        return function () { that.increment.apply(that, args); };
-    },
-
-    decrementer: function () {
-        /** Provide a bound 0-ary function to decrement the provided flags.
-
-            Useful for wrapps around context-free callbacks.
-        */
-        var that = this, args = arguments;
-        return function () { that.decrement.apply(that, args); };
-    }
-});
-
-var BoardController = yT(yuu.C, {
-    constructor: function (rnd, level, colors) {
-        this.contents = generateBoard(rnd, level.slammer);
-        this.colors = colors;
-    },
-    updateChildren: function () {
-        this.entity.data.quads.forEach(function (q) {
-            q.quad.position = [q.x, q.y];
-            var i = this.contents[q.x][q.y];
-            q.quad.color = this.colors[i];
-            q.quad.texBounds = [i / 6, 0.5, (i + 1) / 6, 1.0];
-        }, this);
-    },
-    isComplete: function() {
-        var x, y;
-        var rows = true, cols = true;
-        for (x = 1; x < this.contents.length && rows; ++x)
-            for (y = 0; y < this.contents[x].length && rows; ++y)
-                rows = this.contents[x - 1][y] === this.contents[x][y];
-        for (x = 0; x < this.contents.length && cols; ++x)
-            for (y = 1; y < this.contents[x].length && cols; ++y)
-                cols = this.contents[x][y - 1] === this.contents[x][y];
-        return rows || cols;
-    },
-
-    shift: [
-        function (x, replacement) {
-            var lost = this.contents[x].pop();
-            this.contents[x].unshift(replacement);
-            return lost;
-        },
-        function (y, replacement) {
-            yuu.transpose2d(this.contents);
-            var lost = this.shift[BOTTOM].call(this, y, replacement);
-            yuu.transpose2d(this.contents);
-            return lost;
-        },
-        function (x, replacement) {
-            var lost = this.contents[x].shift();
-            this.contents[x].push(replacement);
-            return lost;
-        },
-        function (y, replacement) {
-            yuu.transpose2d(this.contents);
-            var lost = this.shift[TOP].call(this, y, replacement);
-            yuu.transpose2d(this.contents);
-            return lost;
-        }
-    ],
-
-    SLOTS: ["controller"]
-});
-
-var SlammerController = yT(yuu.C, {
-    constructor: function (rnd, level, colors) {
-        this.blocks = generateSlammer(rnd, level.slammer);
-        this.orientation = TOP;
-        this.colors = colors;
-        this._undoRecord = [];
-    },
-    isComplete: function() {
-        return yf.none(yf.some.bind(null, null), this.blocks);
-    },
-    updateChildren: function () {
-        this.entity.data.quads.forEach(function (q) {
-            var i = this.blocks[q.x][q.y];
-            q.quad.position = [q.x, q.y];
-            q.quad.color = this.colors[i];
-            q.quad.texBounds = [i / 6, 0.5, (i + 1) / 6, 1.0];
-        }, this);
-    },
-
-    lastUndoRecord: { get: function () {
-        return yf.last(this._undoRecord);
-    } },
-
-    clearUndoRecord: function () {
-        this._undoRecord = [];
-    },
-    
-    slam: function (board) {
-        var undoable = (this.orientation !== this.lastUndoRecord);
-        var length = this.blocks.length;
-        this.orientation = opposite(this.orientation);
-        this.blocks = yf.mapr.call(this, function (a, y) {
-            return yf.map(board.shift[this.orientation].bind(board, y), a)
-                .reverse();
-        }, this.blocks, (this.orientation & 2)
-                        ? yf.range(length)
-                        : yf.range(length - 1, -1, -1));
-        yf.each(function (i) {
-            i.x = length - (i.x + 1);
-        }, this.entity.data.quads);
-        this.updateChildren();
-        board.updateChildren();
-        if (undoable)
-            this._undoRecord.push(this.orientation);
-        else
-            this._undoRecord.pop();
-    },
-
-    SLOTS: ["controller"]
-});
-
-function randSide (rnd, except) {
-    return (rnd || yuu.random).choice(
-        yf.without([TOP, LEFT, BOTTOM, RIGHT], except));
-}
-
-var HANDS_LEFT = {
-    0: { tween: { left: { yaw: -0.3 } }, duration: 3 },
-    3: { tween: { left: { yaw: 0.0 } }, duration: 7 },
-};
-
-var HANDS_RIGHT = {
-    0: { tween: { right: { yaw: -0.3 } }, duration: 3 },
-    3: { tween: { right: { yaw: 0.0 } }, duration: 7 },
-};
-
-var HANDS_UNDO = {
-    0: { tween: { left: { yaw: 0.2 }, right: { yaw: 0.2 } },
-         duration: 3 },
-    3: { tween: { left: { yaw: 0.0 }, right: { yaw: 0.0 } },
-         duration: 7 }
-};
-
-var HANDS_MENU_CHOICE = {
-    0: { tween: { left:  { x: -1.3 },
-                  right: { x: -1.3 } },
-         duration: 15, easing: "ease_in"
-       },
-
-    10: { tween: { left:  { scaleX: 1 },
-                   right: { scaleX: 1 } },
-          duration: 20 },
-
-    20: { set: { leftQuad:  { color: "frontColor" },
-                 rightQuad: { color: "frontColor" } },
-          tween: { left: { x: 0 }, right: { x: 0 } },
-          duration: 15
-        },
-};
-
-var HANDS_RETURN = {
-    0: { tween: { left:  { x: -1.3 },
-                  right: { x: -1.3 } },
-         duration: 20
-       },
-
-    10: { tween: { left:  { scaleX: -1 },
-                   right: { scaleX: -1 } },
-          duration: 20 },
-
-    20: { set: { leftQuad:  { color: "backColor" },
-                 rightQuad: { color: "backColor" } },
-          tween: { left: { x: -1 }, right: { x: -1 } },
-          duration: 10
-        },
-};
-
-var HANDS_SLAM = [
-    // TOP
-    { 0: { tween: { left:  { yaw: -0.2, scaleX: 0.8, y: -0.1 },
-                    right: { yaw: -0.2, scaleX: 0.8, y: -0.1 },
-                  }, duration: 10, repeat: -1 },
-    },
-
-    // LEFT
-    { 0: { tween: { left:  { scaleX: 0.8, x: 0.1 },
-                    right: { scaleX: 0.9 },
-                  }, duration: 10, repeat: -1 },
-    },
-
-    // BOTTOM
-    { 0: { tween: { left:  { yaw: 0.2, scaleX: 0.8 },
-                    right: { yaw: 0.2, scaleX: 0.8 },
-                  }, duration: 10, repeat: -1 },
-    },
-
-    // RIGHT
-    { 0: { tween: { left:  { scaleX: 0.9 },
-                    right: { scaleX: 0.8, x: 0.1 },
-                  }, duration: 10, repeat: -1 },
-    },
-];
-
-var HANDS_ROTATE_CW = {
-    0: { tween: { left: { scaleX: 0.8 } }, duration: 5 },
-    5: { tween: { left: { scaleX: 1.0 } }, duration: 5 },
-};
-
-var HANDS_ROTATE_CCW = {
-    0: { tween: { right: { scaleX: 0.8 } }, duration: 5 },
-    5: { tween: { right: { scaleX: 1 } }, duration: 5 },
-};
-
-var BUTTONS_IN = {
-    0: { tween: { a: { x: 0 }, b: { x: 1.5 } }, duration: 25 }
-};
-
-var BUTTONS_OUT = {
-    0: { tween: { a: { x: -1.5 }, b: { x: 0 } }, duration: 25 }
-};
-
-HandScene = yT(yuu.Scene, {
-    constructor: function () {
-        yuu.Scene.call(this);
-        var hands = new yuu.Material("@hand");
-        this.left = new yuu.E(new yuu.Transform());
-        var l = new yuu.E(
-            new yuu.Transform([-0.5, 0.5, 0]),
-            new yuu.DataC({ command: "left" }),
-            this.leftQuad = new yuu.QuadC(hands));
-        this.left.addChild(l);
-        var r = new yuu.E(
-            new yuu.Transform([-0.5, 0.5, 0]),
-            new yuu.DataC({ command: "right" }),
-            this.rightQuad = new yuu.QuadC(hands));
-        this.right = new yuu.E(new yuu.Transform());
-        this.right.addChild(r);
-        var SIZE_X = yuu.random.gauss(1.2, 0.15) * 0.35;
-        var SIZE_Y = yuu.random.gauss(1.1, 0.05) * 0.51;
-        var hand = yuu.random.randrange(3);
-        this.leftQuad.texBounds = this.rightQuad.texBounds = [
-            hand / 2.99, 0, (hand + 1) / 3.01, 1];
-        this.layer0.resize(-0.75, 0, 1.5, 1.5);
-        var leftWrist = new yuu.E(
-            new yuu.Transform([-0.20, 0, 0], null,
-                              [SIZE_X, SIZE_Y, 1]));
-        var rightWrist = new yuu.E(
-            new yuu.Transform([0.20, 0, 0], null,
-                              [-SIZE_X, SIZE_Y, 1]));
-        leftWrist.addChild(this.left);
-        rightWrist.addChild(this.right);
-        this.addEntities(leftWrist, rightWrist);
-        this.backColor = yuu.hslToRgb(
-            (yuu.random.gauss(0.1, 0.1) + 10) % 1,
-            yuu.random.uniform(0.2, 0.7),
-            yuu.random.uniform(0.2, 0.6),
-            1.0);
-        this.leftQuad.alpha = this.rightQuad.alpha = 0.2;
-        var hsl = yuu.rgbToHsl(this.backColor);
-        hsl[2] = hsl[2].lerp(1, 0.15);
-        hsl[1] = hsl[1].lerp(0, 0.30);
-        hsl[3] = 0.4;
-        this.frontColor = yuu.hslToRgb(hsl);
-        this.leftQuad.color = this.rightQuad.color = this.frontColor;
-        this.ready = hands.ready;
-
-        function Button (i, command) {
-            return new yuu.E(
-                new yuu.Transform(),
-                new yuu.DataC({ command: command }),
-                new yuu.QuadC(SIGILS)
-                    .setTexBounds([i / 6, 0, (i + 1) / 6, 0.5])
-                    .setColor(PALETTE[i]));
-        }
-
-        this.helpButton = new Button(1, "help");
-        this.backButton = new Button(3, "back");
-        this.backButton.transform.x -= 1.5;
-        this.leftButtons = new yuu.E(new yuu.Transform());
-        this.leftButtons.addChildren(this.helpButton, this.backButton);
-        this.rightButton = new Button(2, "showOverlay preferences");
-        this.leftButtons.transform.scale
-            = this.rightButton.transform.scale
-            = [0.075, 0.075, 1];
-        this.entity0.addChildren(this.leftButtons, this.rightButton);
-        this.buttons = [this.helpButton, this.backButton, this.rightButton,
-                        l, r];
-     },
-
-    inputs: {
-        resize: function () {
-            var base = new yuu.AABB(-0.75, 0, 0.75, 1.5);
-            var vp = base.matchAspectRatio(yuu.viewport);
-            vp.y1 -= vp.y0;
-            vp.y0 = 0;
-            this.leftButtons.transform.xy = [
-                vp.x0 + this.leftButtons.transform.scaleX,
-                vp.y1 - this.leftButtons.transform.scaleY];
-            this.rightButton.transform.xy = [
-                vp.x1 - this.rightButton.transform.scaleX,
-                vp.y1 - this.rightButton.transform.scaleY];
-            this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
-        },
-
-        mousemove: function (p) {
-            p = this.layer0.worldFromDevice(p);
-            this.cursor = "";
-            for (var i = 0; i < this.buttons.length; ++i) {
-                if (this.buttons[i].transform.contains(p)) {
-                    this.cursor = "pointer";
-                }
-            }
-        },
-
-        tap: function (p) {
-            p = this.layer0.worldFromDevice(p);
-            for (var i = 0; i < this.buttons.length; ++i) {
-                if (this.buttons[i].transform.contains(p)) {
-                    yuu.director.execute(this.buttons[i].data.command);
-                    return true;
-                }
-            }
-        },
-
-        doubletap: function () {
-            return this.inputs.tap.apply(this, arguments);
-        },
-    },
-
-    _anim: function (timeline) {
-        this.entity0.attach(new yuu.Animation(
-            timeline, {
-                left: this.left.transform,
-                right: this.right.transform,
-                leftQuad: this.leftQuad,
-                rightQuad: this.rightQuad,
-                frontColor: this.frontColor,
-                backColor: this.backColor
-            }));
-    },
-
-    undo: function () { this._anim(HANDS_UNDO); },
-    movedLeft: function () { this._anim(HANDS_LEFT); },
-    movedRight: function () { this._anim(HANDS_RIGHT); },
-    slam: function (o) { this._anim(HANDS_SLAM[o]); },
-    rotatedCw: function () { this._anim(HANDS_ROTATE_CW); },
-    rotatedCcw: function () { this._anim(HANDS_ROTATE_CCW); },
-    menuChoice: function () {
-        this.entity0.attach(new yuu.Animation(
-            BUTTONS_IN, {
-                a: this.backButton.transform,
-                b: this.helpButton.transform
-            }));
-        this._anim(HANDS_MENU_CHOICE);
-    },
-    finished: function () {
-        this.entity0.attach(new yuu.Animation(
-            BUTTONS_OUT, {
-                a: this.backButton.transform,
-                b: this.helpButton.transform
-            }));
-        this._anim(HANDS_RETURN);
-    },
-});
-
-var GRID_APPEAR = {
-    0: { set1:   { y: 5 },
-         tween1: { y: 0 }, duration: 10 },
-};
-
-GridScene = yT(yuu.Scene, {
-    constructor: function (level, difficulty) {
-        yuu.Scene.call(this);
-        this.entity0.attach(new yuu.Transform());
-        this.level = level;
-        this.difficulty = difficulty;
-        this._locks = new FlagSet("slam", "spin", "quit");
-        var rnd = levelRandom(level, difficulty);
-        var colors = yuu.random.shuffle(PALETTE.slice());
-        colors.unshift([1.0, 1.0, 1.0]);
-        this.board = new yuu.E(new BoardController(rnd, level, colors),
-                               new yuu.Transform(),
-                               new yuu.DataC({ quads: [] }));
-        this.slammer = new yuu.E(new SlammerController(rnd, level, colors),
-                                 new yuu.Transform(),
-                                 new yuu.DataC({ quads: [] }));
-        this.slammerHead = new yuu.E(new yuu.Transform());
-        this.slammerRoot = new yuu.E(new yuu.Transform());
-        var length = level.slammer.length;
-        var maxSize = length * length;
-        var slammerBatch = new yuu.QuadBatchC(maxSize);
-        slammerBatch.material = SIGILS;
-        var boardBatch = new yuu.QuadBatchC(maxSize);
-        boardBatch.material = SIGILS;
-        this.slammerRoot.transform.xy = [length / 2 - 0.5, length / 2 - 0.5];
-        this.slammerHead.transform.xy = [-length / 2 + 0.5, length / 2 + 2];
-        this.slammerRoot.addChild(this.slammerHead);
-        this.slammerHead.addChild(this.slammer);
-        this.slammer.attach(slammerBatch);
-        this.board.attach(boardBatch);
-        yf.irange.call(this, function (x) {
-            yf.irange.call(this, function (y) {
-                var quad = boardBatch.createQuad();
-                quad.color = colors[this.board.controller.contents[x][y]];
-                quad.position = [x, y];
-                this.board.data.quads.push({ quad: quad, x: x, y: y });
-            }, length);
-        }, length);
-
-        for (var x = 0; x < this.slammer.controller.blocks.length; ++x) {
-            for (var y = 0; y < this.slammer.controller.blocks[x].length; ++y) {
-                var quad = slammerBatch.createQuad();
-                quad.color = colors[this.slammer.controller.blocks[x][y]];
-                quad.position = [x, y];
-                this.slammer.data.quads.push({ quad: quad, x: x, y: y });
-            }
-        }
-        this.addEntities(this.board, this.slammerRoot);
-        this.scramble(rnd);
-        if (!(this.cheating = yuu.director.input.pressed["`"])) {
-            this.slammer.controller.clearUndoRecord();
-        }
-        this._dragging = 0;
-
-        this.gridBB = new yuu.AABB(-0.5, -0.5, length - 0.5, length - 0.5);
-        this.leftBB = new yuu.AABB(
-            -Infinity, this.gridBB.y0, this.gridBB.x0, this.gridBB.y1);
-        this.rightBB = new yuu.AABB(
-            this.gridBB.x1, this.gridBB.y0, Infinity, this.gridBB.y1);
-        this.topBB = new yuu.AABB(
-            this.gridBB.x0, this.gridBB.y1, this.gridBB.x1, Infinity);
-        this.bottomBB = new yuu.AABB(
-            this.gridBB.x0, -Infinity, this.gridBB.x1, this.gridBB.y0);
-    },
-
-    init: function () {
-        this._locks.increment("slam");
-        this.entity0.attach(new yuu.Animation(
-            GRID_APPEAR, { $: this.slammer.transform },
-            this._locks.decrementer("slam")));
-    },
-
-    scramble: function (rnd) {
-        var scramble = (this.level.scramble || {})[this.difficulty];
-        var slammerCon = this.slammer.controller;
-        var boardCon = this.board.controller;
-        if (!scramble) {
-            var count = scrambleForLevel(rnd, this.level, this.difficulty);
-            while (this.isComplete()) {
-                var c = count;
-                while (c--) {
-                    slammerCon.orientation = randSide(rnd, slammerCon.orientation);
-                    slammerCon.slam(boardCon);
-                }
-            }
-        } else {
-            for (var i =0; i < scramble.length; ++i) {
-                slammerCon.orientation = +scramble[i];
-                slammerCon.slam(boardCon);
-            }
-        }
-        slammerCon.orientation = randSide();
-        this.slammerRoot.transform.yaw = slammerCon.orientation * Math.PI / 2;
-    },
-
-    isComplete: function () {
-        return this.slammer.controller.isComplete()
-            && this.board.controller.isComplete();
-    },
-
-    rotateTo: yuu.cmd(function (orientation) {
-        return new Promise(function (resolve) {
-            if (this._locks.some("spin"))
-                return this;
-            this.slammer.controller.orientation = orientation;
-            this._locks.increment("slam");
-            var yaw0 = this.slammerRoot.transform.yaw;
-            var yaw1 = orientation * Math.PI / 2;
-
-            sounds.clicking.play();
-            this.entity0.attach(new yuu.Animation(
-                SLAMMER_ROTATE, {
-                    $: this.slammerRoot.transform,
-                    yaw: yaw0 + yuu.normalizeRadians(yaw1 - yaw0)
-                }, function () {
-                    this._locks.decrement("slam");
-                    resolve(this);
-                }.bind(this)));
-        }.bind(this));
-    }, "<top/bottom/left/right>", "move the slammer to the top"),
-
-    rotateCw: yuu.cmd(function () {
-        handScene.rotatedCw();
-        circleScene.rotated();
-        this.rotateTo(rotateCw(this.slammer.controller.orientation));
-    }, "", "rotate the active piece clockwise"),
-
-    rotateCcw: yuu.cmd(function () {
-        handScene.rotatedCcw();
-        circleScene.rotated();
-        this.rotateTo(rotateCcw(this.slammer.controller.orientation));
-    }, "", "rotate the active piece counter-clockwise"),
-
-    left: yuu.cmd(function () { this.rotateCw(); }),
-    right: yuu.cmd(function () { this.rotateCcw(); }),
-
-    undo: yuu.cmd(function (v) {
-        var con = this.slammer.controller;
-        var _ = function () { this.undo(this._undo); }.bind(this);
-        if ((this._undo = v) && con.lastUndoRecord !== undefined) {
-            if (con.orientation !== con.lastUndoRecord) {
-                circleScene.reverse();
-                handScene.undo();
-                this.rotateTo(con.lastUndoRecord)
-                    .then(this.slam.bind(this))
-                    .then(_);
-            } else {
-                this.slam().then(_);
-            }
-        }
-    }, "", "rotate the active piece counter-clockwise"),
-
-    checkWon: function () {
-        if (this.isComplete() && !this._locks.some("quit")) {
-            this._locks.increment("quit", "slam", "spin");
-            var firstTime = !hasBeaten(this.level, this.difficulty);
-            if (!this.cheating)
-                wonLevel(this.level, this.difficulty);
-            var scene = new MenuScene(this.level);
-            yuu.director.pushScene(scene);
-            scene.didWinLevel(this.level, this.difficulty, firstTime);
-            this.entity0.attach(new yuu.Animation(
-                GRID_FINISHED, {
-                    arm: this.slammerRoot.transform,
-                    armYaw: this.slammerRoot.transform.yaw + 3 * Math.PI,
-                    armY: this.slammerRoot.transform.y + 1.5,
-                    board: this.board.transform,
-                    boardY: this.level.slammer.length * 3
-                }, function () {
-                    yuu.director.removeScene(this);
-                }.bind(this)
-            ));
-        }
-    },
-
-    slideBoardBlocks: function (anim, params) {
-        var dx = 0, dy = 0;
-        var orientation = this.slammer.controller.orientation;
-        switch (orientation) {
-        case LEFT: dx = 1.5; break;
-        case TOP: dy = -1.5; break;
-        case RIGHT: dx = -1.5; break;
-        case BOTTOM: dy = 1.5; break;
-        }
-        var sgnx = Math.sign(dx);
-        var sgny = Math.sign(dy);
-        var $s = [];
-        var positions = [];
-        var blocks = this.slammer.controller.blocks;
-        this.slammer.data.quads.forEach(function (q) {
-            var d = blocks[q.x].length;
-            $s.push(q.quad);
-            positions.push([q.quad.position[0], q.quad.position[1] - d]);
-        }, this);
-        this.board.data.quads.forEach(function (q) {
-            var x = orientation === TOP ? q.x : blocks.length - (q.x + 1);
-            var y = orientation === LEFT ? q.y : blocks.length - (q.y + 1);
-
-            $s.push(q.quad);
-            positions.push([q.quad.position[0] + sgnx * blocks[y].length,
-                            q.quad.position[1] + sgny * blocks[x].length]);
-        }, this);
-        this.entity0.attach(new yuu.Animation(SLIDE_BLOCKS, {
-            $s: $s,
-            positions: positions
-        }));
-    },
-
-    slam: yuu.cmd(function () {
-        var r = new Promise(function (resolve, reject) {
-            if (this._locks.some("slam")) {
-                reject("slamming is locked");
-                return;
-            }
-            this._locks.increment("spin", "slam");
-            circleScene.slam();
-            sounds.slam.play();
-            handScene.slam(this.slammer.controller.orientation);
-            this.entity0.attach(new yuu.Animation(
-                SLAMMER_SLAM, {
-                    $: this.slammer.transform,
-                    slam: function () {
-                        this._locks.decrement("spin");
-                        this.slammer.controller.slam(this.board.controller);
-                        this.slammerRoot.transform.yaw = Math.PI / 2 *
-                            this.slammer.controller.orientation;
-                    }.bind(this),
-                    slideBoardBlocks: this.slideBoardBlocks.bind(this)
-                }, function () {
-                    this.checkWon();
-                    this._locks.decrement("slam");
-                    resolve(this);
-                }.bind(this)));
-        }.bind(this));
-        return r;
-    }, "", "slam the active piece"),
-
-    back: yuu.cmd(function (x, y) {
-        if (this._locks.some("quit"))
-            return;
-        this._locks.increment("quit", "slam", "spin");
-        var scene = new MenuScene(this.level);
-        yuu.director.pushScene(scene);
-        var v = [x || yuu.random.uniform(-1, 1),
-                 y || yuu.random.uniform(-1, 1)];
-        var size = this.board.controller.contents.length * 5;
-        vec2.scale(v, vec2.normalize(v, v), size);
-        this.entity0.attach(new yuu.Animation(
-            GRID_DISMISS, {
-                $: this.entity0.transform,
-                x: v[0], y: v[1]
-            }, function () {
-                yuu.director.removeScene(this);
-            }.bind(this)
-        ));
-        circleScene.lose();
-    }, "", "go back to the menu"),
-
-    slammerBB: { get: function (p) {
-        var length = this.level.slammer.length;
-        switch (this.slammer.controller.orientation) {
-        case LEFT:
-            return new yuu.AABB(-Infinity, -0.5, -1, length - 0.5);
-        case RIGHT:
-            return new yuu.AABB(length + 1, -0.5, Infinity, length - 0.5);
-        case TOP:
-            return new yuu.AABB(-0.5, length + 1, length - 0.5, Infinity);
-        case BOTTOM:
-            return new yuu.AABB(-0.5, -Infinity, length - 0.5, -1);
-        }
-    } },
-
-    _swipe: function (p0, p1) {
-        p0 = this.layer0.worldFromDevice(p0);
-        p1 = this.layer0.worldFromDevice(p1);
-        if (this.slammerBB.contains(p0)) {
-            this.slam();
-            return true;
-        }
-        if (this.gridBB.contains(p0) && !this.gridBB.contains(p1)) {
-            this.back(p1.x - p0.x, p1.y - p0.y);
-            return true;
-        }
-    },
-
-    inputs: {
-        resize: function () {
-            var length = this.level.slammer.length;
-            var base = new yuu.AABB(-length - 2.5, -length - 2.5,
-                                    2 * length + 1.5, 2 * length + 1.5);
-            var vp = base.matchAspectRatio(yuu.viewport);
-            this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
-        },
-
-        tap: function (p) {
-            p = this.layer0.worldFromDevice(p);
-            if (this.gridBB.contains(p)) {
-                this.slam();
-                return true;
-            }
-        },
-
-        touch: function (p) {
-            var length = this.level.slammer.length;
-            var middle = (length - 1) / 2;
-            p = this.layer0.worldFromDevice(p);
-            if (this.slammerBB.contains(p)) {
-                this.slammer.attach(new yuu.Animation(
-                    SLAMMER_BOUNCE, { $: this.slammer.transform }));
-            } else if (this.leftBB.contains(p)) {
-                this.rotateTo(LEFT);
-                handScene.rotatedCw();
-                return true;
-            } else if (this.rightBB.contains(p)) {
-                this.rotateTo(RIGHT);
-                handScene.rotatedCcw();
-                return true;
-            } else if (this.topBB.contains(p)) {
-                this.rotateTo(TOP);
-                if (p.x < middle)
-                    handScene.rotatedCw();
-                else
-                    handScene.rotatedCcw();
-                return true;
-            } else if (this.bottomBB.contains(p)) {
-                this.rotateTo(BOTTOM);
-                if (p.x < middle)
-                    handScene.rotatedCw();
-                else
-                    handScene.rotatedCcw();
-                return true;
-            }
-        },
-
-        doubletap: function () {
-            return this.inputs.tap.apply(this, arguments);
-        },
-
-        hold: function (p) {
-            p = this.layer0.worldFromDevice(p);
-            if (this.gridBB.contains(p)) {
-                this.undo(true);
-                return true;
-            }
-        },
-
-        dragstart: function (p) {
-            p = this.layer0.worldFromDevice(p);
-            this._dragging = this.slammerBB.contains(p);
-        },
-
-        drag: function (p0, p1) {
-            var p = this.layer0.worldFromDevice(p1);
-            if (this._dragging && !this._locks.some("slam")) {
-                var inGrid = this.gridBB.contains(p);
-                var length = this.level.slammer.length;
-                var o;
-                if (this._dragging === true && inGrid) {
-                    this.slam();
-                } else if (p.x > 0 && p.x < length && !inGrid) {
-                    o = p.y < 0 ? BOTTOM : TOP;
-                    if (o !== this.slammer.controller.orientation) {
-                        this.rotateTo(o);
-                        this._dragging = 2;
-                    }
-                } else if (p.y > 0 && p.y < length && !inGrid) {
-                    o = p.x < 0 ? LEFT : RIGHT;
-                    if (o !== this.slammer.controller.orientation) {
-                        this.rotateTo(o);
-                        this._dragging = 2;
-                    }
-                }
-            }
-            return this._dragging;
-        },
-
-        dragend: function (p0, p1) {
-            this._dragging = false;
-        },
-
-        release: function () {
-            this.undo(false);
-        },
-
-        swipeleft: function (p0, p1) {
-            return this._swipe(p0, p1);
-        },
-        swiperight: function (p0, p1) {
-            return this._swipe(p0, p1);
-        },
-        swipeup: function (p0, p1) {
-            return this._swipe(p0, p1);
-        },
-        swipedown: function (p0, p1) {
-            return this._swipe(p0, p1);
-        },
-    },
-
-    KEYBINDS: {
-        w: "rotateCw",
-        a: "rotateCcw",
-        s: "rotateCcw",
-        d: "rotateCw",
-        left: "rotateCcw",
-        right: "rotateCw",
-        up: "rotateCw",
-        down: "rotateCcw",
-        shift: "slam",
-        space: "slam",
-        z: "slam",
-        backspace: "+undo",
-        c: "+undo",
-        escape: "back",
-        back: "back",
-        gamepadbutton0: "slam",
-        gamepadbutton1: "+undo",
-        gamepadbutton2: "slam",
-        gamepadbutton3: "+undo",
-        gamepadbutton4: "rotateCcw",
-        gamepadbutton5: "rotateCw",
-        gamepadbutton8: "back",
-        gamepadbutton14: "rotateCcw",
-        gamepadbutton15: "rotateCw",
-    }
-});
-
-var MENU_APPEAR = {
-    0: [{ set1:   { x: 5, y: 5, scaleX: 0, scaleY: 0 } },
-        { tween1: { x: 0, y: 0, scaleX: 1 }, duration: 24 },
-        { tween1: { scaleY: 1 },
-          duration: 55, easing: yuu.Tween.METASPRING(1, 10)}],
-};
-
-var MENU_SLIDE = {
-    0: { tween1: { x: "x" }, duration: "duration" }
-};
-
-var FLASH = {
-    0:  { tween1: { luminance: 1, alpha: 1 }, duration: 32, repeat: -1 }
-};
-
-var MENU_SLAM = {
-    0:  { tween: { cursor: { y: "mid" } }, duration: 8 },
-    8:  { tween: { cursor: { y: "line" },
-                   select: { y: -1.0 }
-                 }, duration: 12 },
-    20: { tween: { scene: { y: 10 },
-                   select: { y: -11.5, scale: [3, 3, 1] }
-                 }, duration: 18 },
-    38: { event: "appear",
-          tween: { select: { y: 0, scale: [1, 0, 1] } },
-          duration: 20 }
-};
-
-function menuEntityForLevel (level, i) {
-    var activated = false;
-    function randomizeSlammer () {
-        var min = level.randomSlammer[0];
-        var max = level.randomSlammer[1];
-        var size = yuu.random.randrange(min, max + 1);
-        level.slammer = [];
-        do {
-            for (var i = 0; i < size; ++i)
-                level.slammer[i] = yuu.random.randrange(0, size);
-        } while (Math.min.apply(Math, level.slammer) === max
-                || Math.max.apply(Math, level.slammer) === 0);
-    }
-
-    function generateQuads() {
-        batch.disposeAll();
-        var rgb = PALETTE[i % PALETTE.length];
-        var fit = level.slammer.length + 1;
-        batch.createQuad().color = rgb;
-        level.slammer.forEach(function (size, y) {
-            var c = batch.createQuad();
-            c.color = [0, 0, 0];
-            c.alpha = hasBeaten(level, "easy") ? 0.5 : 1.0;
-            c.size = [size / fit, 1 / fit];
-            c.position = [0, -0.5 + (y + 1) / fit];
-        });
-        ce.data.flasher = batch.createQuad();
-        ce.data.flasher.alpha = 0;
-        return ce;
-    }
-
-    if (level.randomSlammer)
-        randomizeSlammer();
-
-    // 14 = maximum slammer size + 1 background + 1 flasher
-    var batch = new yuu.QuadBatchC(14);
-    var ce = new yuu.E(
-        new yuu.Transform([2 * i, 0, 0]),
-        batch,
-        new yuu.DataC({
-            activate: function () {
-                activated = true;
-                var scene = new GridScene(level, difficultyForLevel(level));
-                yuu.director.insertUnderScene(scene);
-            }
-        })
-    );
-
-    if (level.randomSlammer) {
-        ce.attach(new yuu.Ticker(function () {
-            if (!activated && yuu.random.randbool(0.7)) {
-                randomizeSlammer();
-                ++i;
-                generateQuads();
-            }
-            return !activated;
-        }, 30, 15));
-    }
-
-    generateQuads();
-    return ce;
-}
-
-var HAND_TICK_BACK = {
-    0: { tween1: { rotation: "rotation" }, duration: 6, repeat: -1 }
-};
-
-MenuScene = yT(yuu.Scene, {
-    constructor: function (initialLevel) {
-        yuu.Scene.call(this);
-        this.entity0.attach(new yuu.Transform(),
-                            new AnimationQueue());
-
-        this.pointer = new yuu.E(
-            new yuu.Transform([5, 8, 0]),
-            new yuu.QuadC());
-
-        var menu = this.menu = new yuu.E(new yuu.Transform([5, 6.5, 0]));
-        this.addEntities(menu, this.pointer);
-        this.availableLevels = LEVELS.filter(difficultyForLevel);
-        this.availableLevels
-            .map(menuEntityForLevel)
-            .forEach(menu.addChild, menu);
-
-        var initialIdx = this.availableLevels.indexOf(initialLevel);
-        this._locks = new FlagSet("slam", "move");
-        this.activeIndex = Math.max(initialIdx, 0);
-        menu.transform.x = 5 - 2 * this.activeIndex;
-        this.changeActiveIndex(this.activeIndex, false);
-        this._dragStartX = null;
-
-        this.entity0.attach(
-            new yuu.Ticker(this._animation.bind(this), 60));
-    },
-
-    _animation: function (count) {
-        var length = this.availableLevels.length;
-        var range = Math.pow(2, length);
-        var rand = yuu.random.randrange(range);
-        var targets = [];
-        var yaws = [];
-        for (var i = 0; i < length; ++i) {
-            var child = this.menu.children[i];
-            var level = this.availableLevels[i];
-            var won = hasBeaten(level, "hard");
-            if ((won || ((count ^ i) & 1)) && ((count ^ rand) & (1 << i))) {
-                var dyaw = won
-                    ? yuu.random.randsign(Math.PI / 2)
-                    : -Math.PI / 2;
-                targets.push(child.transform);
-                yaws.push(child.transform.yaw + dyaw);
-            }
-        }
-        if (targets.length) {
-            this.entity0.attach(new yuu.Animation(
-                ROTATE_ALL, { $s: targets, yaws: yaws }));
-        }
-        circleScene.clockTick(TICK_ROT2, HAND_TICK_BACK);
-        sounds[["tick", "tock"][count & 1]]
-            .createSound(yuu.audio, yuu.audio.currentTime, 0, 0.2, 1.0)
-            .connect(yuu.audio.music);
-    
-        return true;
-    },
-
-    init: function () {
-        circleScene.toBottom();
-        handScene.finished();
-        this._locks.increment("slam", "move");
-        this.entity0.animationQueue.enqueue(
-            MENU_APPEAR,
-            { $: this.entity0.transform })
-            .then(this._locks.decrementer("slam", "move"));
-    },
-
-    didWinLevel: function (level, difficulty, firstTime) {
-        var idx = this.availableLevels.indexOf(level);
-        circleScene.win();
-        if (firstTime)
-            this.entity0.animationQueue.enqueue(
-                FLASH, { $: this.menu.children[idx].data.flasher });
-        for (var i = idx; i < this.availableLevels.length; ++i) {
-            if (!hasBeaten(this.availableLevels[i], difficulty)) {
-                this._locks.increment("move");
-                this.changeActiveIndex(i, true)
-                    .then(this._locks.decrementer("move"));
-                
-                break;
-            }
-        }
-    },
-
-    changeActiveIndex: function (index, animate) {
-        var oldIndex = this.activeIndex;
-        var p;
-        this.activeIndex = index = yf.clamp(
-            index, 0, this.menu.children.length - 1);
-        if (index !== oldIndex && animate) {
-            this._locks.increment("slam");
-            var duration = Math.ceil(8 * Math.abs(oldIndex - index));
-            p = this.entity0.animationQueue.enqueue(
-                MENU_SLIDE, {
-                    $: this.menu.transform,
-                    x: 5 - 2 * index,
-                    duration: duration
-                });
-            p.then(this._locks.decrementer("slam"));
-        }
-        return p || Promise.resolve();
-    },
-    
-    left: yuu.cmd(function () {
-        if (!this._locks.some("move")) {
-            sounds[this.activeIndex === 0 ? "switchBroke" : "switch"].play();
-            handScene.movedLeft();
-            this.changeActiveIndex(this.activeIndex - 1, true);
-        }
-    }, "move the cursor left"),
-    right: yuu.cmd(function () {
-        if (!this._locks.some("move")) {
-            sounds[this.activeIndex === this.availableLevels.length - 1
-                   ? "switchBroke" : "switch"].play();
-            handScene.movedRight();
-            this.changeActiveIndex(this.activeIndex + 1, true);
-        }
-    }, "move the cursor right"),
-
-    slam: yuu.cmd(function () {
-        if (this._locks.some("slam"))
-            return;
-        var activeChild = this.menu.children[this.activeIndex];
-        this._locks.increment("slam", "move");
-        handScene.menuChoice();
-        circleScene.toBack();
-        circleScene.slam();
-        sounds.winding.play();
-        this.entity0.animationQueue.enqueue(
-            MENU_SLAM, {
-                cursor: this.pointer.transform,
-                select: activeChild.transform,
-                scene: this.entity0.transform,
-                mid: this.pointer.transform.y - 0.5,
-                line: this.pointer.transform.y - 1.5,
-                appear: activeChild.data.activate
-            }).then(function () {
-                this._locks.decrementer("slam", "move");
-                yuu.director.removeScene(this);
-            }.bind(this));
-    }, "choose the active menu item"),
-
-    inputs: {
-        resize: function () {
-            var base = new yuu.AABB(0, 0, 10, 10);
-            var vp = base.matchAspectRatio(yuu.viewport);
-            this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
-        },
-
-        pinchout: function (p0, p1) {
-            p0 = this.layer0.worldFromDevice(p0);
-            p1 = this.layer0.worldFromDevice(p1);
-            if (vec2.sqrDist(p0, p1) > 1) {
-                this.slam();
-                return true;
-            }
-        },
-
-        hold: function (p) {
-            return this.inputs.dragstart.call(this, p);
-        },
-
-        release: function (p) {
-            if (this._dragStartX !== null)
-                return this.inputs.dragend.call(this, p);
-        },
-
-        dragstart: function (p) {
-            if (this._locks.some("move"))
-                return false;
-            p = this.layer0.worldFromDevice(p);
-            if (p.y > 6 && p.y < 8.5 && p.inside && this._dragStartX === null) {
-                sounds.switchOn.play();
-                this._locks.increment("move");
-                this._dragStartX = this.menu.transform.x;
-                return true;
-            }
-        },
-
-        dragdown: function (p0, p1) {
-            p0 = this.layer0.worldFromDevice(p0);
-            p1 = this.layer0.worldFromDevice(p1);
-
-            if (p0.x >= 4.5 && p0.x <= 5.5
-                && p0.y >= 6.0 && p0.y <= 8.5
-                && p0.y - p1.y > 1) {
-                this.slam();
-                return true;
-            }
-        },
-
-        drag: function (p0, p1) {
-            if (this._dragStartX !== null) {
-                p0 = this.layer0.worldFromDevice(p0);
-                p1 = this.layer0.worldFromDevice(p1);
-                this.menu.transform.x = this._dragStartX + (p1.x - p0.x);
-                var index = Math.round((5 - this.menu.transform.x) / 2);
-                this.changeActiveIndex(index);
-                return true;
-            }
-        },
-
-        dragend: function (p0, p1) {
-            if (this._dragStartX !== null) {
-                sounds.switchOff.play();
-                this._locks.decrement("move");
-                this._dragStartX = null;
-                var index = this.activeIndex;
-                this.activeIndex = (5 - this.menu.transform.x) / 2;
-                this.changeActiveIndex(index, true);
-                return true;
-            }
-        },
-
-        tap: function (p) {
-            p = this.layer0.worldFromDevice(p);
-            if (p.y > 6 && p.y < 7 && p.inside) {
-                var dx = Math.round((p.x - 5) / 2);
-                if (dx === 0) this.slam();
-                else if (dx < 0) handScene.movedLeft();
-                else if (dx > 0) handScene.movedRight();
-                var idx = this.activeIndex;
-                this.changeActiveIndex(this.activeIndex + dx, true);
-                if (idx !== this.activeIndex)
-                    sounds.switch.play();
-                else
-                    sounds.switchBroke.play();
-                return true;
-            }
-            
-        },
-
-        doubletap: function (p) {
-            p = this.layer0.worldFromDevice(p);
-            if (p.x >= 4.5 && p.x <= 5.5 && p.y >= 6.0 && p.y <= 8.5) {
-                this.slam();
-                return true;
-            }
-        },
-    },
-
-    resetEverything: yuu.cmd(function () {
-        storage.clear();
-        yuu.director.stop();
-        start();
-    }, "reset all saved data"),
-
-    unlock: yuu.cmd(function (d) {
-        LEVELS.forEach(function (level) { wonLevel(level, d); });
-        yuu.director.pushPopScene(new MenuScene());
-    }, "<difficulty>", "unlock all levels to the given difficulty"),
-
-    KEYBINDS: {
-        left: "left",
-        right: "right",
-        up: "right",
-        down: "left",
-        w: "right",
-        a: "left",
-        s: "left",
-        d: "right",
-        shift: "slam",
-        space: "slam",
-        z: "slam",
-        "`+r+e": "resetEverything",
-        "`+u+e": "unlock easy",
-        "`+u+h": "unlock hard",
-        gamepadbutton0: "slam",
-        gamepadbutton8: "help",
-        gamepadbutton9: "slam",
-        gamepadbutton13: "slam",
-        gamepadbutton14: "left",
-        gamepadbutton15: "right",
-    }
-});
-
-
-var BOOK_APPEAR = {
-    0: { set1:   { y: 1.5, x: -1.5 },
-         tween: { bgQuad: { alpha: 0.75 }, $: { y: 0, x: 0 }, },
-         duration: 30 }
-};
-
-var BOOK_DISMISS = {
-    0: { tween: { bgQuad: { alpha: 0 }, $: { y: 1.5, x: -1.5, } },
-         duration: 30 }
-};
-
-var KEYBOARD_PAGE = [0.25, 0.50, 0.50, 1.00];
-var POINTERS_PAGE = [0.25, 0.00, 0.50, 0.50];
-var GAMEPAD_PAGE = [0.00, 0.00, 0.25, 0.50];
-
-var BOOK_FORWARD = [
-    { 0:  { set:   { page2Quad: { color: [0.2, 0.2, 0.2, 1], texBounds: "page" } },
-            tween: { page1: { x: -1/3 / 2, scaleX: 0 },
-                     page2: { x: +1/3 / 2 },
-                     page2Quad: { color: [1, 1, 1, 1] },
-                   }, duration: 15, easing: "linear" },
-      15: { set: { page1Quad: { z: 0, texBounds: [0.25, 0.5, 0.00, 1] },
-                   page2Quad: { z: 0, texBounds: "page" } },
-            tween: { page1: { x: -1/3, scaleX: -2/3 },
-                     page2: { x: +1/3 }
-                   }, duration: 15, easing: "linear" },
-    },
-
-    { 0:  { tween: { page1: { x: -1/3 / 2 },
-                     page2: { x: +1/3 / 2, scaleX: 0 }
-                   }, duration: 15, easing: "linear" },
-      15: { set:   { page1Quad: { z: 0, texBounds: [0.25, 0.5, 0.00, 1] },
-                     page2Quad: { z: 1, texBounds: [1.00, 0.5, 0.75, 1] } },
-            tween: { page1Quad: { color: [0.2, 0.2, 0.2, 1] },
-                     page1: { x: 0 },
-                     page2: { x: 0, scaleX: -2/3 },
-                   }, duration: 15, easing: "linear" },
-    },
-
-    BOOK_DISMISS
-];
-
-var BOOK_BACKWARD = [
-    { 0:   { tween: { page1: { x: -1/3 / 2, scaleX: 0 },
-                      page2: { x: +1/3 / 2 },
-                    }, duration: 15, easing: "linear" },
-      15: { set:   { page1Quad: { z: 1, texBounds: [0.50, 0.5, 0.75, 1] },
-                     page2Quad: { z: 0 } },
-            tween: { page2Quad: { color: [0.2, 0.2, 0.2, 1] },
-                     page1: { x: 0, scaleX: 2/3 },
-                     page2: { x: 0 },
-                   }, duration: 15, easing: "linear" },
-    },
-
-    { 0:  { set:   { page1Quad: { color: [0.2, 0.2, 0.2, 1] } },
-            tween: { page1Quad: { color: [1.0, 1.0, 1.0, 1] },
-                     page1: { x: -1/3 / 2 },
-                     page2: { x: +1/3 / 2, scaleX: 0 }
-                   }, duration: 15, easing: "linear" },
-    
-      15: { set:   { page1Quad: { z: 0, texBounds: [0.25, 0.5, 0.00, 1] },
-                     page2Quad: { z: 0, texBounds: "page" } },
-            tween: { page1: { x: -1/3 },
-                     page2: { x: +1/3, scaleX: 2/3 },
-                   }, duration: 15, easing: "linear" },
-    },
-];
-
-BookScene = new yT(yuu.Scene, {
-    constructor: function () {
-        yuu.Scene.call(this);
-        var bg = new yuu.E(
-            new yuu.Transform().setScale([20, 20, 1]),
-            this.bgQuad = new yuu.QuadC()
-                .setColor([0, 0, 0, 0])
-                .setZ(-1));
-        this.page1 = new yuu.E(new yuu.Transform(),
-                               this.page1Quad = new yuu.QuadC(BOOK));
-        this.page1Quad.texBounds = [0.50, 0.5, 0.75, 1];
-        this.page1Quad.z = 1;
-        this.page2 = new yuu.E(new yuu.Transform(),
-                               this.page2Quad = new yuu.QuadC(BOOK));
-        this.page2Quad.texBounds = [0.25, 0.5, 0.50, 1];
-        this.page1.transform.scale = [2/3, 1, 1];
-        this.page2.transform.scale = [2/3, 1, 1];
-        this.entity0.attach(new yuu.Transform());
-        this.current = 0;
-        this._locks = new FlagSet("turn");
-        this.addEntities(bg, this.page1, this.page2);
-
-        this.dismissSound = new yuu.Instrument("@book-dismiss");
-        this.pageSounds = [new yuu.Instrument("@page-turn-1"),
-                           new yuu.Instrument("@page-turn-2"),
-                           new yuu.Instrument("@page-turn-3")];
-
-        this.ready = yuu.ready([this.dismissSound].concat(this.pageSounds));
-    },
-
-    help: yuu.cmd(function () {
-        this.skip();
-    }, "dismiss the help screen"),
-
-    licensing: yuu.cmd(function () {
-        var licensing = document.getElementById("yuu-licensing");
-        var parent = licensing.parentNode;
-        var spinner = document.createElement("div");
-        spinner.className = "yuu-spinner";
-        spinner.id = licensing.id;
-        parent.replaceChild(spinner, licensing);
-        Promise.all(
-            yf.map(yuu.GET,
-                   [yuu.PATH + "data/license.txt", "data/license.txt"]))
-            .then(function (texts) {
-                var text = texts.join("\n-- \n\n");
-                var p = document.createElement("pre");
-                p.textContent = text;
-                p.id = spinner.id;
-                parent.replaceChild(p, spinner);
-            });
-    }, "why would you ever want to run this?"),
-
-    init: function () {
-        this._anim(BOOK_APPEAR);
-        storage.setFlag("instructions");
-    },
-
-    _anim: function (anim) {
-        this._locks.increment("turn");
-        // FIXME: Need hooks from animations to audio
-        var completion = this._locks.decrementer("turn");
-        switch (anim) {
-        case BOOK_DISMISS:
-            this.dismissSound.play();
-            completion = yuu.director.removeScene.bind(yuu.director, this);
-            break;
-        case BOOK_APPEAR:
-            sounds.bookAppear.play();
-            break;
-        default:
-            yuu.random.choice(this.pageSounds).play();
-            break;
-        }
-
-        var device = yuu.director.preferredDevice();
-        this.entity0.attach(new yuu.Animation(
-            anim, {
-                $: this.entity0.transform,
-                page: device === "keyboard" ? KEYBOARD_PAGE
-                    : device === "gamepad" ? GAMEPAD_PAGE 
-                    : POINTERS_PAGE,
-                page1: this.page1.transform,
-                page2: this.page2.transform,
-                page1Quad: this.page1Quad,
-                page2Quad: this.page2Quad,
-                bgQuad: this.bgQuad
-            }, completion));
-    },
-
-    advance: yuu.cmd(function () {
-        if (this._locks.some("turn"))
-            return;
-        this._anim(BOOK_FORWARD[this.current++]);
-    }),
-
-    skip: yuu.cmd(function () {
-        if (this._locks.some("turn"))
-            return;
-        this._anim(BOOK_DISMISS);
-    }),
-
-    back: yuu.cmd(function () {
-        if (this._locks.some("turn"))
-            return;
-        if (this.current > 0)
-            this._anim(BOOK_BACKWARD[--this.current]);
-    }),
-
-    LOGOTYPE: new yuu.AABB(-0.16, -0.41, 0.12, -0.33),
-    COLOPHON: new yuu.AABB(-0.06, -0.41, 0.11, -0.28),
-
-    inputs: {
-        resize: function () {
-            var base = new yuu.AABB(-0.7, -0.55, 0.7, 0.55);
-            var vp = base.matchAspectRatio(yuu.viewport);
-            this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
-        },
-
-        mousemove: function (p) {
-            p = this.layer0.worldFromDevice(p);
-            if (this.current === BOOK_FORWARD.length - 1
-                && this.LOGOTYPE.contains(p)) {
-                this.cursor = "pointer";
-            } else if (this.current === 0 && this.COLOPHON.contains(p)) {
-                this.cursor = "pointer";
-            } else if (this.current === 0 || p.x >= -0.2) {
-                this.cursor = "";
-            } else {
-                this.cursor = "W-resize";
-            }
-        },
-        
-        tap: function (p) {
-            p = this.layer0.worldFromDevice(p);
-            if (this.current === BOOK_FORWARD.length - 1
-                && this.LOGOTYPE.contains(p)) {
-                yuu.openURL("http://www.yukkurigames.com/");
-            } else if (this.current === 0 && this.COLOPHON.contains(p)) {
-                yuu.director.showOverlay("colophon");
-            } else if (this.current === 0 || p.x >= -0.2) {
-                this.advance();
-            } else {
-                this.back();
-            }
-            return true;
-        },
-        swipeleft: function (event) { this.advance(); return true; },
-        swiperight: function (event) { this.back(); return true; },
-        dragleft: function (event) { this.advance(); return true; },
-        dragright: function (event) { this.back(); return true; },
-        swipeup: function (event) { this.skip(); return true; },
-        dragup: function (event) { this.skip(); return true; },
-
-        consume: yuu.Director.prototype.GESTURES
-            .concat(yuu.Director.prototype.CANVAS_EVENTS)
-    },
-
-    KEYBINDS: {
-        space: "advance",
-        shift: "advance",
-        z: "advance",
-        x: "advance",
-        right: "advance",
-        left: "back",
-        back: "skip",
-        escape: "skip",
-        gamepadbutton0: "advance",
-        gamepadbutton1: "skip",
-        gamepadbutton4: "back",
-        gamepadbutton5: "advance",
-        gamepadbutton8: "skip",
-        gamepadbutton9: "skip",
-        gamepadbutton14: "back",
-        gamepadbutton15: "advance",
-    }
-});
-
-var OUTER_FLIP_TICK = {
-    0: { tween1: { yaw: "yaw" }, duration: 15 }
-};
-
-var CIRCLE_TO_BOTTOM = {
-    0: { tween1: { pitch: Math.PI * 0.35, y: -0.3 }, duration: 35 }
-};
-
-var CIRCLE_TO_BACK = {
-    0: { tween1: { pitch: Math.PI * 0.15, y: -0.1 }, duration: 35 }
-};
-
-var CIRCLE_INNER_RATCHET = {
-    0:  { tween1: { rotation: "rotation1" }, duration: 15 },
-    10: { tween1: { rotation: "rotation2" }, duration: 10 },
-    20: { tween1: { rotation: "rotation1" }, duration: 20,
-          easing: yuu.Tween.STEPPED(5) },
-    40: { tween1: { rotation: "rotation2" }, duration: 15 }
-};
-
-var CIRCLE_INNER_WIND = {
-    0:  { tween1: { rotation: "rotation1" }, duration: 8 },
-    15: { tween1: { rotation: "rotation2" }, duration: 20 },
-};
-
-var BACKGROUND_DRIFT = {
-    0: [{ tween1: { yaw: Math.PI * 2 },
-          duration: 13 * 60 * 60, repeat: -Infinity, easing: "linear" },
-        { tween1: { scaleX: 0.5 },
-          duration: 11 * 60 * 60, repeat: -Infinity },
-        { tween1: { scaleY: 0.5 },
-          duration: 7 * 60 * 60, repeat: -Infinity }]
-};
-
-var HAND_TICK = {
-    0: { tween1: { rotation: "rotation" }, duration: 6 }
-};
-
-var CHIMES = [
-    // Nearly all derived from
-    // http://www.clockguy.com/SiteRelated/SiteReferencePages/ClockChimeTunes.html
-    //
-    // All transposition & transcription errors are mine.
-
-    { name: "Westminster",
-      keys: ["D4", "E4"],
-      bars: ["0 2 1 -3",
-             "0 1 2 0",
-             "2 0 1 -3",
-             "2 1 0 -3",
-             "-3 1 2 0"]
-    },
-
-    { name: "Wittington",
-      keys: ["Eb4", "E4"],
-      bars: ["1 2 3 5 4 6 7 0",
-             "1 3 5 7 6 4 2 0",
-             "3 1 2 4 5 6 7 0",
-             "4 3 5 2 6 1 7 0",
-             "6 7 2 5 4 1 3 0",
-             "7 1 6 2 5 3 4 0",
-             "7 3 2 1 4 5 6 0",
-             "7 3 6 2 5 1 4 0",
-             "7 5 3 1 6 4 2 0",
-             "7 5 6 4 1 3 2 0",
-             "7 6 3 2 5 4 1 0",
-             "7 6 5 4 3 2 1 0"]
-    },
-
-    { name: "Canterbury",
-      keys: ["D4", "E4"],
-      bars: ["2 0 5 3 1 4",
-             "3 5 1 4 0 2",
-             "3 5 4 2 1 0",
-             "5 3 1 4 2 0",
-             "1 3 5 2 0 4",
-             "0 5 3 1 2 4",
-             "5 3 1 2 4 0"]
-    },
-
-    { name: "Trinity",
-      keys: ["F3", "D4"],
-      bars: ["5 4 3 2 1 0",
-             "2 4 3 1 2 0",
-             "5 3 4 2 1 0",
-             "4 3 2 1 5 2",
-             "5 0 4 3 2 1"]
-    },
-
-    /*
-    { name: "St. Michael's",
-      keys: ["F3", "C4"],
-      bars: ["7 6 5 4 3 2 1 0",
-             "7 1 2 3 6 4 5 0",
-             "4 3 2 5 1 6 7 0",
-             "6 7 2 3 1 4 5 0",
-             "4 6 2 7 3 1 5 0"]
-    },
-    */
-
-    { name: "Winchester",
-      keys: ["C4", "E4"],
-      bars: ["5 3 1 0 2 4",
-             "0 1 3 5 4 2",
-             "5 3 1 4 2 0",
-             "1 2 5 4 0 1",
-             "5 1 3 2 4 0"]
-    }
-             
-];
-
-function third (s) {
-    return "Q " + s.replace(/([^ ]+ [^ ]+)/g, "$1 Z");
-}
-
-function silence (s) {
-    return "Q " + s.replace(/[^ ]+/g, "Z");
-}
-
-var TIMES1 = ["Q", "H", "H", "H", "H.", "H.", "W", "W"];
-var TIMES2 = ["Q", "Q", "Q", "Q", "Q", "Q", "Q", "Q", "Q",
-              third, third, "Q.", "Q.",
-              "H", "H"];
-var TIMES3 = ["Q", "Q", silence, silence,
-              third, third, third, third,
-              "Q.", "Q.",
-              "H", "H"];
-
-function deck (pack, random) {
-    random = random || yuu.random;
-    var stock = [];
-    return function () {
-        if (stock.length === 0)
-            stock = random.shuffle(pack.slice());
-        return stock.pop();
-    };
-}
-
-function generateScore () {
-    var chimes = yuu.random.choice(CHIMES);
-    var bar = deck(chimes.bars);
-    function draw (t) {
-        return yf.isFunction(t) ? t(bar()) : t + " " + bar();
-    }
-    
-    function line (times) {
-        return yf.map(draw, yuu.random.shuffle(times)).join(" ");
-    }
-
-    var track = "{ - W HZ " + line(TIMES1)
-        + " { W HZ Z " + line(TIMES2)
-        + " { W HZ Z Z I Z " + line(TIMES3);
-    var key = yuu.random.choice(chimes.keys);
-    yuu.log("messages", "Playing " + chimes.name + " in " + key + " major.");
-    var score = yuu.parseScore(track, yuu.Scales.MAJOR, key);
-    score.key = key;
-    return score;
-}
-
-CircleScene = yT(yuu.Scene, {
-    constructor: function () {
-        yuu.Scene.call(this);
-        this.layer0.resize(-0.6, -0.6, 1.2, 1.2);
-        var arm = this.arm = new yuu.E(new yuu.Transform());
-        this.outer = new yuu.E(
-            new yuu.Transform([Math.sqrt(2) / 5, -Math.sqrt(2) / 5, 0]),
-            this.outerQuad = new yuu.QuadC(new yuu.Material("@circle-outer"))
-                .setZ(1)
-                .setLuminance(0.4)
-                .setSize([0.35417, 0.35417]));
-        arm.addChild(this.outer);
-
-        var rim = new yuu.E(
-            new yuu.Transform(),
-            this.rimQuad = new yuu.QuadC(new yuu.Material("@circle-rim"))
-                .setLuminance(0.2));
-        var inner = this.inner = new yuu.E(
-            new yuu.Transform(),
-            this.innerQuad = new yuu.QuadC(new yuu.Material("@circle-inner"))
-                .setLuminance(0.3));
-
-        var NOISY_QUADS = new yuu.ShaderProgram(
-            ["@noise.glsl", "@noisyquads"], ["yuu/@color"]);
-
-        var bgMat = new yuu.Material(
-            yuu.Texture.DEFAULT, NOISY_QUADS, { range: 0.8 });
-        bgMat.uniforms.cut = yf.volatile(cycler(100000));
-        var DIM = 16;
-        var batch = new yuu.QuadBatchC(DIM * DIM);
-        batch.material = bgMat;
-        var bg = new yuu.E(new yuu.Transform(), batch);
-        yf.irange(function (x) {
-            yf.irange(function (y) {
-                var quad = batch.createQuad();
-                quad.size = [1/4, 1/4];
-                quad.position = [(x - DIM / 2) * 1/4,
-                                 (y - DIM / 2) * 1/4];
-                quad.color = [0.12, 0.08, 0.16];
-                quad.texBounds = yf.repeat(x * DIM + y, 4);
-            }, DIM);
-        }, DIM);
-
-        this.entity0.addChild(bg);
-        this.entity0.attach(new yuu.Animation(
-            BACKGROUND_DRIFT, { $: bg.transform }));
-
-        this.ground = new yuu.E(new yuu.Transform());
-        this.ground.addChildren(rim, inner, arm);
-        this.entity0.addChild(this.ground);
-
-        this.music = yuu.audio.createGain();
-        this.music.gain.value = 0.3;
-        this.music.connect(yuu.audio.music);
-        this._finished = false;
-
-        this.ready = yuu.ready([
-            this.outerQuad.material,
-            this.innerQuad.material,
-            this.rimQuad.material,
-            bgMat.ready
-        ]);
-    },
-
-    help: yuu.cmd(function () {
-        yuu.director.pushScene(new BookScene());
-    }, "bring up the help screen"),
-
-    yuu: yuu.cmd(function () {
-        this.outerQuad.material = new yuu.Material("@circle-outer-ee");
-    }, "yuu~"),
-
-    KEYBINDS: {
-        slash: "help",
-        f1: "help",
-        gamepadbutton6: "help",
-        f10: "showOverlay preferences",
-        "shift+y+u+`": "yuu",
-        "gamepadbutton10+gamepadbutton11": "yuu",
-    },
-
-    inputs: {
-        resize: function () {
-            var vp = new yuu.AABB(-0.6, -0.6, 0.6, 0.6)
-                .matchAspectRatio(yuu.viewport);
-            this.layer0.resize(vp.x0, vp.y0, vp.w, vp.h);
-        }
-    },
-
-    toBottom: function () {
-        this.entity0.attach(
-            new yuu.Animation(CIRCLE_TO_BOTTOM, { $: this.ground.transform }));
-    },
-
-    wind: function () {
-        var rot1 = this.inner.transform.rotation;
-        quat.rotateZ(rot1, rot1, Math.PI / (2 * Math.E));
-        var rot2 = quat.rotateX(quat.create(), rot1, -Math.PI / 2);
-        quat.rotateY(rot2, rot2, Math.PI / 2);
-        quat.rotateX(rot2, rot2, -Math.PI / 2);
-        this.entity0.attach(
-            new yuu.Animation(CIRCLE_INNER_WIND, {
-                $: this.inner.transform,
-                rotation1: rot1,
-                rotation2: rot2
-            }));
-        this.tension = 0.5;
-        this.reversed = 0;
-        var score = [];
-        score.key = this.score && this.score.key;
-        this.score = score;
-    },
-
-    _musicSchedule: function (count) {
-        var t = yuu.director.currentAudioTime;
-        var note;
-
-        if (this._finished) {
-            if (this._finished === "won" && this.score.key) {
-                var score = yuu.parseScore(
-                    yuu.random.choice([
-                        "1 3 2 Z 0 { - 1 Z 2 Z 0",
-                        "1 2 3 Z 0 { - 1 Z 3 Z 0",
-                        "0 1 2 Z 4 { - 0 Z 2 Z 4",
-                    ]),
-                    yuu.Scales.MAJOR, this.score.key);
-                while ((note = score.shift())) {
-                    sounds.chime.createSound(
-                        yuu.audio,
-                        t + note.time / 4,
-                        note.hz,
-                        1.0, note.duration
-                    ).connect(this.music);
-                }
-            }
-            this._finished = false;
-            return false;
-        }
-
-        if (!(this.score && this.score.length)) {
-            this.score = generateScore();
-            this.playing = 0;
-        }
-
-        ++this.playing;
-        while (this.score.length && this.score[0].time < this.playing) {
-            note = this.score.shift();
-            sounds.chime.createSound(
-                yuu.audio,
-                t + note.time % 1 + yuu.random.gauss(0, 0.015),
-                note.hz,
-                1.0, note.duration
-            ).connect(this.music);
-        }
-
-        if ((this.tension *= 0.95) > 1) {
-            this.tension /= 2;
-            sounds.winding.createSound(yuu.audio, t, 0, 1.0, 1.0)
-                .connect(this.music);
-            var flip = !this.outer.transform.yaw * yuu.random.randsign(Math.PI);
-            this.entity0.attach(
-                new yuu.Animation(OUTER_FLIP_TICK, {
-                    $: this.outer.transform,
-                    yaw: flip
-                }));
-        } else {
-            [sounds.tick, sounds.tock][count & 1]
-                .createSound(yuu.audio, t, 0, 0.5, 1.0)
-                .connect(this.music);
-        }
-
-        this.clockTick(this.reversed-- > 0 ? TICK_REV : TICK_ROT);
-        
-        return true;
-    },
-
-    clockTick: function (amount, anim) {
-        var rot = this.arm.transform.rotation;
-        quat.multiply(rot, rot, amount || TICK_ROT);
-        this.arm.attach(new yuu.Animation(
-            anim || HAND_TICK,
-            { $: this.arm.transform, rotation: rot }));
-    },
-
-    toBack: function () {
-        this.wind();
-        this.entity0.attach(
-            new yuu.Animation(CIRCLE_TO_BACK, { $: this.ground.transform }));
-
-        this.playing = 4;
-        this.arm.attach(
-            new yuu.Ticker(this._musicSchedule.bind(this), 60));
-    },
-
-    win: function () {
-        this._finished = "won";
-        this.wind();
-        this.entity0.attach(
-            new yuu.Animation(FLASH, { $: this.innerQuad }),
-            new yuu.Animation(FLASH, { $: this.rimQuad }, null, 32),
-            new yuu.Animation(FLASH, { $: this.outerQuad }, null, 48)
-        );
-    },
-
-    lose: function () {
-        this._finished = "lose";
-        var rot1 = this.inner.transform.rotation;
-        quat.rotateZ(rot1, rot1, -Math.PI / Math.E);
-        var rot2 = quat.rotateZ(quat.create(), rot1, Math.PI / Math.E);
-        this.entity0.attach(
-            new yuu.Animation(CIRCLE_INNER_RATCHET, {
-                $: this.inner.transform,
-                rotation1: rot1,
-                rotation2: rot2
-            }));
-        sounds.regear.play();
-    },
-
-    rotated: function () {
-        this.tension += yuu.random.uniform(0.1);
-    },
-
-    slam: function () {
-        this.tension += yuu.random.uniform(0.2);
-    },
-
-    reverse: function () {
-        this.tension -= yuu.random.uniform(0.1);
-        this.reversed = Math.max(this.reversed, 0) + 1;
-    }
-
-});