proximity-structures/js/proximity.js

1154 lines
42 KiB
JavaScript
Raw Permalink Normal View History

2017-08-04 04:50:37 +00:00
/* 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
2017-08-01 03:59:24 +00:00
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;
2017-08-13 06:46:51 +00:00
var numPoints;
2017-08-04 04:50:37 +00:00
// global non-configurable vars (modifying these might break stuff)
var click = null;
var hover = null;
var lastHover = null;
var clickEnd = false;
var sprites = [];
2017-08-13 05:15:24 +00:00
var reset = false;
2017-08-04 04:50:37 +00:00
// 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
];
2017-08-04 04:50:37 +00:00
// 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],
2017-08-11 04:20:02 +00:00
back: [24]
};
var tweeningFns = tweeningSets.back; // the actual set of tweening functions points will randomly choose from
2017-08-04 04:50:37 +00:00
// 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
2017-08-01 05:34:53 +00:00
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)
2017-08-04 04:50:37 +00:00
var clickMaxDistInc = 2; // amount to increase clickMaxDist every tick that a click is held
var clickMaxDistMax = 5000; // maximum value of clickMaxDist
2017-08-01 05:34:53 +00:00
var clickMaxDist = clickMaxDistStart;
2017-08-04 04:50:37 +00:00
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;
2017-08-04 04:50:37 +00:00
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;
2017-08-04 04:50:37 +00:00
var clickColorShiftAmt = disconnectedColorShiftAmt * 3; // amount of RGB color value to shift for each point in effect radius
2017-08-04 15:30:12 +00:00
var clickPullRateEnd = -0.5; // value of clickPullRate during tick after end of click (for "rebound" effect)
2017-08-04 04:50:37 +00:00
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)
2017-08-04 04:50:37 +00:00
var hoverMaxDistMax = 1000; // maximum value of hoverMaxDist
var hoverMaxDist = hoverMaxDistStart;
2017-08-04 04:50:37 +00:00
var hoverTweeningFn = 5; // specific tweening function to assign to points in effect radius
2017-08-11 04:14:33 +00:00
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
2017-08-11 04:14:33 +00:00
var nodeImgRes = 100; // resolution of nodeImg file in pixels (aspect ratio should be square)
2017-08-13 07:20:53 +00:00
// 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
2017-08-11 04:14:33 +00:00
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
2017-08-13 04:24:56 +00:00
var lineSize = 1; // thickness in pixels of drawn lines between points
2017-08-04 04:50:37 +00:00
/* 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
2017-08-01 03:59:24 +00:00
return ((c * t) / d) + b;
}
function easeOutBounce (t, b, c, d) {
2017-08-01 03:59:24 +00:00
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) {
2017-08-01 03:59:24 +00:00
return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;
}
function easeOutSine (t, b, c, d) {
2017-08-01 03:59:24 +00:00
return c * Math.sin(t / d * (Math.PI / 2)) + b;
}
function easeInOutSine (t, b, c, d) {
2017-08-01 03:59:24 +00:00
return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
}
function easeInQuad (t, b, c, d) {
2017-08-01 03:59:24 +00:00
return c * (t /= d) * t + b;
}
function easeOutQuad (t, b, c, d) {
2017-08-01 03:59:24 +00:00
return -c * (t /= d) * (t - 2) + b;
}
function easeInOutQuad (t, b, c, d) {
2017-08-01 03:59:24 +00:00
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) {
2017-08-01 03:59:24 +00:00
return c * (t /= d) * t * t + b;
}
function easeOutCubic (t, b, c, d) {
2017-08-01 03:59:24 +00:00
return c * ((t = t / d - 1) * t * t + 1) + b;
}
function easeInOutCubic (t, b, c, d) {
2017-08-01 03:59:24 +00:00
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) {
2017-08-01 03:59:24 +00:00
return (t === 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
}
function easeOutExpo (t, b, c, d) {
2017-08-01 03:59:24 +00:00
return (t === d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
}
function easeInOutExpo (t, b, c, d) {
2017-08-01 03:59:24 +00:00
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) {
2017-08-01 03:59:24 +00:00
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) {
2017-08-01 03:59:24 +00:00
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) {
2017-08-01 03:59:24 +00:00
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) {
2017-08-01 03:59:24 +00:00
return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
}
function easeOutCirc (t, b, c, d) {
2017-08-01 03:59:24 +00:00
return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
}
function easeInOutCirc (t, b, c, d) {
2017-08-01 03:59:24 +00:00
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;
}
2017-08-01 03:59:24 +00:00
/* eslint-enable no-unused-vars */
/* TOGGLE FUNCTIONS */
2017-08-04 04:50:37 +00:00
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';
}
}
2017-08-13 03:15:24 +00:00
function toggleFPS () {
2017-08-13 04:24:56 +00:00
var fpsCheckbox = document.getElementsByName('fpsCounterToggle')[0];
2017-08-13 03:15:24 +00:00
if (fpsEnabled) {
stage.removeChild(fpsGraphic);
fpsEnabled = false;
} else {
stage.addChild(fpsGraphic);
fpsEnabled = true;
lastLoop = new Date();
}
fpsCheckbox.checked = fpsEnabled;
}
function toggleDebug () {
2017-08-13 04:24:56 +00:00
var fpsCheckbox = document.getElementsByName('fpsCounterToggle')[0];
var debugCheckbox = document.getElementsByName('debugToggle')[0];
2017-08-13 03:15:24 +00:00
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;
}
2017-08-13 04:24:56 +00:00
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 */
2017-08-04 04:50:37 +00:00
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) {
2017-08-01 03:59:24 +00:00
return parseInt(((1 << 24) + (color.r << 16) + (color.g << 8) + color.b).toString(16).slice(1), 16);
}
/* Choose a random RGB color */
function randomColor () {
2017-08-01 03:59:24 +00:00
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) {
2017-08-01 03:59:24 +00:00
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) {
2017-08-01 03:59:24 +00:00
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) {
2017-08-01 03:59:24 +00:00
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) {
2017-08-01 03:59:24 +00:00
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) {
2017-08-01 03:59:24 +00:00
return {
r: shiftColorComponent(color.r, maxShiftAmt),
g: shiftColorComponent(color.g, maxShiftAmt),
b: shiftColorComponent(color.b, maxShiftAmt)
};
}
2017-08-01 05:34:53 +00:00
/* 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)
};
}
}
2017-08-04 04:50:37 +00:00
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) {
2017-08-04 04:50:37 +00:00
// 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);
2017-08-01 05:34:53 +00:00
}
2017-08-04 04:50:37 +00:00
// 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;
}
2017-08-04 04:50:37 +00:00
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
);
}
function clearSprites () {
2017-08-13 06:46:51 +00:00
if (sprites.length > 0) {
// need to clear out old sprites
for (i = 0; i < sprites.length; i++) {
stage.removeChild(sprites[i]);
}
sprites = [];
}
}
/* POINT OPERATION FUNCTIONS */
function getRandomPoints (numPoints, maxX, maxY, maxZ, tweeningFns) {
var i, x, y, z, color, cycleStart, easingFn, sprite;
var points = [];
2017-08-01 03:59:24 +00:00
for (i = 0; i < numPoints; i++) {
x = randomInt(0, maxX - 1);
y = randomInt(0, maxY - 1);
2017-08-11 04:14:33 +00:00
// 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
2017-08-01 03:59:24 +00:00
cycleStart = randomInt(0, cycleDuration - 1);
color = randomColor();
easingFn = tweeningFns[Math.floor(Math.random() * tweeningFns.length)];
2017-08-13 06:46:51 +00:00
// save PIXI Sprite for each point in array
sprite = createSprite();
2017-08-13 07:09:19 +00:00
if (!drawNodes) sprite.visible = false;
2017-08-13 06:46:51 +00:00
sprites.push(sprite);
stage.addChild(sprite);
points[i] = [x, y, z, cycleStart, color, easingFn];
2017-08-01 03:59:24 +00:00
}
return points;
}
function addOrRemovePoints (numPoints, points) {
/* Given new value numPoints, remove or add new random points to match new count. */
var deletedSprites, newPoints;
if (points.target.length > numPoints) {
points.original.splice(numPoints - 1);
points.target.splice(numPoints - 1);
points.tweened.splice(numPoints - 1);
deletedSprites = sprites.splice(numPoints - 1);
for (var i = 0; i < deletedSprites.length; i++) {
stage.removeChild(deletedSprites[i]);
}
} else if (points.target.length < numPoints) {
newPoints = getRandomPoints(numPoints - points.target.length, screenWidth, screenHeight, zRange, tweeningFns);
points.original = points.original.concat(newPoints);
points.target = points.target.concat(JSON.parse(JSON.stringify(newPoints)));
points.tweened = points.tweened.concat(JSON.parse(JSON.stringify(newPoints)));
}
return points;
}
function shiftPoints (points, maxShiftAmt, counter, tweeningFns) {
2017-08-01 03:59:24 +00:00
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);
2017-08-01 03:59:24 +00:00
}
if (points.target[i][3] === counter) {
2017-08-01 03:59:24 +00:00
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);
2017-08-01 03:59:24 +00:00
}
}
// clear pointShiftBiases now that they have been "used"
2017-08-01 03:59:24 +00:00
pointShiftBiasX = 0;
pointShiftBiasY = 0;
2017-08-01 03:59:24 +00:00
return points;
}
function pullPoints (points, clickPos, pullRate, inertia, maxDist, counter, resetPoints, tweeningFn) {
2017-08-04 04:50:37 +00:00
var targetXDiff, targetYDiff, originXDiff, originYDiff;
2017-08-01 05:34:53 +00:00
for (var i = 0; i < points.target.length; i++) {
2017-08-04 04:50:37 +00:00
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];
2017-08-13 06:46:51 +00:00
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];
}
2017-08-04 04:50:37 +00:00
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) {
2017-08-04 04:50:37 +00:00
// 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
}
}
2017-08-01 05:34:53 +00:00
}
}
2017-08-13 06:46:51 +00:00
function clearAffectedPoints (points) {
for (var i = 0; i < points.target.length; i++) {
points.target[i][6] = false;
}
}
function redistributeCycles (points, oldCycleDuration, cycleDuration) {
2017-08-04 04:50:37 +00:00
/* 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;
2017-08-01 03:59:24 +00:00
for (var i = 0; i < points.original.length; i++) {
progress = points.target[i][3] / oldCycleDuration;
points.target[i][3] = Math.round(progress * cycleDuration);
2017-08-01 03:59:24 +00:00
}
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;
}
2017-08-04 04:50:37 +00:00
/* DRAW FUNCTIONS */
function drawPolygon (polygon, points, counter, tweeningFns) {
2017-08-11 04:14:33 +00:00
var i, j, easingFn, relativeCount, avgColor, shadedColor, connectionCount, dist, connectivity, scale, nodeDiameter;
// calculate vectors
2017-08-01 03:59:24 +00:00
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)
);
}
}
2017-08-01 03:59:24 +00:00
}
// draw lines
2017-08-01 03:59:24 +00:00
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.
2017-08-01 03:59:24 +00:00
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);
2017-08-01 03:59:24 +00:00
}
avgColor = averageColor(points.tweened[i][4], points.tweened[j][4]);
2017-08-01 03:59:24 +00:00
shadedColor = shadeColor(avgColor, connectivity);
2017-08-11 04:14:33 +00:00
if (drawLines) {
2017-08-13 04:24:56 +00:00
polygon.lineStyle(lineSize, rgbToHex(shadedColor), 1);
2017-08-11 04:14:33 +00:00
polygon.moveTo(points.tweened[i][0], points.tweened[i][1]);
polygon.lineTo(points.tweened[j][0], points.tweened[j][1]);
}
2017-08-01 03:59:24 +00:00
connectionCount = connectionCount + 1;
}
}
2017-08-01 03:59:24 +00:00
if (connectionCount === 0) {
points.tweened[i][4] = shiftColor(points.tweened[i][4], disconnectedColorShiftAmt);
2017-08-01 03:59:24 +00:00
}
2017-08-11 04:14:33 +00:00
if (drawNodes) {
// draw nodes
2017-08-13 07:20:53 +00:00
nodeDiameter = nodeSize;
2017-08-11 04:14:33 +00:00
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]);
}
2017-08-01 03:59:24 +00:00
}
}
2017-08-04 04:50:37 +00:00
/* MAIN LOOP */
function loop () {
screenWidth = window.innerWidth;
screenHeight = window.innerHeight;
2017-08-01 03:59:24 +00:00
renderer.resize(screenWidth, screenHeight);
2017-08-01 03:59:24 +00:00
polygon.clear();
2017-08-13 05:15:24 +00:00
if (reset === true) {
var newPoints;
clearSprites();
2017-08-13 06:46:51 +00:00
newPoints = getRandomPoints(numPoints, screenWidth, screenHeight, zRange, tweeningFns);
2017-08-13 05:15:24 +00:00
polygonPoints = {
original: newPoints,
target: JSON.parse(JSON.stringify(newPoints)),
tweened: JSON.parse(JSON.stringify(newPoints))
};
reset = false;
}
2017-08-01 05:34:53 +00:00
if (click !== null) {
if (clickEnd) {
2017-08-04 04:50:37 +00:00
// apply "rebound" effects
clickPullRate = clickPullRateEnd;
clickInertia = clickInertiaEnd;
clickTweeningFn = clickTweeningFnEnd;
if (debug) {
2017-08-04 04:50:37 +00:00
// 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) {
2017-08-04 04:50:37 +00:00
// draw click effect radius blue when debug is on
polygon.lineStyle(1, 0x483D8B, 1);
polygon.drawCircle(click.x, click.y, clickMaxDist);
}
2017-08-01 05:34:53 +00:00
}
2017-08-04 04:50:37 +00:00
// a pointer event is occuring and needs to affect the points in effect radius
pullPoints(polygonPoints, click, clickPullRate, clickInertia, clickMaxDist, counter, clickEnd, clickTweeningFn);
2017-08-01 05:34:53 +00:00
// slightly increase effect amount for next loop if click is still occuring
if (clickMaxDist <= clickMaxDistMax) {
clickMaxDist += clickMaxDistInc;
}
if (clickPullRate <= clickPullRateMax) {
clickPullRate += clickPullRateInc;
}
if (clickEnd) {
2017-08-04 04:50:37 +00:00
// done with rebound effect, re-initialize everything to prepare for next click
2017-08-01 05:34:53 +00:00
click = null;
clickEnd = false;
clickMaxDist = clickMaxDistStart;
clickPullRate = clickPullRateStart;
clickInertia = clickInertiaStart;
clickTweeningFn = clickTweeningFnStart;
2017-08-13 06:46:51 +00:00
clearAffectedPoints(polygonPoints);
2017-08-01 05:34:53 +00:00
}
} else if (hover !== null) {
if (lastHover !== null) {
2017-08-04 04:50:37 +00:00
// hover effect radius grows bigger the faster the mouse moves
hoverMaxDist += Math.min(Math.round(distancePos(hover, lastHover)), hoverMaxDistMax);
}
if (debug) {
2017-08-04 04:50:37 +00:00
// draw hover effect radius yellow when debug is on
polygon.lineStyle(1, 0xBDB76B, 1);
polygon.drawCircle(hover.x, hover.y, hoverMaxDist);
}
2017-08-04 04:50:37 +00:00
// 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;
2017-08-01 05:34:53 +00:00
}
2017-08-04 04:50:37 +00:00
// TODO: it would be cool to fill in triangles
// polygon.beginFill(0x00FF00);
2017-08-01 03:59:24 +00:00
drawPolygon(polygon, polygonPoints, counter, tweeningFns);
// polygon.endFill();
2017-08-01 03:59:24 +00:00
counter += 1;
counter = counter % cycleDuration;
2017-08-01 03:59:24 +00:00
if (counter === 0 && fpsEnabled) {
thisLoop = new Date();
fps = Math.round((1000 / (thisLoop - lastLoop)) * cycleDuration);
fpsGraphic.setText(fps.toString());
lastLoop = thisLoop;
}
2017-08-04 04:50:37 +00:00
// points that have reached the end of their cycles need new targets
2017-08-01 03:59:24 +00:00
polygonPoints = shiftPoints(polygonPoints, pointShiftDistance, counter, tweeningFns);
// If user scrolled, modify cycleDuration by amount scrolled
2017-08-01 03:59:24 +00:00
if (scrollDelta !== 0) {
var oldCycleDuration = cycleDuration;
cycleDuration = Math.round(cycleDuration + scrollDelta);
2017-08-01 03:59:24 +00:00
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`
2017-08-01 03:59:24 +00:00
renderer.render(stage);
window.requestAnimationFrame(loop);
}
function registerEventHandlers() {
var tweeningInputs, debugCheckbox, fpsCheckbox, nodeCheckbox, linesCheckbox;
tweeningInputs = document.getElementsByName('tweening');
2017-08-13 04:24:56 +00:00
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('wheel', function (e) {
if (e.target.tagName !== 'CANVAS') return;
scrollDelta = scrollDelta + e.deltaY;
});
window.addEventListener('touchstart', function (e) {
if (e.target.tagName !== 'CANVAS') return;
2017-08-13 04:24:56 +00:00
e.target.focus();
click = getMousePos(e.changedTouches[0], resolution);
clickEnd = false;
});
2017-08-01 05:34:53 +00:00
window.addEventListener('touchmove', function (e) {
if (e.target.tagName !== 'CANVAS') return;
if (click !== null) {
click = getMousePos(e.changedTouches[0], resolution);
2017-08-01 03:59:24 +00:00
}
});
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;
2017-08-13 04:24:56 +00:00
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;
2017-08-13 06:46:51 +00:00
clearAffectedPoints(polygonPoints);
});
document.addEventListener('mouseleave', function (e) {
clickEnd = true;
hover = null;
lastHover = null;
2017-08-13 06:46:51 +00:00
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
2017-08-13 03:15:24 +00:00
toggleFPS();
} else if (e.keyCode === 68) { // d
2017-08-13 03:15:24 +00:00
toggleDebug();
} else if (e.keyCode === 78) { // n
2017-08-13 04:24:56 +00:00
toggleNodes();
} else if (e.keyCode === 76) { // l
2017-08-13 04:24:56 +00:00
toggleLines();
} else if (e.keyCode === 191) { // ?
toggleHelp();
2017-08-11 05:24:18 +00:00
}
});
/* 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);
2017-08-13 05:15:24 +00:00
}, false);
document.getElementById('reset').addEventListener('click', function () {
reset = true;
}, false);
2017-08-13 06:46:51 +00:00
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);
polygonPoints = addOrRemovePoints(numPoints, polygonPoints);
2017-08-13 06:46:51 +00:00
});
var pointsNumInput = document.getElementsByName('pointsNumInput')[0];
pointsNumInput.value = numPoints;
pointsNumInput.addEventListener('input', function (e) {
numPoints = parseInt(this.value, 10);
polygonPoints = addOrRemovePoints(numPoints, polygonPoints);
2017-08-13 06:46:51 +00:00
});
2017-08-13 07:09:19 +00:00
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);
});
2017-08-13 06:46:51 +00:00
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);
});
2017-08-13 06:46:51 +00:00
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];
});
2017-08-01 03:24:09 +00:00
}
2017-08-13 03:15:24 +00:00
2017-08-13 06:46:51 +00:00
var pointSizeRange = document.getElementsByName('pointSizeRange')[0];
2017-08-13 04:24:56 +00:00
pointSizeRange.value = nodeSize;
pointSizeRange.addEventListener('input', function (e) {
nodeSize = parseInt(this.value, 10);
});
2017-08-13 06:46:51 +00:00
var pointSizeInput = document.getElementsByName('pointSizeInput')[0];
2017-08-13 04:24:56 +00:00
pointSizeInput.value = nodeSize;
pointSizeInput.addEventListener('input', function (e) {
nodeSize = parseInt(this.value, 10);
});
2017-08-13 06:46:51 +00:00
var lineSizeRange = document.getElementsByName('lineSizeRange')[0];
2017-08-13 04:24:56 +00:00
lineSizeRange.value = lineSize;
lineSizeRange.addEventListener('input', function (e) {
lineSize = parseInt(this.value, 10);
});
2017-08-13 07:09:19 +00:00
var lineSizeInput = document.getElementsByName('lineSizeInput')[0];
lineSizeInput.value = lineSize;
lineSizeInput.addEventListener('input', function (e) {
2017-08-13 04:24:56 +00:00
lineSize = parseInt(this.value, 10);
});
2017-08-13 06:46:51 +00:00
var colorShiftRange = document.getElementsByName('colorShiftRange')[0];
colorShiftRange.value = disconnectedColorShiftAmt;
colorShiftRange.addEventListener('input', function (e) {
disconnectedColorShiftAmt = parseInt(this.value, 10);
});
2017-08-13 07:09:19 +00:00
var colorShiftInput = document.getElementsByName('colorShiftInput')[0];
colorShiftInput.value = disconnectedColorShiftAmt;
colorShiftInput.addEventListener('input', function (e) {
disconnectedColorShiftAmt = parseInt(this.value, 10);
});
2017-08-13 04:24:56 +00:00
2017-08-13 03:15:24 +00:00
debugCheckbox.addEventListener('change', function (e) {
toggleDebug();
});
fpsCheckbox.addEventListener('change', function (e) {
toggleFPS();
});
2017-08-13 04:24:56 +00:00
nodesCheckbox.addEventListener('change', function (e) {
toggleNodes();
});
linesCheckbox.addEventListener('change', function (e) {
toggleLines();
});
}
function loopStart () {
screenWidth = window.innerWidth;
screenHeight = window.innerHeight;
// 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;
registerEventHandlers();
window.requestAnimationFrame(loop);
}
// Use PIXI loader to load image and then start animation
window.PIXI.loader
.add(nodeImg)
.load(loopStart);