Skip to content

Instantly share code, notes, and snippets.

@taylorchasewhite
Last active August 20, 2017 12:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save taylorchasewhite/f5ede58351f8e7e5e3cd31cfc1ce2e7e to your computer and use it in GitHub Desktop.
Save taylorchasewhite/f5ede58351f8e7e5e3cd31cfc1ce2e7e to your computer and use it in GitHub Desktop.
Flocking With Sperm
<!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>
/*
* 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