Inspired by the Paper.js "tadpoles" example. Built with D3.js. I'm not sure the word is inspired, but I've forked this example and incorporate the Boids flocking algorithm.
-
-
Save aflaxman/4611181 to your computer and use it in GitHub Desktop.
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
*~ |
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
// Boid flocking based on http://harry.me/2011/02/17/neat-algorithms---flocking | |
// From http://www.jasondavies.com/voroboids/ | |
var boid = (function() { | |
function boid() { | |
var position = [0, 0], | |
velocity = [0, 0], | |
gravityCenter = null, | |
neighborRadius = 50, | |
maxForce = .1, | |
maxSpeed = 1, | |
separationWeight = 2, | |
alignmentWeight = 1, | |
cohesionWeight = 1, | |
desiredSeparation = 10; | |
function boid(neighbors) { | |
var accel = flock(neighbors); | |
d3_ai_boidWrap(position); | |
velocity[0] += accel[0]; | |
velocity[1] += accel[1]; | |
if (gravityCenter) { | |
var g = d3_ai_boidGravity(gravityCenter, position, neighborRadius); | |
velocity[0] += g[0]; | |
velocity[1] += g[1]; | |
} | |
d3_ai_boidLimit(velocity, maxSpeed); | |
position[0] += velocity[0]; | |
position[1] += velocity[1]; | |
return position; | |
} | |
function flock(neighbors) { | |
var separation = [0, 0], | |
alignment = [0, 0], | |
cohesion = [0, 0], | |
separationCount = 0, | |
alignmentCount = 0, | |
cohesionCount = 0, | |
i = -1, | |
l = neighbors.length; | |
while (++i < l) { | |
var n = neighbors[i]; | |
if (n === this) continue; | |
var npos = n.position(), | |
d = d3_ai_boidDistance(position, npos); | |
if (d > 0) { | |
if (d < desiredSeparation) { | |
var tmp = d3_ai_boidNormalize(d3_ai_boidSubtract(position.slice(), npos)); | |
separation[0] += tmp[0] / d; | |
separation[1] += tmp[1] / d; | |
separationCount++; | |
} | |
if (d < neighborRadius) { | |
var nvel = n.velocity(); | |
alignment[0] += nvel[0]; | |
alignment[1] += nvel[1]; | |
alignmentCount++; | |
cohesion[0] += npos[0]; | |
cohesion[1] += npos[1]; | |
cohesionCount++; | |
} | |
} | |
} | |
if (separationCount > 0) { | |
separation[0] /= separationCount; | |
separation[1] /= separationCount; | |
} | |
if (alignmentCount > 0) { | |
alignment[0] /= alignmentCount; | |
alignment[1] /= alignmentCount; | |
} | |
d3_ai_boidLimit(alignment, maxForce); | |
if (cohesionCount > 0) { | |
cohesion[0] /= cohesionCount; | |
cohesion[1] /= cohesionCount; | |
} else { | |
cohesion = position.slice(); | |
} | |
cohesion = steerTo(cohesion); | |
return [ | |
separation[0] * separationWeight + | |
alignment[0] * alignmentWeight + | |
cohesion[0] * cohesionWeight, | |
separation[1] * separationWeight + | |
alignment[1] * alignmentWeight + | |
cohesion[1] * cohesionWeight | |
]; | |
} | |
function steerTo(target) { | |
var desired = d3_ai_boidSubtract(target, position), | |
d = d3_ai_boidMagnitude(desired); | |
if (d > 0) { | |
d3_ai_boidNormalize(desired); | |
// Two options for desired vector magnitude (1 -- based on distance, 2 -- maxspeed) | |
var mul = maxSpeed * (d < 100 ? d / 100 : 1); | |
desired[0] *= mul; | |
desired[1] *= mul; | |
// Steering = Desired minus Velocity | |
var steer = d3_ai_boidSubtract(desired, velocity); | |
d3_ai_boidLimit(steer, maxForce) // Limit to maximum steering force | |
} else { | |
steer = [0, 0]; | |
} | |
return steer; | |
} | |
boid.position = function(x) { | |
if (!arguments.length) return position; | |
position = x; | |
return boid; | |
} | |
boid.velocity = function(x) { | |
if (!arguments.length) return velocity; | |
velocity = x; | |
return boid; | |
} | |
boid.gravityCenter = function(x) { | |
if (!arguments.length) return gravityCenter; | |
gravityCenter = x; | |
return boid; | |
} | |
boid.neighborRadius = function(x) { | |
if (!arguments.length) return neighborRadius; | |
neighborRadius = x; | |
return boid; | |
} | |
boid.maxForce = function(x) { | |
if (!arguments.length) return maxForce; | |
maxForce = x; | |
return boid; | |
} | |
boid.maxSpeed = function(x) { | |
if (!arguments.length) return maxSpeed; | |
maxSpeed = x; | |
return boid; | |
} | |
boid.separationWeight = function(x) { | |
if (!arguments.length) return separationWeight; | |
separationWeight = x; | |
return boid; | |
} | |
boid.alignmentWeight = function(x) { | |
if (!arguments.length) return alignmentWeight; | |
alignmentWeight = x; | |
return boid; | |
} | |
boid.cohesionWeight = function(x) { | |
if (!arguments.length) return cohesionWeight; | |
cohesionWeight = x; | |
return boid; | |
} | |
boid.desiredSeparation = function(x) { | |
if (!arguments.length) return desiredSeparation; | |
desiredSeparation = x; | |
return boid; | |
} | |
return boid; | |
} | |
function d3_ai_boidNormalize(a) { | |
var m = d3_ai_boidMagnitude(a); | |
if (m > 0) { | |
a[0] /= m; | |
a[1] /= m; | |
} | |
return a; | |
} | |
function d3_ai_boidWrap(position) { | |
if (position[0] > w) position[0] = 0; | |
else if (position[0] < 0) position[0] = w; | |
if (position[1] > h) position[1] = 0; | |
else if (position[1] < 0) position[1] = h; | |
} | |
function d3_ai_boidGravity(center, position, neighborRadius) { | |
if (center[0] != null) { | |
var m = d3_ai_boidSubtract(center.slice(), position), | |
d = d3_ai_boidMagnitude(m) - 10; | |
if (d > 0 && d < neighborRadius * 5) { | |
d3_ai_boidNormalize(m); | |
m[0] /= d; | |
m[1] /= d; | |
return m; | |
} | |
} | |
return [0, 0]; | |
} | |
function d3_ai_boidDistance(a, b) { | |
var dx = a[0] - b[0], | |
dy = a[1] - b[1]; | |
if (dx > w / 2) dx = w - dx; | |
if (dy > h / 2) dy = h - dy; | |
return Math.sqrt(dx * dx + dy * dy); | |
} | |
function d3_ai_boidSubtract(a, b) { | |
a[0] -= b[0]; | |
a[1] -= b[1]; | |
return a; | |
} | |
function d3_ai_boidMagnitude(v) { | |
return Math.sqrt(v[0] * v[0] + v[1] * v[1]); | |
} | |
function d3_ai_boidLimit(a, max) { | |
if (d3_ai_boidMagnitude(a) > max) { | |
d3_ai_boidNormalize(a); | |
a[0] *= max; | |
a[1] *= max; | |
} | |
return a; | |
} | |
return boid; | |
})(); |
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> | |
<head> | |
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.js?1.29.1"></script> | |
<script type="text/javascript" src="boid.js"></script> | |
<style type="text/css"> | |
body { | |
background: #000; | |
} | |
ellipse { | |
fill: #fff; | |
opacity: .5; | |
} | |
path { | |
fill: none; | |
stroke: #fff; | |
stroke-linecap: round; | |
opacity: .5; | |
} | |
.mid { | |
stroke-width: 4px; | |
} | |
.tail { | |
stroke-width: 2px; | |
} | |
</style> | |
</head> | |
<body> | |
<script type="text/javascript"> | |
var w = 960, | |
h = 500, | |
n = 100, | |
m = 12, | |
mouse = [0, 0], | |
degrees = 180 / Math.PI; | |
// Initialise boids. | |
var boids = d3.range(n).map(function() { | |
return boid() | |
.position([Math.random() * w, Math.random() * h]) | |
.velocity([Math.random() * 2 - 1, Math.random() * 2 - 1]) | |
.gravityCenter(mouse) | |
.desiredSeparation(25); | |
}); | |
var spermatozoa = boids.map(function(boid) { | |
return { | |
boid: boid, | |
path: d3.range(m).map(function() { return [boid.position()[0], boid.position()[1]]; }), | |
count: 0, | |
}; | |
}); | |
var svg = d3.select("body").append("svg:svg") | |
.attr("width", w) | |
.attr("height", h) | |
.on("mousemove", function() { | |
var m = d3.svg.mouse(this); | |
mouse[0] = m[0]; | |
mouse[1] = m[1]; | |
}) | |
.on("mouseout", function() { | |
mouse[0] = mouse[1] = null; | |
}); | |
var g = svg.selectAll("g") | |
.data(spermatozoa) | |
.enter().append("svg:g"); | |
var head = g.append("svg:ellipse") | |
.attr("rx", 6.5) | |
.attr("ry", 5); | |
g.append("svg:path") | |
.map(function(d) { return d.path.slice(0, 3); }) | |
.attr("class", "mid"); | |
g.append("svg:path") | |
.map(function(d) { return d.path; }) | |
.attr("class", "tail"); | |
var tail = g.selectAll("path"); | |
d3.timer(function() { | |
boids.forEach(function(boid) { | |
boid(boids); | |
}); | |
for (var i = -1; ++i < n;) { | |
var spermatozoon = spermatozoa[i], | |
sboid = boids[i], | |
path = spermatozoon.path, | |
dx = sboid.velocity()[0], | |
dy = sboid.velocity()[1], | |
x = path[0][0] = sboid.position()[0], | |
y = path[0][1] = sboid.position()[1], | |
speed = Math.sqrt(dx * dx + dy * dy), | |
count = speed * 10, | |
k1 = -5 - speed / 3; | |
// 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) / 600) / 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", function(d) { | |
return "translate(" + d.path[0] + ")rotate(" + Math.atan2(d.boid.velocity()[1], d.boid.velocity()[0]) * degrees + ")"; | |
}); | |
tail.attr("d", function(d) { | |
return "M" + d.join("L"); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment