/* Sorry, this code is pretty damn ugly. I spend 8 hours 5 days a week refactoring code, I just don't feel like making
 * it better right now. Maybe I'll have enough caffiene in my veins and nothing better to do sometime to put this all
 * into ES2015 modules or something.
 */
// global vars
var renderer;
var stage;
var screenWidth;
var screenHeight;
var counter;
var totalScreenPixels;
var connectionDistance;
var pointShiftDistance;
var polygon;
var startPoints;
var polygonPoints;
var lastLoop;
var thisLoop;
var fps;
var fpsGraphic;
var scrollDelta;
var pointShiftBiasX;
var pointShiftBiasY;
var numPoints;

// global non-configurable vars (modifying these might break stuff)
var click = null;
var hover = null;
var lastHover = null;
var clickEnd = false;
var sprites = [];
var reset = false;

// global configurable vars
var resolution = 1; // scaling for PIXI renderer
var debug = false; // toggles drawing extra indicators for debugging
var fpsEnabled = debug; // toggles the FPS counter
var cycleDuration = 60; // length of a point's "cycle": number of frames it takes for it to travel to its chosen destination
var connectionLimit = 10; // maximum number of lines drawn from one point to others within connection distance
// colorShiftAmt = 80; // disabled for now
var disconnectedColorShiftAmt = 10; // when a point is alone (not connected), shift RGB values by this amount every tick
var allTweeningFns = [ // array of all possible tweening functions, these are defined below
    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
];
// sets of tweening functions that I think look good with points randomly choose from them
var tweeningSets = { // numbers refer to indicies into the allTweeningsFns array above
    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: [24]
};
var tweeningFns = tweeningSets.meandering; // the actual set of tweening functions points will randomly choose from
// click effect related config vars
var clickPullRateStart = 0.01; // initial value for the ratio of a point's distance from the click position to travel in one cycle
var clickPullRateInc = 0.005; // amount to increase clickPullRate every tick that a click is held
var clickPullRateMax = 0.5; // maximum value of clickPullRate
var clickPullRate = clickPullRateStart;
var clickMaxDistStart = 50; // initial value for the effect radius of a click: points this distance from click position will be pulled (overridden if small screen size)
var clickMaxDistInc = 2; // amount to increase clickMaxDist every tick that a click is held
var clickMaxDistMax = 5000; // maximum value of clickMaxDist
var clickMaxDist = clickMaxDistStart;
var clickInertiaStart = -0.7; // initial value of the ratio of point's origin distance from the click position to be added to point's new target
var clickInertia = clickInertiaStart;
var clickTweeningFnStart = null; // initial value of the specific tweening function to assign to points in effect radius (null will not change functions)
var clickTweeningFn = clickTweeningFnStart;
var clickColorShiftAmt = disconnectedColorShiftAmt * 3; // amount of RGB color value to shift for each point in effect radius
var clickPullRateEnd = -0.5; // value of clickPullRate during tick after end of click (for "rebound" effect)
var clickInertiaEnd = 0.3; // value of clickInertia during tick after end of click
var clickTweeningFnEnd = 12; // value of clickTweeningFn during tick after end of click (number refers to index into allTweeningsFns)
// hover effect related config vars
var hoverPushRate = -0.05; // ratio of a point's distance from the hover position to travel in one cycle
var hoverInertia = 0.8;  // ratio of a point's origin distance from the click position to be added to point's new target
var hoverMaxDistStart = 75; // initial value for the effect radius of a hover: points this distance from hover position will be pushed (overridden if small screen size)
var hoverMaxDistMax = 1000; // maximum value of hoverMaxDist
var hoverMaxDist = hoverMaxDistStart;
var hoverTweeningFn = 5;  // specific tweening function to assign to points in effect radius
var zRange = 50;  // maximum value for the range of possible z coords for a point
var nodeImg = 'img/node.png';  // image file location for representing every point
var nodeImgRes = 100;  // resolution of nodeImg file in pixels (aspect ratio should be square)
// var minNodeDiameter = 3;  // minimum pixel size of point on canvas when z coord is 0, maximum is this value plus zRange
var nodeSize = 3;  // with z coord ignored, this value is used for scaling the drawing for each node
var drawNodes = true;  // whether to display circles at each point's current position
var drawLines = true;  // whether to display lines connecting points if they are in connection distance
var lineSize = 1;  // thickness in pixels of drawn lines between points

/* TWEENING FUNCTIONS */

// These are modified versions of the jquery easing functions:
// https://github.com/danro/jquery-easing/blob/master/jquery.easing.js
// See license in LICENSE-3RD-PARTY.txt

/* 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 */

/* TOGGLE FUNCTIONS */

function toggleHelp () {
    var help, controls;
    help = document.getElementById('help');
    controls = document.getElementById('controls');
    if (help.style.display === 'none') {
        help.style.display = 'block';

        // hide controls if open (only want one panel open at a time)
        if (controls.style.display === 'block') {
            controls.style.display = 'none';
        }
    } else {
        help.style.display = 'none';
    }
}

function toggleControls () {
    var help, controls;
    help = document.getElementById('help');
    controls = document.getElementById('controls');
    if (controls.style.display === 'none') {
        controls.style.display = 'block';

        // hide help if open (only want one panel open at a time)
        if (help.style.display === 'block') {
            help.style.display = 'none';
        }
    } else {
        controls.style.display = 'none';
    }
}

function toggleFPS () {
    var fpsCheckbox = document.getElementsByName('fpsCounterToggle')[0];
    if (fpsEnabled) {
        stage.removeChild(fpsGraphic);
        fpsEnabled = false;
    } else {
        stage.addChild(fpsGraphic);
        fpsEnabled = true;
        lastLoop = new Date();
    }
    fpsCheckbox.checked = fpsEnabled;
}

function toggleDebug () {
    var fpsCheckbox = document.getElementsByName('fpsCounterToggle')[0];
    var debugCheckbox = document.getElementsByName('debugToggle')[0];
    if (debug) {
        if (fpsEnabled) {
            stage.removeChild(fpsGraphic);
        }
        debug = false;
        fpsEnabled = debug;
    } else {
        if (!fpsEnabled) {
            stage.addChild(fpsGraphic);
        }
        debug = true;
        fpsEnabled = debug;
        lastLoop = new Date();
    }
    fpsCheckbox.checked = fpsEnabled;
    debugCheckbox.checked = debug;
}

function toggleNodes () {
    nodesCheckbox = document.getElementsByName('nodesToggle')[0];
    if (drawNodes) {
        for (i = 0; i < sprites.length; i++) {
            sprites[i].visible = false;
        }
        drawNodes = false;
    } else {
        for (i = 0; i < sprites.length; i++) {
            sprites[i].visible = true;
        }
        drawNodes = true;
    }
    nodesCheckbox.checked = drawNodes;
}

function toggleLines () {
    linesCheckbox = document.getElementsByName('linesToggle')[0];
    drawLines = !drawLines;
    linesCheckbox.checked = drawLines;
}

/* UTILITY FUNCTIONS */

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

// 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 distance (point1, point2) {
    var a = point1[0] - point2[0];
    var b = point1[1] - point2[1];
    return Math.sqrt(a * a + b * b);
}

function distancePos (point1, point2) {
    // TODO: refactor distance and distancePos to the same function
    var a = point1.x - point2.x;
    var b = point1.y - point2.y;
    return Math.sqrt(a * a + b * b);
}

// 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 relativeCounter (counter, targetStart) {
    /* Return current progress of point in its cycle. AKA. what count would be if cycleDuration == targetStart */
    var relCounter = counter - targetStart;
    if (relCounter < 0) {
        return cycleDuration + relCounter;
    }
    return relCounter;
}

function createSprite () {
    return new window.PIXI.Sprite(
        window.PIXI.loader.resources[nodeImg].texture
    );
}

/* POINT OPERATION FUNCTIONS */

function getRandomPoints (numPoints, maxX, maxY, maxZ, tweeningFns) {
    var i, x, y, z, color, cycleStart, easingFn, sprite;
    var points = [];
    if (sprites.length > 0) {
        // need to clear out old sprites
        for (i = 0; i < sprites.length; i++) {
            stage.removeChild(sprites[i]);
        }
        sprites = [];
    }
    for (i = 0; i < numPoints; i++) {
        x = randomInt(0, maxX - 1);
        y = randomInt(0, maxY - 1);
        // z = randomInt(0, maxZ - 1);  // TODO: do something with the 3rd dimension
        z = 0;  // turns out that 3D is hard and I am a weak 2D boy
        cycleStart = randomInt(0, cycleDuration - 1);
        color = randomColor();
        easingFn = tweeningFns[Math.floor(Math.random() * tweeningFns.length)];
        // save PIXI Sprite for each point in array
        sprite = createSprite();
        if (!drawNodes) sprite.visible = false;
        sprites.push(sprite);
        stage.addChild(sprite);
        points[i] = [x, y, z, cycleStart, color, easingFn];
    }
    return points;
}

function shiftPoints (points, maxShiftAmt, counter, tweeningFns) {
    var i, shiftX, shiftY, candidateX, candidateY;
    for (i = 0; i < points.original.length; i++) {
        if (points.target[i][3] >= cycleDuration) {
            // cycleDuration was reduced and now this point's cycle is out of bounds. Randomly pick a new valid one.
            points.target[i][3] = randomInt(0, cycleDuration - 1);
        }
        if (points.target[i][3] === 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][5] = tweeningFns[Math.floor(Math.random() * tweeningFns.length)];
            // FIXME: buggy, makes points jump around too fast
            // points.target[i][3] = shiftPointCounter(points.original[i][3], 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 targetXDiff, targetYDiff, originXDiff, originYDiff;
    for (var i = 0; i < points.target.length; i++) {
        targetXDiff = clickPos.x - points.target[i][0];
        targetYDiff = clickPos.y - points.target[i][1];
        originXDiff = clickPos.x - points.original[i][0];
        originYDiff = clickPos.y - points.original[i][1];
        if (Math.sqrt((targetXDiff * targetXDiff) + (targetYDiff * targetYDiff)) <= maxDist) {
            // point is within effect radius
            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((targetXDiff + (inertia * originXDiff)) * pullRate); // pull X
            points.target[i][1] += Math.round((targetYDiff + (inertia * originYDiff)) * pullRate); // pull Y
            // shift the color of each point in effect radius by some configurable amount
            points.target[i][4] = shiftColor(points.original[i][4], clickColorShiftAmt);
            if (tweeningFn !== null) {
                // Also switch the tweening function for all affected points for additional effect
                // The tweening function will be re-assigned at the start of the point's next cycle
                points.target[i][4] = tweeningFn;
            }
            if (debug) {
                points.target[i][6] = true;  // marks this point as affected
            }

            // 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][3]) > Math.roundcycleDuration - 10) {
                points.target[i][3] = (points.target[i][3] + Math.round(cycleDuration / 2)) % cycleDuration;
            }
        } else {
            if (debug) {
                points.target[i][6] = false;  // marks this point as unaffected
            }
        }
    }
}

function clearAffectedPoints (points) {
    for (var i = 0; i < points.target.length; i++) {
        points.target[i][6] = false;
    }
}

function redistributeCycles (points, oldCycleDuration, cycleDuration) {
    /* Given old and new cycleDuration, re-assign points' cycle starts that expand/compress to fit the new range in a
     * way that ensures the current progress of the point in its cycle is around the same percentage (so that the point
     * does not jump erratically back or forward in it's current trajectory).
     */
    // FIXME: if cycleDuration goes to 1 all points' cycles will be compressed to about the same value, and when
    // cycleDuration goes back up, the values will remain the same, making the points appear to dance in sync.
    var progress;
    for (var i = 0; i < points.original.length; i++) {
        progress = points.target[i][3] / oldCycleDuration;
        points.target[i][3] = Math.round(progress * cycleDuration);
    }
    return points;
}

function randomizeCycles (points, cycleDuration) {
    /* Assigns every point a new random cycle start */
    for (var i = 0; i < points.original.length; i++) {
        points.target[i][3] = randomInt(0, cycleDuration - 1);
    }
    return points;
}

function synchronizeCycles (points, cycleDuration) {
    /* Assigns every point the same cycle start (0) */
    for (var i = 0; i < points.original.length; i++) {
        points.target[i][3] = 0;
    }
    return points;
}

/* DRAW FUNCTIONS */

function drawPolygon (polygon, points, counter, tweeningFns) {
    var i, j, easingFn, relativeCount, avgColor, shadedColor, connectionCount, dist, connectivity, scale, nodeDiameter;
    // calculate vectors
    for (i = 0; i < points.original.length; i++) {
        easingFn = allTweeningFns[points.target[i][5]];
        relativeCount = relativeCounter(counter, points.target[i][3]);
        points.tweened[i][0] = easingFn(relativeCount, points.original[i][0], points.target[i][0] - points.original[i][0], cycleDuration);
        points.tweened[i][1] = easingFn(relativeCount, points.original[i][1], points.target[i][1] - points.original[i][1], cycleDuration);

        if (debug) {
            // draw vector trajectories
            if (points.target[i][6]) {
                polygon.lineStyle(1, 0x008b8b, 1);  // draw path different color if it is under effect of click/hover
            } else {
                polygon.lineStyle(1, 0x191970, 1);
            }
            polygon.moveTo(points.tweened[i][0], points.tweened[i][1]);
            for (j = relativeCount; j < cycleDuration; j++) {
                polygon.lineTo(
                    easingFn(j, points.original[i][0], points.target[i][0] - points.original[i][0], cycleDuration),
                    easingFn(j, 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][3] === counter) || (points.tweened[j][3] === counter)) {
                    // avgColor = shiftColor(avgColor, Math.round(colorShiftAmt * (1 - connectivity)));
                    points.tweened[i][4] = weightedAverageColor(points.tweened[i][4], points.tweened[j][4], connectivity);
                    points.tweened[j][4] = weightedAverageColor(points.tweened[j][4], points.tweened[i][4], connectivity);
                }
                avgColor = averageColor(points.tweened[i][4], points.tweened[j][4]);
                shadedColor = shadeColor(avgColor, connectivity);
                if (drawLines) {
                    polygon.lineStyle(lineSize, 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][4] = shiftColor(points.tweened[i][4], disconnectedColorShiftAmt);
        }

        if (drawNodes) {
            // draw nodes
            nodeDiameter = nodeSize;
            scale = nodeDiameter / nodeImgRes;
            sprites[i].scale.x = scale;
            sprites[i].scale.y = scale;
            sprites[i].x = points.tweened[i][0] - (nodeDiameter / 2);
            sprites[i].y = points.tweened[i][1] - (nodeDiameter / 2);
            sprites[i].tint = rgbToHex(points.tweened[i][4]);
        }
    }
}

/* MAIN LOOP */

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

    polygon.clear();

    if (reset === true) {
        var newPoints;
        newPoints = getRandomPoints(numPoints, screenWidth, screenHeight, zRange, tweeningFns);
        polygonPoints = {
            original: newPoints,
            target: JSON.parse(JSON.stringify(newPoints)),
            tweened: JSON.parse(JSON.stringify(newPoints))
        };
        reset = false;
    }

    if (click !== null) {
        if (clickEnd) {
            // apply "rebound" effects
            clickPullRate = clickPullRateEnd;
            clickInertia = clickInertiaEnd;
            clickTweeningFn = clickTweeningFnEnd;
            if (debug) {
                // draw debug click effect radius red color when clickEnd == true
                polygon.lineStyle(1, 0xDC143C, 1);
                polygon.drawCircle(click.x, click.y, clickMaxDist);
            }
        } else {
            if (debug) {
                // draw click effect radius blue when debug is on
                polygon.lineStyle(1, 0x483D8B, 1);
                polygon.drawCircle(click.x, click.y, clickMaxDist);
            }
        }

        // a pointer event is occuring and needs to affect the points in effect radius
        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) {
            // done with rebound effect, re-initialize everything to prepare for next click
            click = null;
            clickEnd = false;
            clickMaxDist = clickMaxDistStart;
            clickPullRate = clickPullRateStart;
            clickInertia = clickInertiaStart;
            clickTweeningFn = clickTweeningFnStart;
            clearAffectedPoints(polygonPoints);
        }
    } else if (hover !== null) {
        if (lastHover !== null) {
            // hover effect radius grows bigger the faster the mouse moves
            hoverMaxDist += Math.min(Math.round(distancePos(hover, lastHover)), hoverMaxDistMax);
        }
        if (debug) {
            // draw hover effect radius yellow when debug is on
            polygon.lineStyle(1, 0xBDB76B, 1);
            polygon.drawCircle(hover.x, hover.y, hoverMaxDist);
        }

        // a hover event is occuring and needs to affect the points in effect radius
        pullPoints(polygonPoints, hover, hoverPushRate, hoverInertia, hoverMaxDist, counter, false, hoverTweeningFn);

        hoverMaxDist = hoverMaxDistStart;
        lastHover = hover;
    }

    // TODO: it would be cool to fill in triangles
    // 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;
    }

    // points that have reached the end of their cycles need new targets
    polygonPoints = shiftPoints(polygonPoints, pointShiftDistance, counter, tweeningFns);

    // 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);

        // Update control inputs
        var timeRange = document.getElementsByName('timeRange')[0];
        var timeInput = document.getElementsByName('timeInput')[0];
        timeRange.value = cycleDuration;
        timeInput.value = cycleDuration;
    }

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

function loopStart () {
    screenWidth = document.body.clientWidth;
    screenHeight = document.body.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;
    connectionDistance = Math.min(Math.round(totalScreenPixels / 16), 75);
    pointShiftDistance = Math.round(totalScreenPixels / 45);
    clickMaxDistStart = Math.min(Math.round(totalScreenPixels / 20), clickMaxDistStart);
    hoverMaxDistStart = Math.min(Math.round(totalScreenPixels / 16), hoverMaxDistStart);
    polygon = new window.PIXI.Graphics();
    stage.addChild(polygon);
    numPoints = Math.round(totalScreenPixels / 6);
    startPoints = getRandomPoints(numPoints, screenWidth, screenHeight, zRange, tweeningFns);
    polygonPoints = {
        original: startPoints,
        target: JSON.parse(JSON.stringify(startPoints)),
        tweened: JSON.parse(JSON.stringify(startPoints))
    };

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

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

    lastLoop = new Date();

    scrollDelta = 0;

    // Try to fix bug where click initializes to a bogus value
    click = null;
    hover = null;

    window.requestAnimationFrame(loop);
}

// Use PIXI loader to load image and then start animation
window.PIXI.loader
    .add(nodeImg)
    .load(loopStart);

window.onload = function () {
    var canvas, tweeningInputs, debugCheckbox, fpsCheckbox, nodeCheckbox, linesCheckbox;
    canvas = document.getElementsByTagName('canvas')[0];
    canvas.setAttribute('tabindex', 0);
    tweeningInputs = document.getElementsByName('tweening');
    debugCheckbox = document.getElementsByName('debugToggle')[0];
    fpsCheckbox = document.getElementsByName('fpsCounterToggle')[0];
    nodesCheckbox = document.getElementsByName('nodesToggle')[0];
    linesCheckbox = document.getElementsByName('linesToggle')[0];

    /* MOUSE AND TOUCH EVENTS */

    window.addEventListener('mousewheel', function (e) {
        if (e.target.tagName !== 'CANVAS') return;
        scrollDelta = scrollDelta + ((e.deltaY / 100) * 3);
    });

    window.addEventListener('touchstart', function (e) {
        if (e.target.tagName !== 'CANVAS') return;
        e.target.focus();
        click = getMousePos(e.changedTouches[0], resolution);
        clickEnd = false;
    });

    window.addEventListener('touchmove', function (e) {
        if (e.target.tagName !== 'CANVAS') return;
        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) {
        if (e.target.tagName !== 'CANVAS') return;
        e.target.focus();
        click = getMousePos(e, resolution);
        clickEnd = false;
    });

    window.addEventListener('mousemove', function (e) {
        if (e.target.tagName !== 'CANVAS') return;
        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;
        clearAffectedPoints(polygonPoints);
    });

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

    /* KEYBOARD EVENTS */

    window.addEventListener('keydown', function (e) {
        var i;
        if (e.target.tagName === 'INPUT') return;
        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;
            tweeningInputs[0].checked = true;
        } else if (e.keyCode === 50) { // 2
            tweeningFns = tweeningSets.meandering;
            tweeningInputs[1].checked = true;
        } else if (e.keyCode === 51) { // 3
            tweeningFns = tweeningSets.snappy;
            tweeningInputs[2].checked = true;
        } else if (e.keyCode === 52) { // 4
            tweeningFns = tweeningSets.bouncy;
            tweeningInputs[3].checked = true;
        } else if (e.keyCode === 53) { // 5
            tweeningFns = tweeningSets.elastic;
            tweeningInputs[4].checked = true;
        } else if (e.keyCode === 54) { // 6
            tweeningFns = tweeningSets.back;
            tweeningInputs[5].checked = true;
        } else if (e.keyCode === 70) { // f
            toggleFPS();
        } else if (e.keyCode === 68) { // d
            toggleDebug();
        } else if (e.keyCode === 78) { // n
            toggleNodes();
        } else if (e.keyCode === 76) { // l
            toggleLines();
        } else if (e.keyCode === 191) { // ?
            toggleHelp();
        }
    });

    /* BUTTON & INPUT EVENTS */

    document.getElementById('toggle-help').addEventListener('click', function () {
        toggleHelp();
    }, false);

    document.getElementById('toggle-controls').addEventListener('click', function () {
        toggleControls();
    }, false);

    document.getElementById('close-help').addEventListener('click', function () {
        toggleHelp();
    }, false);

    document.getElementById('close-controls').addEventListener('click', function () {
        toggleControls();
    }, false);

    document.getElementById('synchronize-cycles').addEventListener('click', function () {
        synchronizeCycles(polygonPoints, cycleDuration);
    }, false);

    document.getElementById('randomize-cycles').addEventListener('click', function () {
        randomizeCycles(polygonPoints, cycleDuration);
    }, false);

    document.getElementById('reset').addEventListener('click', function () {
        reset = true;
    }, false);

    var connectDistRange = document.getElementsByName('connectDistRange')[0];
    connectDistRange.value = connectionDistance;
    connectDistRange.addEventListener('input', function (e) {
        connectionDistance = parseInt(this.value, 10);
    });

    var connectDistInput = document.getElementsByName('connectDistInput')[0];
    connectDistInput.value = connectionDistance;
    connectDistInput.addEventListener('input', function (e) {
        connectionDistance = parseInt(this.value, 10);
    });

    var connectLimitRange = document.getElementsByName('connectLimitRange')[0];
    connectLimitRange.value = connectionLimit;
    connectLimitRange.addEventListener('input', function (e) {
        connectionLimit = parseInt(this.value, 10);
    });

    var connectLimitInput = document.getElementsByName('connectLimitInput')[0];
    connectLimitInput.value = connectionLimit;
    connectLimitInput.addEventListener('input', function (e) {
        connectionLimit = parseInt(this.value, 10);
    });

    var pointsNumRange = document.getElementsByName('pointsNumRange')[0];
    pointsNumRange.value = numPoints;
    pointsNumRange.addEventListener('input', function (e) {
        numPoints = parseInt(this.value, 10);
    });

    var pointsNumInput = document.getElementsByName('pointsNumInput')[0];
    pointsNumInput.value = numPoints;
    pointsNumInput.addEventListener('input', function (e) {
        numPoints = parseInt(this.value, 10);
    });

    var maxTravelRange = document.getElementsByName('maxTravelRange')[0];
    maxTravelRange.value = pointShiftDistance;
    maxTravelRange.addEventListener('input', function (e) {
        pointShiftDistance = parseInt(this.value, 10);
    });

    var maxTravelInput = document.getElementsByName('maxTravelInput')[0];
    maxTravelInput.value = pointShiftDistance;
    maxTravelInput.addEventListener('input', function (e) {
        pointShiftDistance = parseInt(this.value, 10);
    });

    var timeRange = document.getElementsByName('timeRange')[0];
    timeRange.value = cycleDuration;
    timeRange.addEventListener('input', function (e) {
        var oldCycleDuration = cycleDuration;
        cycleDuration = parseInt(this.value, 10);
        polygonPoints = redistributeCycles(polygonPoints, oldCycleDuration, cycleDuration);
    });

    var timeInput = document.getElementsByName('timeInput')[0];
    timeInput.value = cycleDuration;
    timeInput.addEventListener('input', function (e) {
        var oldCycleDuration = cycleDuration;
        if (this.value === '' || this.value === '0') return;
        cycleDuration = parseInt(this.value, 10);
        polygonPoints = redistributeCycles(polygonPoints, oldCycleDuration, cycleDuration);
    });

    var i;
    for (i = 0; i < tweeningInputs.length; i++) {
        tweeningInputs[i].addEventListener('change', function (e) {
            tweeningFns = tweeningSets[this.value];
        });
    }

    var pointSizeRange = document.getElementsByName('pointSizeRange')[0];
    pointSizeRange.value = nodeSize;
    pointSizeRange.addEventListener('input', function (e) {
        nodeSize = parseInt(this.value, 10);
    });

    var pointSizeInput = document.getElementsByName('pointSizeInput')[0];
    pointSizeInput.value = nodeSize;
    pointSizeInput.addEventListener('input', function (e) {
        nodeSize = parseInt(this.value, 10);
    });

    var lineSizeRange = document.getElementsByName('lineSizeRange')[0];
    lineSizeRange.value = lineSize;
    lineSizeRange.addEventListener('input', function (e) {
        lineSize = parseInt(this.value, 10);
    });

    var lineSizeInput = document.getElementsByName('lineSizeInput')[0];
    lineSizeInput.value = lineSize;
    lineSizeInput.addEventListener('input', function (e) {
        lineSize = parseInt(this.value, 10);
    });

    var colorShiftRange = document.getElementsByName('colorShiftRange')[0];
    colorShiftRange.value = disconnectedColorShiftAmt;
    colorShiftRange.addEventListener('input', function (e) {
        disconnectedColorShiftAmt = parseInt(this.value, 10);
    });

    var colorShiftInput = document.getElementsByName('colorShiftInput')[0];
    colorShiftInput.value = disconnectedColorShiftAmt;
    colorShiftInput.addEventListener('input', function (e) {
        disconnectedColorShiftAmt = parseInt(this.value, 10);
    });


    debugCheckbox.addEventListener('change', function (e) {
        toggleDebug();
    });

    fpsCheckbox.addEventListener('change', function (e) {
        toggleFPS();
    });

    nodesCheckbox.addEventListener('change', function (e) {
        toggleNodes();
    });

    linesCheckbox.addEventListener('change', function (e) {
        toggleLines();
    });
};
