Last active
August 20, 2017 12:41
-
-
Save taylorchasewhite/f5ede58351f8e7e5e3cd31cfc1ce2e7e to your computer and use it in GitHub Desktop.
Flocking With Sperm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<meta charset="utf-8" /> | |
<head> | |
<style> | |
body { | |
background: #000; | |
} | |
ellipse { | |
fill: #fff; | |
} | |
path { | |
fill: none; | |
stroke: #fff; | |
stroke-linecap: round; | |
} | |
.mid { | |
stroke-width: 4px; | |
} | |
.tail { | |
stroke-width: 2px; | |
} | |
.btn { | |
margin-top: 3px; | |
margin-bottom: 5px; | |
position: relative; | |
vertical-align: bottom; | |
width: 115px; | |
height: 35px; | |
font-size: 18px; | |
color: white; | |
padding: 5px; | |
text-align: center; | |
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); | |
border: 0; | |
border-radius: 3px; | |
cursor: pointer; | |
} | |
.btnRed { | |
background-color: #e74c3c; | |
border-bottom: 2px solid #d62c1a; | |
-webkit-box-shadow: inset 0 -2px #d62c1a; | |
box-shadow: inset 0 -2px #d62c1a; | |
} | |
/* http://codepen.io/Varo/full/gbzpmw */ | |
.btnGreen { | |
background: #2ecc71; | |
border-bottom: 2px solid #28be68; | |
-webkit-box-shadow: inset 0 -2px #28be68; | |
box-shadow: inset 0 -2px #28be68; | |
} | |
.btnBlue { | |
background: #3498db; | |
border-bottom: 2px solid #2a8bcc; | |
-webkit-box-shadow: inset 0 -2px #2a8bcc; | |
box-shadow: inset 0 -2px #2a8bcc; | |
} | |
</style> | |
<script type="text/javascript" src="https://d3js.org/d3.v3.min.js"></script> | |
<script type="text/javascript" src="sperm.js"></script> | |
</head> | |
<body> | |
<button id="btnAdd" class="btn btnGreen" type="button" title="There are supposed to be sperm jokes here.">Deposit</button> | |
<button id="btnRemove" class="btn btnRed" style="visibility:hidden;" type="button" title="Do it quickly! There's no time!">Withdraw</button> | |
<script type="text/javascript">spermInitialize();</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Title: sperm.js | |
* Description: | |
* This was grabbed from Mike Bostock's site. He challenged anyone up to the challenge | |
* to incorporate Boid's flocking algorithm to this sperm visualization, and I'm taking | |
* him up on it. | |
* Author: Taylor White | |
* Date: 11/08/2015 2:17 AM | |
* | |
*/ | |
// Global vars | |
var counter, degrees, g, head, height, jokes, n, m, spermatozoa, svg, tail, width; | |
/** | |
* Kick off the rendering of the DOM with sperm. | |
* @public | |
* | |
*/ | |
function spermInitialize() { | |
initializeGlobalVars(100, 12, 180, 960, 500) | |
initializeJokes(); | |
initializeDom(); | |
} | |
/** | |
* Instantiate the global variables with various settings desired | |
* | |
* @param {number} number - Desired number of sperm to be rendered | |
* @param {number} paths - The number of different directions they can take | |
* @param {number} degreesVar - The direction they should bounce off the wall with (maybe deprecate this) | |
* @param {number} svgWidth - The width of the SVG element | |
* @param {number} svgHeight - The height of the SVG element | |
*/ | |
function initializeGlobalVars(number, paths, degreesVar, svgWidth, svgHeight) { | |
n = number; // number of sperm | |
m = paths; // 12 different paths they can take? | |
degrees = degreesVar / Math.PI; // | |
jokes = []; | |
counter = 0; | |
width = svgWidth; | |
height = svgHeight; | |
} | |
function initializeDom() { | |
// add to DOM | |
svg = d3.select("body").append("svg") | |
.attr("width", width) | |
.attr("height", height); | |
renderAllSperm(); | |
addClickHandlers(); | |
} | |
/** | |
* At this point there are no groups (g)s, that's okay, that's how d3 works | |
* populate each group (g) with a value from spermatozoa. and add a g for each. | |
* | |
*/ | |
function renderAllSperm() { | |
spermatozoa = spermatozoaFunc(); | |
g = svg.selectAll("g") | |
.data(spermatozoa) | |
.enter().append("g"); | |
head = g.append("ellipse") // create a head for the group (g) | |
.attr("rx", 6.5) // slightly "longer" | |
.attr("ry", 4); // not as wide | |
g.append("path") // add the path array to the group. | |
.datum(function (d) { return d.path.slice(0, 3); }) // | |
.attr("class", "mid"); // this is the section between tail and head | |
g.append("path") | |
.datum(function (d) { return d.path; }) // tail | |
.attr("class", "tail"); | |
tail = g.selectAll("path"); | |
makeSpermSwim(); | |
} | |
/** | |
* Start the timer to make each of the sperm move in a direction | |
* @private | |
*/ | |
function makeSpermSwim() { | |
d3.timer(function () { | |
counter++; | |
counter %= n; | |
for (var i = -1; ++i < n;) { | |
var spermatozoon = spermatozoa[i]; | |
id = spermatozoon.id; | |
if ((counter - id) === 0) { | |
spermatozoon.headChange = (!spermatozoon.headChange); | |
} | |
steer = spermatozoon.separate(spermatozoa); | |
align = spermatozoon.align(spermatozoa); | |
sum = spermatozoon.cohesion(spermatozoa); | |
path = spermatozoon.path; | |
dx = spermatozoon.vx + align[0] + sum[0] + steer[0]; | |
dy = spermatozoon.vy + align[1] + sum[1] + steer[1]; | |
x = path[0][0] += dx; | |
y = path[0][1] += dy; | |
speed = Math.sqrt(dx * dx + dy * dy); | |
count = speed * 10; | |
k1 = -5 - speed / 3; | |
// Bounce off the walls. | |
if (x < 0 || x > width) spermatozoon.vx *= -1; | |
if (y < 0 || y > height) spermatozoon.vy *= -1; | |
// Swim! | |
for (var j = 0; ++j < m;) { | |
var vx = x - path[j][0], | |
vy = y - path[j][1], | |
k2 = Math.sin(((spermatozoon.count += count) + j * 3) / 300) / speed; | |
path[j][0] = (x += dx / speed * k1) - dy * k2; | |
path[j][1] = (y += dy / speed * k1) + dx * k2; | |
speed = Math.sqrt((dx = vx) * dx + (dy = vy) * dy); | |
} | |
} | |
head.attr("transform", headTransform); | |
tail.attr("d", tailPath); | |
}); | |
} | |
/** | |
* Add jokes to the jokes variable | |
* @private | |
*/ | |
function initializeJokes() { | |
jokes.push("Two sperms are having a race. One sperm says, \"Fuck me all this swimming is knackering me, how long till we reach the womb?\" \nThe second sperm says, \"Fucking long way to go yet mate - we've only just gone past her tonsils!\""); | |
jokes.push("A girl takes a dress into the dry cleaners and asks for it to be cleaned.\nThe man, who is a little deaf, says, \"Come again?\"The girl blushes and replies, \"No, it's yoghurt this time.\""); | |
jokes.push("Why are men like sperm cells? Only one out of a million is useful."); | |
jokes.push("I had an appointment at the sperm bank today, but I had to call up to say I couldn't come."); | |
jokes.push("I read that eating bananas makes your spunk taste nicer, so I've been eating about 20 every day.\nThere's been a real improvement in the customer feedback reviews at the Burger King where I work."); | |
jokes.push("I was so embarrassed when I spilled a pint down myself.\nThe woman at the sperm bank asked, \"Christ, how long have you gone without a wank?\""); | |
jokes.push("I'm too lazy to look for more sperm jokes. (This is not a joke)"); | |
} | |
/** | |
* Add click handlers to elements in the DOM needing them | |
* @private | |
*/ | |
function addClickHandlers() { | |
d3.select("button").on("click", clickHandler); | |
d3.select("#btnRemove").on("click", removeHandler); | |
} | |
/** | |
* Handle adding new sperm on the button click of the sperm deposit | |
* @private | |
*/ | |
function clickHandler() { | |
var sperm = newSperm(); | |
spermatozoa.push(sperm); | |
g.remove(); | |
g = svg.selectAll("g") | |
.data(spermatozoa) | |
.enter().append("g"); | |
head = g.append("ellipse") // create a head for the group (g) | |
.attr("rx", 6.5) // slightly "longer" | |
.attr("ry", 4); // not as wide | |
g.append("path") // add the path array to the group. | |
.datum(function (d) { return d.path.slice(0, 3); }) // | |
.attr("class", "mid"); // this is the section between tail and head | |
g.append("path") | |
.datum(function (d) { return d.path; }) // tail | |
.attr("class", "tail"); | |
tail = g.selectAll("path"); | |
} | |
/** | |
* Change the title of the button to be a different sperm joke | |
* @public | |
*/ | |
function changeBtnJoke() { | |
document.getElementById("btnAdd").setAttribute("title", jokes[Math.floor(n % jokes.length)]); | |
} | |
/** | |
* Add the handler to remove these guys if necessary | |
* @private | |
*/ | |
function removeHandler() { | |
spermatozoa.pop(); | |
g.remove(); | |
g = svg.selectAll("g") | |
.data(spermatozoa) | |
.enter().append("g"); | |
head = g.append("ellipse") // create a head for the group (g) | |
.attr("rx", 6.5) // slightly "longer" | |
.attr("ry", 4); // not as wide | |
g.append("path") // add the path array to the group. | |
.datum(function (d) { return d.path.slice(0, 3); }) // | |
.attr("class", "mid"); // this is the section between tail and head | |
g.append("path") | |
.datum(function (d) { return d.path; }) // tail | |
.attr("class", "tail"); | |
tail = g.selectAll("path"); | |
} | |
/** | |
* | |
* | |
* @returns | |
*/ | |
function newSperm() { | |
var coordinates = [0, 0]; | |
var x = width / 2, // starting X | |
y = 250; // starting Y | |
n += 1; | |
changeBtnJoke(); | |
return { | |
id: n + 1, | |
headChange: true, | |
vx: (Math.random() * 2 - 1) * 2, // vector (direction) x | |
vy: (Math.random() * 2 - 1) * 2, // vector (direction) y | |
path: d3.range(m).map(function () { return [x, y]; }), // not sure | |
count: 0, // not sure | |
// A method that calculates a steering vector towards a target | |
// Takes a second argument, if true, it slows down as it approaches | |
// the target | |
steer: function (target, slowdown) { | |
var steer = [0, 0], desired = [0, 0]; | |
desired[0] = target[0] - this.path[0][0]; | |
desired[1] = target[1] - this.path[0][1]; | |
var distance = 0//desired.length; | |
// Two options for desired vector magnitude | |
// (1 -- based on distance, 2 -- maxSpeed) | |
if (slowdown && distance < 100) { | |
// This damping is somewhat arbitrary: | |
//desired.length = this.maxSpeed * (distance / 100); | |
} else { | |
//desired.length = this.maxSpeed; | |
} | |
normalize(desired, 1.25); | |
var tempPoint; | |
tempPoint = [this.path[0][0], this.path[0][1]]; | |
normalize(tempPoint, 1); | |
steer[0] = desired[0] - tempPoint[0]; | |
steer[1] = desired[1] - tempPoint[1]; | |
//steer.length = Math.min(this.maxForce, steer.length); | |
normalize(steer, 1); | |
return steer; | |
}, | |
separate: function (boids) { | |
var desiredSeperation = 30; | |
var steer = [0, 0]; | |
var count = 0; | |
// For every boid in the system, check if it's too close | |
for (var i = 0, l = boids.length; i < l; i++) { | |
var other = boids[i]; | |
var vector = [0, 0]; | |
vector[0] = this.path[0][0] - other.path[0][0]; | |
vector[1] = this.path[0][1] - other.path[0][1]; | |
var distance = getDistance(this.path[0], other.path[0]); | |
if (distance > 0 && distance < desiredSeperation) { | |
// Calculate vector pointing away from neighbor | |
//normalize(vector,1); | |
//steer[0] += vector[0]*(1 / distance); // TODO Normalize | |
vector[0] = vector[0] * (1 / distance); | |
vector[1] = vector[1] * (1 / distance); | |
normalize(vector, .15); | |
steer[0] += vector[0]; | |
steer[1] += vector[1]; | |
//steer[1] += vector[1]*(1 / distance); // TODO Normalize | |
count++; | |
} | |
} | |
// Average -- divide by how many | |
if (count > 0) { | |
steer[0] /= count; | |
steer[1] /= count; | |
} | |
if (!steer[0] === 0 && !steer[1] === 0) { | |
// Implement Reynolds: Steering = Desired - Velocity | |
//steer.length = this.maxSpeed; | |
//steer -= this.vector; | |
//steer.length = Math.min(steer.length, this.maxForce); | |
} | |
normalize(steer, 1) | |
return steer; | |
}, | |
// Alignment | |
// For every nearby boid in the system, calculate the average velocity | |
align: function (boids) { | |
var neighborDist = 65; | |
var steer = [0, 0]; | |
var count = 0; | |
for (var i = 0, l = boids.length; i < l; i++) { | |
var other = boids[i]; | |
var distance = getDistance(this.path[0], other.path[0]); | |
if (distance > 0 && distance < neighborDist) { | |
steer[0] += other.vx; | |
steer[1] += other.vy; | |
count++; | |
} | |
} | |
if (count > 0) { | |
steer[0] /= count; | |
steer[1] /= count; | |
} | |
if (!steer[0] === 0 && !steer[1] === 0) { | |
// Implement Reynolds: Steering = Desired - Velocity | |
//steer.length = this.maxSpeed; | |
//steer -= this.vector; | |
//steer.length = Math.min(steer.length, this.maxForce); | |
} | |
return steer; | |
}, | |
// Cohesion | |
// For the average location (i.e. center) of all nearby boids, | |
// calculate steering vector towards that location | |
cohesion: function (boids) { | |
var neighborDist = 100; | |
var sum = [0, 0]; | |
var count = 0; | |
for (var i = 0, l = boids.length; i < l; i++) { | |
var other = boids[i]; | |
var distance = getDistance(this.path[0], other.path[0]); | |
if (distance > 0 && distance < neighborDist) { | |
sum[0] += other.path[0][0]; // Add location | |
sum[1] += other.path[0][1]; | |
count++; | |
} | |
} | |
if (count > 0) { | |
sum[0] /= count; | |
sum[1] /= count; | |
// Steer towards the location | |
return this.steer(sum, false); | |
} | |
return sum; | |
} | |
}; | |
} | |
/** | |
* Closure that creates the sperm itself and the functions the sperm uses to move | |
* | |
* @returns | |
*/ | |
function spermatozoaFunc() { | |
return d3.range(n).map(function (i) { | |
var x = Math.random() * width, // starting X | |
y = Math.random() * height; // starting Y | |
return { | |
id: i, | |
headChange: true, | |
vx: Math.random() * 2 - 1, // vector (direction) x | |
vy: Math.random() * 2 - 1, // vector (direction) y | |
path: d3.range(m).map(function () { return [x, y]; }), // not sure | |
count: 0, // not sure | |
/** | |
* A method that calculates a steering vector towards a target | |
* | |
* @param {any} target - The target point [X,Y] to calculate agianst | |
* @param {any} slowdown - If true, slows down as it approaches the target | |
* @returns {array} - The point/vector in which to head to | |
*/ | |
steer: function (target, slowdown) { | |
var steer = [0, 0], desired = [0, 0]; | |
desired[0] = target[0] - this.path[0][0]; | |
desired[1] = target[1] - this.path[0][1]; | |
var distance = 0//desired.length; | |
// Two options for desired vector magnitude | |
// (1 -- based on distance, 2 -- maxSpeed) | |
if (slowdown && distance < 100) { | |
// This damping is somewhat arbitrary: | |
//desired.length = this.maxSpeed * (distance / 100); | |
} else { | |
//desired.length = this.maxSpeed; | |
} | |
normalize(desired, 2); | |
var tempPoint; | |
tempPoint = [this.path[0][0], this.path[0][1]]; | |
normalize(tempPoint, 1); | |
steer[0] = desired[0] - tempPoint[0]; | |
steer[1] = desired[1] - tempPoint[1]; | |
//steer.length = Math.min(this.maxForce, steer.length); | |
normalize(steer, 1); | |
return steer; | |
}, | |
/** | |
* Separation | |
* | |
* @param {any} boids | |
* @returns | |
*/ | |
separate: function (boids) { | |
var desiredSeperation = 30; | |
var steer = [0, 0]; | |
var count = 0; | |
// For every boid in the system, check if it's too close | |
for (var i = 0, l = boids.length; i < l; i++) { | |
var other = boids[i]; | |
var vector = [0, 0]; | |
vector[0] = this.path[0][0] - other.path[0][0]; | |
vector[1] = this.path[0][1] - other.path[0][1]; | |
var distance = getDistance(this.path[0], other.path[0]); | |
if (distance > 0 && distance < desiredSeperation) { | |
// Calculate vector pointing away from neighbor | |
//normalize(vector,1); | |
//steer[0] += vector[0]*(1 / distance); // TODO Normalize | |
vector[0] = vector[0] * (1 / distance); | |
vector[1] = vector[1] * (1 / distance); | |
normalize(vector, .15); | |
steer[0] += vector[0]; | |
steer[1] += vector[1]; | |
//steer[1] += vector[1]*(1 / distance); // TODO Normalize | |
count++; | |
} | |
} | |
// Average -- divide by how many | |
if (count > 0) { | |
steer[0] /= count; | |
steer[1] /= count; | |
} | |
if (!steer[0] === 0 && !steer[1] === 0) { | |
// Implement Reynolds: Steering = Desired - Velocity | |
//steer.length = this.maxSpeed; | |
//steer -= this.vector; | |
//steer.length = Math.min(steer.length, this.maxForce); | |
} | |
normalize(steer, 1) | |
return steer; | |
}, | |
/** | |
* Alignment | |
* For every nearby boid in the system, calculate the average velocity | |
* | |
* @param {any} boids | |
* @returns | |
*/ | |
align: function (boids) { | |
var neighborDist = 65; | |
var steer = [0, 0]; | |
var count = 0; | |
for (var i = 0, l = boids.length; i < l; i++) { | |
var other = boids[i]; | |
var distance = getDistance(this.path[0], other.path[0]); | |
if (distance > 0 && distance < neighborDist) { | |
steer[0] += other.vx; | |
steer[1] += other.vy; | |
count++; | |
} | |
} | |
if (count > 0) { | |
steer[0] /= count; | |
steer[1] /= count; | |
} | |
if (!steer[0] === 0 && !steer[1] === 0) { | |
// Implement Reynolds: Steering = Desired - Velocity | |
//steer.length = this.maxSpeed; | |
//steer -= this.vector; | |
//steer.length = Math.min(steer.length, this.maxForce); | |
} | |
return steer; | |
}, | |
/** | |
* Cohesion | |
* For the average location (i.e. center) of all nearby boids, | |
* calculate steering vector towards that location | |
* | |
* @param {any} boids | |
* @returns | |
*/ | |
cohesion: function (boids) { | |
var neighborDist = 100; | |
var sum = [0, 0]; | |
var count = 0; | |
for (var i = 0, l = boids.length; i < l; i++) { | |
var other = boids[i]; | |
var distance = getDistance(this.path[0], other.path[0]); | |
if (distance > 0 && distance < neighborDist) { | |
sum[0] += other.path[0][0]; // Add location | |
sum[1] += other.path[0][1]; | |
count++; | |
} | |
} | |
if (count > 0) { | |
sum[0] /= count; | |
sum[1] /= count; | |
// Steer towards the location | |
return this.steer(sum, false); | |
} | |
return sum; | |
} | |
}; | |
}); | |
} | |
/** | |
* Move the head based on the direction of the sperm | |
* @private | |
* @param {any} d | |
* @returns | |
*/ | |
function headTransform(d) { | |
//if (d.headChange) { | |
var steer = d.separate(spermatozoa); | |
var align = d.align(spermatozoa); | |
var sum = d.cohesion(spermatozoa); | |
return "translate(" + d.path[0] + ")rotate(" + Math.atan2((d.vy + align[1] + sum[1] + steer[1]), (d.vx + align[0] + sum[0] + steer[0])) * degrees + ")"; | |
//} | |
//else { | |
// return "translate(" + d.path[0] + ")rotate(" + Math.atan2((d.vy), (d.vx)) * degrees + ")"; | |
//} | |
} | |
/** | |
* The path for the tail of the sperm | |
* @private | |
* | |
* @param {any} d | |
* @returns | |
*/ | |
function tailPath(d) { | |
return "M" + d.join("L"); | |
} | |
/** | |
* | |
* | |
* @param {Array} point - X,Y array of numbers | |
* @param {any} scale - | |
*/ | |
function normalize(point, scale) { | |
var norm = Math.sqrt(point[0] * point[0] + point[1] * point[1]); | |
if (norm != 0) { // as3 return 0,0 for a point of zero length | |
point[0] = scale * point[0] / norm; | |
point[1] = scale * point[1] / norm; | |
} | |
} | |
/** | |
* Get the distance between two point/position objects | |
* @private | |
* @param {Array} pos1 - Position 1 | |
* @param {Array} pos2 - Position 2 | |
* @returns number - distance remembering the two. | |
*/ | |
function getDistance(pos1, pos2) { | |
return Math.sqrt(Math.pow((pos2[1] - pos1[1]), 2) + Math.pow((pos2[0] - pos1[0]), 2)); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment