var renderer;
var stage;
var screenWidth;
var screenHeight;
var counter;
var totalScreenPixels;
var cycleDuration;
var connectionDistance;
var connectionLimit;
var pointShiftDistance;
// var colorShiftAmt;
var disconnectedColorShiftAmt;
var polygon;
var startPoints;
var polygonPoints;
var tweeningFns;
var lastLoop;
var thisLoop;
var fps;
var fpsGraphic;
var scrollDelta;
var pointShiftBiasX;
var pointShiftBiasY;
var resolution = 1;
var debug = false;
var fpsEnabled = debug;
var allTweeningFns = [
    linearTweening,
    easeInSine,
    easeOutSine,
    easeInOutSine,
    easeInQuad,
    easeOutQuad,
    easeInOutQuad,
    easeInCubic,
    easeOutCubic,
    easeInOutCubic,
    easeInExpo,
    easeOutExpo,
    easeInOutExpo,
    easeInCirc,
    easeOutCirc,
    easeInOutCirc,
    easeOutBounce,
    easeInBounce,
    easeInOutBounce,
    easeInElastic,
    easeOutElastic,
    easeInOutElastic,
    easeInBack,
    easeOutBack,
    easeInOutBack
];
var tweeningSets = {
    linear: [0],
    meandering: [1, 2, 3, 4, 5, 6, 7, 8, 9],
    snappy: [10, 11, 12, 13, 14, 15],
    bouncy: [16, 17, 18],
    elastic: [19, 20, 21],
    back: [22, 23, 24]
};
var click = null;
var hover = null;
var lastHover = null;
var clickEnd = false;
var clickPullRateStart = 0.01;
var clickMaxDistStart = 50;
var clickPullRateInc = 0.005;
var clickPullRateMax = 0.3;
var clickMaxDistInc = 2;
var clickMaxDistMax = 5000;
var clickPullRate = clickPullRateStart;
var clickMaxDist = clickMaxDistStart;
var clickPullRateEnd = -0.6;
var clickInertiaStart = -0.7;
var clickInertia = clickInertiaStart;
var clickInertiaEnd = 0.3;
var clickTweeningFnStart = null;
var clickTweeningFn = clickTweeningFnStart;
var clickTweeningFnEnd = 12;
var hoverPushRate = -0.05;
var hoverInertia = 0.8;
var hoverMaxDistStart = 75;
var hoverMaxDistMax = 1000;
var hoverMaxDist = hoverMaxDistStart;
var hoverTweeningFn = 5;

function randomInt (min, max) {
    // inclusive of min and max
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

/* eslint-disable no-unused-vars */
function linearTweening (t, b, c, d) {
    // t = current time
    // b = start value
    // c = change in value
    // d = duration
    return ((c * t) / d) + b;
}

function easeOutBounce (t, b, c, d) {
    if ((t /= d) < (1 / 2.75)) {
        return c * (7.5625 * t * t) + b;
    } else if (t < (2 / 2.75)) {
        return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b;
    } else if (t < (2.5 / 2.75)) {
        return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b;
    } else {
        return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b;
    }
}

function easeInBounce (t, b, c, d) {
    return c - easeOutBounce(d - t, 0, c, d) + b;
}

function easeInOutBounce (t, b, c, d) {
    if (t < d / 2) return easeInBounce(t * 2, 0, c, d) * 0.5 + b;
    return easeOutBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b;
}

function easeInSine (t, b, c, d) {
    return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;
}

function easeOutSine (t, b, c, d) {
    return c * Math.sin(t / d * (Math.PI / 2)) + b;
}

function easeInOutSine (t, b, c, d) {
    return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
}

function easeInQuad (t, b, c, d) {
    return c * (t /= d) * t + b;
}

function easeOutQuad (t, b, c, d) {
    return -c * (t /= d) * (t - 2) + b;
}

function easeInOutQuad (t, b, c, d) {
    if ((t /= d / 2) < 1) return c / 2 * t * t + b;
    return -c / 2 * ((--t) * (t - 2) - 1) + b;
}

function easeInCubic (t, b, c, d) {
    return c * (t /= d) * t * t + b;
}

function easeOutCubic (t, b, c, d) {
    return c * ((t = t / d - 1) * t * t + 1) + b;
}

function easeInOutCubic (t, b, c, d) {
    if ((t /= d / 2) < 1) return c / 2 * t * t * t + b;
    return c / 2 * ((t -= 2) * t * t + 2) + b;
}

function easeInExpo (t, b, c, d) {
    return (t === 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
}

function easeOutExpo (t, b, c, d) {
    return (t === d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
}

function easeInOutExpo (t, b, c, d) {
    if (t === 0) return b;
    if (t === d) return b + c;
    if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
    return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;
}

function easeInElastic (t, b, c, d) {
    var s = 1.70158; var p = 0; var a = c;
    if (t === 0) return b; if ((t /= d) === 1) return b + c; if (!p) p = d * 0.3;
    if (a < Math.abs(c)) { a = c; s = p / 4; } else s = p / (2 * Math.PI) * Math.asin(c / a);
    return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
}

function easeOutElastic (t, b, c, d) {
    var s = 1.70158; var p = 0; var a = c;
    if (t === 0) return b; if ((t /= d) === 1) return b + c; if (!p) p = d * 0.3;
    if (a < Math.abs(c)) { a = c; s = p / 4; } else s = p / (2 * Math.PI) * Math.asin(c / a);
    return a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b;
}

function easeInOutElastic (t, b, c, d) {
    var s = 1.70158; var p = 0; var a = c;
    if (t === 0) return b; if ((t /= d / 2) === 2) return b + c; if (!p) p = d * (0.3 * 1.5);
    if (a < Math.abs(c)) { a = c; s = p / 4; } else s = p / (2 * Math.PI) * Math.asin(c / a);
    if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
    return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * 0.5 + c + b;
}

function easeInBack (t, b, c, d, s) {
    if (s === undefined) s = 1.70158;
    return c * (t /= d) * t * ((s + 1) * t - s) + b;
}

function easeOutBack (t, b, c, d, s) {
    if (s === undefined) s = 1.70158;
    return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
}

function easeInOutBack (t, b, c, d, s) {
    if (s === undefined) s = 1.70158;
    if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b;
    return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b;
}

function easeInCirc (t, b, c, d) {
    return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
}

function easeOutCirc (t, b, c, d) {
    return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
}

function easeInOutCirc (t, b, c, d) {
    if ((t /= d / 2) < 1) return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;
    return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
}
/* eslint-enable no-unused-vars */

// from: http://stackoverflow.com/a/5624139
// modified to return integer literal
function rgbToHex (color) {
    return parseInt(((1 << 24) + (color.r << 16) + (color.g << 8) + color.b).toString(16).slice(1), 16);
}

/* Choose a random RGB color */
function randomColor () {
    return {
        r: Math.floor(Math.random() * (255 + 1)),
        g: Math.floor(Math.random() * (255 + 1)),
        b: Math.floor(Math.random() * (255 + 1))
    };
}

/* Find the average of two RGB colors and return one RGB of that color */
function averageColor (color1, color2) {
    return {
        r: Math.round((color1.r + color2.r) / 2),
        g: Math.round((color1.g + color2.g) / 2),
        b: Math.round((color1.b + color2.b) / 2)
    };
}

/*
 * Find the average of two RGB colors where color1 is weighted against color2 by the given weight.
 * weight is a decimal between 0 and 1.
 */
function weightedAverageColor (color1, color2, weight) {
    return {
        r: Math.round(((color1.r * (2 * weight)) + (color2.r * (2 * (1 - weight)))) / 2),
        g: Math.round(((color1.g * (2 * weight)) + (color2.g * (2 * (1 - weight)))) / 2),
        b: Math.round(((color1.b * (2 * weight)) + (color2.b * (2 * (1 - weight)))) / 2)
    };
}

/* Darken the color by a factor of 0 to 1, where 1 is black and 0 is white */
function shadeColor (color, shadeFactor) {
    return {
        r: Math.round(color.r * (1 - shadeFactor)),
        g: Math.round(color.g * (1 - shadeFactor)),
        b: Math.round(color.b * (1 - shadeFactor))
    };
}

/* Given a color component (red, green, or blue int), randomly shift by configurable amount */
function shiftColorComponent (component, maxShiftAmt) {
    var shiftAmt = randomInt(maxShiftAmt * -1, maxShiftAmt);
    var newComponent = component + shiftAmt;
    if ((newComponent < 0) || (newComponent > 255)) {
        newComponent = component - shiftAmt;
    }
    return newComponent;
}

/* Randomly shift a RGB color by a configurable amount and return new RGB color */
function shiftColor (color, maxShiftAmt) {
    return {
        r: shiftColorComponent(color.r, maxShiftAmt),
        g: shiftColorComponent(color.g, maxShiftAmt),
        b: shiftColorComponent(color.b, maxShiftAmt)
    };
}

/* from: https://stackoverflow.com/a/17130415 */
function getMousePos (evt, res) {
    var canvas = document.getElementsByTagName('canvas')[0];
    if (canvas !== undefined) {
        var rect = canvas.getBoundingClientRect();
        return {
            x: Math.round((evt.clientX - rect.left) / (rect.right - rect.left) * canvas.width / res),
            y: Math.round((evt.clientY - rect.top) / (rect.bottom - rect.top) * canvas.height / res)
        };
    }
}

function distancePos (point1, point2) {
    var a = point1.x - point2.x;
    var b = point1.y - point2.y;
    return Math.sqrt(a * a + b * b);
}

function distance (point1, point2) {
    var a = point1[0] - point2[0];
    var b = point1[1] - point2[1];
    return Math.sqrt(a * a + b * b);
}

function getRandomPoints (numPoints, maxX, maxY, tweeningFns) {
    var i, x, y, color, cycleStart, easingFn;
    var points = [];
    for (i = 0; i < numPoints; i++) {
        x = randomInt(0, maxX - 1);
        y = randomInt(0, maxY - 1);
        cycleStart = randomInt(0, cycleDuration - 1);
        color = randomColor();
        easingFn = tweeningFns[Math.floor(Math.random() * tweeningFns.length)];
        points[i] = [x, y, cycleStart, color, easingFn];
    }
    return points;
}

// eslint-disable-next-line no-unused-vars
function shiftPointCounter (original, maxShiftAmt) {
    var shiftAmt = randomInt(maxShiftAmt * -1, 0);
    var newCounter = original + shiftAmt;
    if (newCounter < 0) {
        newCounter = cycleDuration + shiftAmt;
    }
    return newCounter;
}

function shiftPoints (points, maxShiftAmt, counter, tweeningFns) {
    var i, shiftX, shiftY, candidateX, candidateY;
    for (i = 0; i < points.original.length; i++) {
        if (points.target[i][2] >= cycleDuration) {
            // cycleDuration was reduced and now this point's cycle is out of bounds. Randomly pick a new valid one.
            points.target[i][2] = randomInt(0, cycleDuration - 1);
        }
        if (points.target[i][2] === counter) {
            points.original[i] = points.target[i].slice();
            shiftX = randomInt(maxShiftAmt * -1, maxShiftAmt);
            shiftY = randomInt(maxShiftAmt * -1, maxShiftAmt);
            if (((shiftX < 0) && (pointShiftBiasX === 1)) || ((shiftX > 0) && (pointShiftBiasX === -1))) {
                shiftX = shiftX * -1;
            }
            if (((shiftY < 0) && (pointShiftBiasY === 1)) || ((shiftY > 0) && (pointShiftBiasY === -1))) {
                shiftY = shiftY * -1;
            }
            candidateX = points.original[i][0] + shiftX;
            candidateY = points.original[i][1] + shiftY;
            if ((candidateX > screenWidth) || (candidateX < 0)) {
                candidateX = points.original[i][0] - shiftX;
            }
            if ((candidateY > screenHeight) || (candidateY < 0)) {
                candidateY = points.original[i][1] - shiftY;
            }
            points.target[i][0] = candidateX;
            points.target[i][1] = candidateY;
            points.target[i][4] = tweeningFns[Math.floor(Math.random() * tweeningFns.length)];
            // FIXME: buggy, makes points jump around too fast
            // points.target[i][2] = shiftPointCounter(points.original[i][2], maxShiftAmt);
        }
    }
    // clear pointShiftBiases now that they have been "used"
    pointShiftBiasX = 0;
    pointShiftBiasY = 0;

    return points;
}

function pullPoints (points, clickPos, pullRate, inertia, maxDist, counter, resetPoints, tweeningFn) {
    var xDist, yDist, xTraveled, yTraveled;
    for (var i = 0; i < points.target.length; i++) {
        xDist = clickPos.x - points.target[i][0];
        yDist = clickPos.y - points.target[i][1];
        xTraveled = clickPos.x - points.original[i][0];
        yTraveled = clickPos.y - points.original[i][1];
        if (Math.abs(xDist) <= maxDist && Math.abs(yDist) <= maxDist) {
            if (resetPoints) {
                // Good for changing directions, reset the points original positions to their current positions
                points.original[i][0] = points.tweened[i][0];
                points.original[i][1] = points.tweened[i][1];
            }
            points.target[i][0] += Math.round((xDist + (inertia * xTraveled)) * pullRate);
            points.target[i][1] += Math.round((yDist + (inertia * yTraveled)) * pullRate);
            points.target[i][3] = shiftColor(points.original[i][3], disconnectedColorShiftAmt * 3);
            if (tweeningFn !== null) {
                // Also twitch the tweening function for all affected points for additional effect
                points.target[i][4] = tweeningFn;
            }
        }
        // TODO: define these magic numbers somewhere
        // If this point's cycle is near it's end, bump it up some ticks to make the animation smoother
        if (relativeCounter(points.target[i][2]) > cycleDuration - 10) {
            points.target[i][2] = (points.target[i][2] + Math.round(cycleDuration / 2)) % cycleDuration;
        }
    }
}

function redistributeCycles (points, oldCycleDuration, cycleDuration) {
    var progress;
    for (var i = 0; i < points.original.length; i++) {
        progress = points.target[i][2] / oldCycleDuration;
        points.target[i][2] = Math.round(progress * cycleDuration);
    }
    return points;
}

function relativeCounter (counter, targetStart) {
    var relCounter = counter - targetStart;
    if (relCounter < 0) {
        return cycleDuration + relCounter;
    }
    return relCounter;
}

function drawPolygon (polygon, points, counter, tweeningFns) {
    var i, j, easingFn, avgColor, shadedColor, connectionCount, dist, connectivity;
    // calculate vectors
    for (i = 0; i < points.original.length; i++) {
        easingFn = allTweeningFns[points.target[i][4]];
        points.tweened[i][0] = easingFn(relativeCounter(counter, points.target[i][2]), points.original[i][0], points.target[i][0] - points.original[i][0], cycleDuration);
        points.tweened[i][1] = easingFn(relativeCounter(counter, points.target[i][2]), points.original[i][1], points.target[i][1] - points.original[i][1], cycleDuration);
    }
    // draw lines
    for (i = 0; i < points.original.length; i++) {
        connectionCount = 0;
        for (j = i + 1; j < points.original.length; j++) {
            // TODO pick the N (connectionLimit) closest connections instead of the first N that occur sequentially.
            if (connectionCount >= connectionLimit) break;
            dist = distance(points.tweened[i], points.tweened[j]);
            connectivity = dist / connectionDistance;
            if ((j !== i) && (dist <= connectionDistance)) {
                // find average color of both points
                if ((points.tweened[i][2] === counter) || (points.tweened[j][2] === counter)) {
                    // avgColor = shiftColor(avgColor, Math.round(colorShiftAmt * (1 - connectivity)));
                    points.tweened[i][3] = weightedAverageColor(points.tweened[i][3], points.tweened[j][3], connectivity);
                    points.tweened[j][3] = weightedAverageColor(points.tweened[j][3], points.tweened[i][3], connectivity);
                }
                avgColor = averageColor(points.tweened[i][3], points.tweened[j][3]);
                shadedColor = shadeColor(avgColor, connectivity);
                polygon.lineStyle(1, rgbToHex(shadedColor), 1);

                polygon.moveTo(points.tweened[i][0], points.tweened[i][1]);
                polygon.lineTo(points.tweened[j][0], points.tweened[j][1]);
                connectionCount = connectionCount + 1;
            }
        }

        if (connectionCount === 0) {
            points.tweened[i][3] = shiftColor(points.tweened[i][3], disconnectedColorShiftAmt);
        }

        // draw vectors
        polygon.lineStyle(1, rgbToHex(points.tweened[i][3]), 1);
        polygon.drawCircle(points.tweened[i][0], points.tweened[i][1], 1);
    }
}

function loop () {
    screenWidth = document.documentElement.clientWidth;
    screenHeight = document.documentElement.clientHeight;
    renderer.resize(screenWidth, screenHeight);

    polygon.clear();

    if (click !== null) {
        if (clickEnd) {
            clickPullRate = clickPullRateEnd;
            clickInertia = clickInertiaEnd;
            clickTweeningFn = clickTweeningFnEnd;
            if (debug) {
                polygon.lineStyle(1, 0xDC143C, 1);
                polygon.drawCircle(click.x, click.y, clickMaxDist);
            }
        } else {
            if (debug) {
                polygon.lineStyle(1, 0x483D8B, 1);
                polygon.drawCircle(click.x, click.y, clickMaxDist);
            }
        }

        // a pointer event is occuring and needs to affect the points
        pullPoints(polygonPoints, click, clickPullRate, clickInertia, clickMaxDist, counter, clickEnd, clickTweeningFn);

        // slightly increase effect amount for next loop if click is still occuring
        if (clickMaxDist <= clickMaxDistMax) {
            clickMaxDist += clickMaxDistInc;
        }
        if (clickPullRate <= clickPullRateMax) {
            clickPullRate += clickPullRateInc;
        }

        if (clickEnd) {
            click = null;
            clickEnd = false;
            clickMaxDist = clickMaxDistStart;
            clickPullRate = clickPullRateStart;
            clickInertia = clickInertiaStart;
            clickTweeningFn = clickTweeningFnStart;
        }
    } else if (hover !== null) {
        if (lastHover !== null) {
            hoverMaxDist += Math.min(Math.round(distancePos(hover, lastHover)), hoverMaxDistMax);
        }
        if (debug) {
            polygon.lineStyle(1, 0xBDB76B, 1);
            polygon.drawCircle(hover.x, hover.y, hoverMaxDist);
        }

        pullPoints(polygonPoints, hover, hoverPushRate, hoverInertia, hoverMaxDist, counter, false, hoverTweeningFn);

        hoverMaxDist = hoverMaxDistStart;
        lastHover = hover;
    }

    // polygon.beginFill(0x00FF00);
    drawPolygon(polygon, polygonPoints, counter, tweeningFns);
    // polygon.endFill();

    counter += 1;
    counter = counter % cycleDuration;

    if (counter === 0 && fpsEnabled) {
        thisLoop = new Date();
        fps = Math.round((1000 / (thisLoop - lastLoop)) * cycleDuration);
        fpsGraphic.setText(fps.toString());
        lastLoop = thisLoop;
    }

    polygonPoints = shiftPoints(polygonPoints, pointShiftDistance, counter, tweeningFns);

    // var mousePosition = renderer.plugins.interaction.mouse.global;
    // triangle.x = mousePosition.x;
    // triangle.y = mousePosition.y;

    // If user scrolled, modify cycleDuration by amount scrolled
    if (scrollDelta !== 0) {
        var oldCycleDuration = cycleDuration;
        cycleDuration = Math.round(cycleDuration + scrollDelta);
        if (cycleDuration < 1) {
            cycleDuration = 1;
        }
        scrollDelta = 0;
        polygonPoints = redistributeCycles(polygonPoints, oldCycleDuration, cycleDuration);
    }

    // Tell the `renderer` to `render` the `stage`
    renderer.render(stage);
    window.requestAnimationFrame(loop);
}

function loopStart () {
    screenWidth = document.documentElement.clientWidth;
    screenHeight = document.documentElement.clientHeight;
    // Create the renderer
    renderer = window.PIXI.autoDetectRenderer(screenWidth, screenHeight, {antialias: true, resolution: resolution});

    // Add the canvas to the HTML document
    document.body.appendChild(renderer.view);

    // Create a container object called the `stage`
    stage = new window.PIXI.Container();

    renderer.view.style.position = 'absolute';
    renderer.view.style.display = 'block';
    renderer.autoResize = true;

    counter = 0;
    totalScreenPixels = screenWidth + screenHeight;
    cycleDuration = 60;
    connectionDistance = Math.min(Math.round(totalScreenPixels / 16), 75);
    connectionLimit = 10;
    pointShiftDistance = Math.round(totalScreenPixels / 45);
    // colorShiftAmt = 80;
    disconnectedColorShiftAmt = 10;
    polygon = new window.PIXI.Graphics();
    tweeningFns = tweeningSets.meandering;
    startPoints = getRandomPoints(Math.round(totalScreenPixels / 6), screenWidth, screenHeight, tweeningFns);
    polygonPoints = {
        original: startPoints,
        target: JSON.parse(JSON.stringify(startPoints)),
        tweened: JSON.parse(JSON.stringify(startPoints))
    };
    stage.addChild(polygon);

    fpsGraphic = new window.PIXI.Text('0', {font: '25px monospace', fill: 'yellow'});
    fpsGraphic.x = 0;
    fpsGraphic.y = 0;

    if (fpsEnabled) {
        stage.addChild(fpsGraphic);
    }

    lastLoop = new Date();

    scrollDelta = 0;

    window.requestAnimationFrame(loop);
}

window.onload = loopStart;

/* MOUSE AND TOUCH EVENTS */

// FIXME: buggy :(
window.addEventListener('mousewheel', function (e) {
    scrollDelta = scrollDelta + ((e.deltaY / 100) * 3);
});

window.addEventListener('touchstart', function (e) {
    click = getMousePos(e.changedTouches[0], resolution);
});

window.addEventListener('touchmove', function (e) {
    if (click !== null) {
        click = getMousePos(e.changedTouches[0], resolution);
    }
});

window.addEventListener('touchend', function (e) {
    clickEnd = true;
});

window.addEventListener('touchcancel', function (e) {
    clickEnd = true;
});

window.addEventListener('mousedown', function (e) {
    click = getMousePos(e, resolution);
});

window.addEventListener('mousemove', function (e) {
    var pos = getMousePos(e, resolution);
    if (click !== null) {
        click = pos;
    }
    hover = pos;
});

window.addEventListener('mouseup', function (e) {
    clickEnd = true;
    hover = null;
    lastHover = null;
});

window.addEventListener('mouseleave', function (e) {
    clickEnd = true;
    hover = null;
    lastHover = null;
});

/* KEYBOARD EVENTS */

window.addEventListener('keydown', function (e) {
    if (e.keyCode === 37) { // left
        pointShiftBiasX = -1;
    } else if (e.keyCode === 38) { // up
        pointShiftBiasY = -1;
    } else if (e.keyCode === 39) { // right
        pointShiftBiasX = 1;
    } else if (e.keyCode === 40) { // down
        pointShiftBiasY = 1;
    } else if (e.keyCode === 49) { // 1
        tweeningFns = tweeningSets.linear;
    } else if (e.keyCode === 50) { // 2
        tweeningFns = tweeningSets.meandering;
    } else if (e.keyCode === 51) { // 3
        tweeningFns = tweeningSets.snappy;
    } else if (e.keyCode === 52) { // 4
        tweeningFns = tweeningSets.bouncy;
    } else if (e.keyCode === 53) { // 5
        tweeningFns = tweeningSets.elastic;
    } else if (e.keyCode === 54) { // 6
        tweeningFns = tweeningSets.back;
    } else if (e.keyCode === 70) { // f
        // toggle fpsCounter
        if (fpsEnabled) {
            stage.removeChild(fpsGraphic);
            fpsEnabled = false;
        } else {
            stage.addChild(fpsGraphic);
            fpsEnabled = true;
            lastLoop = new Date();
        }
    }
});