2011-04-19 15:27:59 -04:00
|
|
|
<!DOCTYPE html>
|
|
|
|
<html>
|
|
|
|
<head>
|
2014-08-16 13:24:54 -04:00
|
|
|
<meta charset="UTF-8">
|
|
|
|
<title>Tadpoles</title>
|
|
|
|
<link rel="stylesheet" href="../css/style.css">
|
|
|
|
<script type="text/javascript" src="../../dist/paper-full.js"></script>
|
|
|
|
<script type="text/paperscript" canvas="canvas">
|
|
|
|
// Adapted from Flocking Processing example by Daniel Schiffman:
|
|
|
|
// http://processing.org/learning/topics/flocking.html
|
2011-07-07 10:09:02 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
var Boid = Base.extend({
|
|
|
|
initialize: function(position, maxSpeed, maxForce) {
|
|
|
|
var strength = Math.random() * 0.5;
|
|
|
|
this.acceleration = new Point();
|
|
|
|
this.vector = Point.random() * 2 - 1;
|
|
|
|
this.position = position.clone();
|
|
|
|
this.radius = 30;
|
|
|
|
this.maxSpeed = maxSpeed + strength;
|
|
|
|
this.maxForce = maxForce + strength;
|
|
|
|
this.amount = strength * 10 + 10;
|
|
|
|
this.count = 0;
|
|
|
|
this.createItems();
|
|
|
|
},
|
2011-07-07 10:09:02 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
run: function(boids) {
|
|
|
|
this.lastLoc = this.position.clone();
|
|
|
|
if (!groupTogether) {
|
|
|
|
this.flock(boids);
|
|
|
|
} else {
|
|
|
|
this.align(boids);
|
|
|
|
}
|
|
|
|
this.borders();
|
|
|
|
this.update();
|
|
|
|
this.calculateTail();
|
|
|
|
this.moveHead();
|
|
|
|
},
|
2013-03-05 08:34:19 -05:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
calculateTail: function() {
|
|
|
|
var segments = this.path.segments,
|
|
|
|
shortSegments = this.shortPath.segments;
|
|
|
|
var speed = this.vector.length;
|
|
|
|
var pieceLength = 5 + speed / 3;
|
|
|
|
var point = this.position;
|
|
|
|
segments[0].point = shortSegments[0].point = point;
|
|
|
|
// Chain goes the other way than the movement
|
|
|
|
var lastVector = -this.vector;
|
|
|
|
for (var i = 1; i < this.amount; i++) {
|
|
|
|
var vector = segments[i].point - point;
|
|
|
|
this.count += speed * 10;
|
|
|
|
var wave = Math.sin((this.count + i * 3) / 300);
|
|
|
|
var sway = lastVector.rotate(90).normalize(wave);
|
|
|
|
point += lastVector.normalize(pieceLength) + sway;
|
|
|
|
segments[i].point = point;
|
|
|
|
if (i < 3)
|
|
|
|
shortSegments[i].point = point;
|
|
|
|
lastVector = vector;
|
|
|
|
}
|
|
|
|
this.path.smooth();
|
|
|
|
},
|
2013-03-05 08:34:19 -05:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
createItems: function() {
|
|
|
|
this.head = new Shape.Ellipse({
|
|
|
|
center: [0, 0],
|
|
|
|
size: [13, 8],
|
|
|
|
fillColor: 'white'
|
|
|
|
});
|
2013-12-08 04:58:19 -05:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
this.path = new Path({
|
|
|
|
strokeColor: 'white',
|
|
|
|
strokeWidth: 2,
|
|
|
|
strokeCap: 'round'
|
|
|
|
});
|
|
|
|
for (var i = 0; i < this.amount; i++)
|
|
|
|
this.path.add(new Point());
|
2013-12-08 04:58:19 -05:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
this.shortPath = new Path({
|
|
|
|
strokeColor: 'white',
|
|
|
|
strokeWidth: 4,
|
|
|
|
strokeCap: 'round'
|
|
|
|
});
|
|
|
|
for (var i = 0; i < Math.min(3, this.amount); i++)
|
|
|
|
this.shortPath.add(new Point());
|
|
|
|
},
|
2013-03-05 08:34:19 -05:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
moveHead: function() {
|
|
|
|
this.head.position = this.position;
|
|
|
|
this.head.rotation = this.vector.angle;
|
|
|
|
},
|
2011-07-07 10:09:02 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
// We accumulate a new acceleration each time based on three rules
|
|
|
|
flock: function(boids) {
|
|
|
|
var separation = this.separate(boids) * 3;
|
|
|
|
var alignment = this.align(boids);
|
|
|
|
var cohesion = this.cohesion(boids);
|
|
|
|
this.acceleration += separation + alignment + cohesion;
|
|
|
|
},
|
2013-03-05 08:34:19 -05:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
update: function() {
|
|
|
|
// Update velocity
|
|
|
|
this.vector += this.acceleration;
|
|
|
|
// Limit speed (vector#limit?)
|
|
|
|
this.vector.length = Math.min(this.maxSpeed, this.vector.length);
|
|
|
|
this.position += this.vector;
|
|
|
|
// Reset acceleration to 0 each cycle
|
|
|
|
this.acceleration = new Point();
|
|
|
|
},
|
2011-07-07 10:09:02 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
seek: function(target) {
|
|
|
|
this.acceleration += this.steer(target, false);
|
|
|
|
},
|
2011-07-07 10:09:02 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
arrive: function(target) {
|
|
|
|
this.acceleration += this.steer(target, true);
|
|
|
|
},
|
2013-03-05 08:34:19 -05:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
borders: function() {
|
|
|
|
var vector = new Point();
|
|
|
|
var position = this.position;
|
|
|
|
var radius = this.radius;
|
|
|
|
var size = view.size;
|
|
|
|
if (position.x < -radius) vector.x = size.width + radius;
|
|
|
|
if (position.y < -radius) vector.y = size.height + radius;
|
|
|
|
if (position.x > size.width + radius) vector.x = -size.width -radius;
|
|
|
|
if (position.y > size.height + radius) vector.y = -size.height -radius;
|
|
|
|
if (!vector.isZero()) {
|
|
|
|
this.position += vector;
|
|
|
|
var segments = this.path.segments;
|
|
|
|
for (var i = 0; i < this.amount; i++) {
|
|
|
|
segments[i].point += vector;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2011-07-07 10:09:02 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
// 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,
|
|
|
|
desired = target - this.position;
|
|
|
|
var distance = 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;
|
|
|
|
}
|
|
|
|
steer = desired - this.vector;
|
|
|
|
steer.length = Math.min(this.maxForce, steer.length);
|
|
|
|
return steer;
|
|
|
|
},
|
2011-07-07 10:09:02 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
separate: function(boids) {
|
|
|
|
var desiredSeperation = 60;
|
|
|
|
var steer = new Point();
|
|
|
|
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 = this.position - other.position;
|
|
|
|
var distance = vector.length;
|
|
|
|
if (distance > 0 && distance < desiredSeperation) {
|
|
|
|
// Calculate vector pointing away from neighbor
|
|
|
|
steer += vector.normalize(1 / distance);
|
|
|
|
count++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Average -- divide by how many
|
|
|
|
if (count > 0)
|
|
|
|
steer /= count;
|
|
|
|
if (!steer.isZero()) {
|
|
|
|
// Implement Reynolds: Steering = Desired - Velocity
|
|
|
|
steer.length = this.maxSpeed;
|
|
|
|
steer -= this.vector;
|
|
|
|
steer.length = Math.min(steer.length, this.maxForce);
|
|
|
|
}
|
|
|
|
return steer;
|
|
|
|
},
|
2011-07-07 10:09:02 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
// Alignment
|
|
|
|
// For every nearby boid in the system, calculate the average velocity
|
|
|
|
align: function(boids) {
|
|
|
|
var neighborDist = 25;
|
|
|
|
var steer = new Point();
|
|
|
|
var count = 0;
|
|
|
|
for (var i = 0, l = boids.length; i < l; i++) {
|
|
|
|
var other = boids[i];
|
|
|
|
var distance = this.position.getDistance(other.position);
|
|
|
|
if (distance > 0 && distance < neighborDist) {
|
|
|
|
steer += other.vector;
|
|
|
|
count++;
|
|
|
|
}
|
|
|
|
}
|
2011-07-07 10:09:02 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
if (count > 0)
|
|
|
|
steer /= count;
|
|
|
|
if (!steer.isZero()) {
|
|
|
|
// Implement Reynolds: Steering = Desired - Velocity
|
|
|
|
steer.length = this.maxSpeed;
|
|
|
|
steer -= this.vector;
|
|
|
|
steer.length = Math.min(steer.length, this.maxForce);
|
|
|
|
}
|
|
|
|
return steer;
|
|
|
|
},
|
2011-07-07 10:09:02 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
// 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 = new Point();
|
|
|
|
var count = 0;
|
|
|
|
for (var i = 0, l = boids.length; i < l; i++) {
|
|
|
|
var other = boids[i];
|
|
|
|
var distance = this.position.getDistance(other.position);
|
|
|
|
if (distance > 0 && distance < neighborDist) {
|
|
|
|
sum += other.position; // Add location
|
|
|
|
count++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (count > 0) {
|
|
|
|
sum /= count;
|
|
|
|
// Steer towards the location
|
|
|
|
return this.steer(sum, false);
|
|
|
|
}
|
|
|
|
return sum;
|
|
|
|
}
|
|
|
|
});
|
2011-07-07 10:09:02 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
var heartPath = new Path('M514.69629,624.70313c-7.10205,-27.02441 -17.2373,-52.39453 -30.40576,-76.10059c-13.17383,-23.70703 -38.65137,-60.52246 -76.44434,-110.45801c-27.71631,-36.64355 -44.78174,-59.89355 -51.19189,-69.74414c-10.5376,-16.02979 -18.15527,-30.74951 -22.84717,-44.14893c-4.69727,-13.39893 -7.04297,-26.97021 -7.04297,-40.71289c0,-25.42432 8.47119,-46.72559 25.42383,-63.90381c16.94775,-17.17871 37.90527,-25.76758 62.87354,-25.76758c25.19287,0 47.06885,8.93262 65.62158,26.79834c13.96826,13.28662 25.30615,33.10059 34.01318,59.4375c7.55859,-25.88037 18.20898,-45.57666 31.95215,-59.09424c19.00879,-18.32178 40.99707,-27.48535 65.96484,-27.48535c24.7373,0 45.69531,8.53564 62.87305,25.5957c17.17871,17.06592 25.76855,37.39551 25.76855,60.98389c0,20.61377 -5.04102,42.08691 -15.11719,64.41895c-10.08203,22.33203 -29.54687,51.59521 -58.40723,87.78271c-37.56738,47.41211 -64.93457,86.35352 -82.11328,116.8125c-13.51758,24.0498 -23.82422,49.24902 -30.9209,75.58594z');
|
2013-03-05 08:34:19 -05:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
var boids = [];
|
|
|
|
var groupTogether = false;
|
2011-04-28 15:23:30 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
// Add the boids:
|
|
|
|
for (var i = 0; i < 30; i++) {
|
|
|
|
var position = Point.random() * view.size;
|
|
|
|
boids.push(new Boid(position, 10, 0.05));
|
|
|
|
}
|
2011-04-27 18:19:33 -04:00
|
|
|
|
2013-03-05 08:34:19 -05:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
function onFrame(event) {
|
|
|
|
for (var i = 0, l = boids.length; i < l; i++) {
|
|
|
|
if (groupTogether) {
|
|
|
|
var length = ((i + event.count / 30) % l) / l * heartPath.length;
|
|
|
|
var point = heartPath.getPointAt(length);
|
|
|
|
if (point)
|
|
|
|
boids[i].arrive(point);
|
|
|
|
}
|
|
|
|
boids[i].run(boids);
|
|
|
|
}
|
|
|
|
}
|
2011-04-27 18:19:33 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
// Reposition the heart path whenever the window is resized:
|
|
|
|
function onResize(event) {
|
|
|
|
heartPath.fitBounds(view.bounds);
|
|
|
|
heartPath.scale(0.8);
|
|
|
|
}
|
2011-05-16 07:51:20 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
function onMouseDown(event) {
|
|
|
|
groupTogether = !groupTogether;
|
|
|
|
}
|
2011-07-07 10:09:02 -04:00
|
|
|
|
2014-08-16 13:24:54 -04:00
|
|
|
function onKeyDown(event) {
|
|
|
|
if (event.key == 'space') {
|
|
|
|
var layer = project.activeLayer;
|
|
|
|
layer.selected = !layer.selected;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
<style>
|
|
|
|
body {
|
|
|
|
background: black;
|
|
|
|
}
|
|
|
|
</style>
|
2011-04-19 15:27:59 -04:00
|
|
|
</head>
|
2011-05-05 08:31:36 -04:00
|
|
|
<body>
|
2014-08-16 13:24:54 -04:00
|
|
|
<canvas id="canvas" resize></canvas>
|
2011-05-30 18:27:39 -04:00
|
|
|
</body>
|
2014-04-06 07:44:19 -04:00
|
|
|
</html>
|