├── README.md
├── _config.yml
├── _includes
└── header.html
├── favicon.ico
├── favicon_package_v0.16
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── browserconfig.xml
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── mstile-150x150.png
├── safari-pinned-tab.svg
└── site.webmanifest
├── simulation
├── application.js
├── boid.js
├── boids.css
├── boids.js
├── canvas_init.js
├── css
├── obstacle.js
├── simulation.js
└── vector.js
└── starlings.gif
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Flock Dynamics and Societal Catastrophe
3 | ## an essay with javascript fidgets
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | I have a clear memory of sitting by a window at a young age watching a flock of starlings weave its way through a dimly lit sky. The ebb and the flow of that mass of birds drew me in completely and I sat and watched their collective movements until they finally came to roost. It was an awesome display, and I mean that in an old sense of the word. That is to say it filled me with awe--which I can’t say is always a pleasant feeling. There was something almost ominous in the coordination of their movements. They seemed to be so instantaneously in sync and I was intimidated by that perfect communication. Since then, this behaviour has held my interest. But I am coming to see it as a lens through which to view the behaviour of people and many of our society’s current ailments. I want to know how a group of starlings, so in sync and coordinated, can on occasion fly head first into a wall.
15 |
16 | This behaviour in starlings is called a murmuration and is exhibited by many herd animals including bats and minnows. It can be thought of as an emergent property. In other words, it is a property of a group that arises out of the properties of individuals and the interactions there of. Colour is often used as an example of an emergent property — a single molecule does not have a colour whereas a group of molecules, a shirt for instance, most certainly does. In this case, the colour of your shirt is a function of the arrangement of the molecules of which it is made, and in particular which frequencies of light are reflected or absorbed by this arrangement. The colour red is an emergent property of molecules that when put together, absorb all frequencies of visible light but red. This is a rather banal example of an emergent property, but the topic becomes more interesting when you think of things such as cosmic order or consciousness. It seems unlikely that a single neuron is conscious, but the group that make up mine and your brains are. Consciousness, or in this case starling murmurations, are emergent properties worth at least a few paragraphs.
17 |
18 |
19 |
20 | We are living in a convenient time for such paragraphs as we have at our disposal a relatively new tool for scientific exploration — computational simulation. There are countless philosophical arguments dedicated to the role of simulation in science and I will by no means attempt to summarize them here. I will simply posit that a new and interesting way of investigating a natural phenomenon is to attempt to simulate it yourself. If you are able to recreate an accurate simulation of the observed phenomenon then you will have learned something about what initial conditions that phenomenon may need to arise, and what rules need to be followed to maintain it. This is particularly interesting when the phenomenon is an emergent one, because we are able to simulate the individuals and see if the we can accurately recreate the global property.
21 |
22 | This investigative tool has been used for the behaviour of ants, the growth of plants, or the development of entire ecosystems[^1] to name just a few. Here it has been used to explore the murmurations of birds[^2][^3].
23 |
24 | [^1]:See wikipedia for an overview of each, and respectively, [here](https://en.wikipedia.org/wiki/Artificial_ants), [here](https://en.wikipedia.org/wiki/Simulated_growth_of_plants) and [here](ttps://en.wikipedia.org/wiki/Ecosystem_model)
25 | [^2]:code forked from [Emergent Mind](http://www.emergentmind.com/boids) and updated by me.
26 | [^3]: thanks to Martha, Thomas, Jon, Michael, Aidan, and Lyon for suggestions and edits, and Natalie for css sanity.
27 |
28 | Each bird above (small red triangle) is governed by three simple rules:
29 | 1. Fly towards your neighbors (cohesion)
30 | 2. Match your neighbours’ speed and direction (alignment)
31 | 3. Don’t get too close to your neighbours (separation)
32 |
33 | That’s it. All the patterns, movement, and motion are just born from those simple rules followed by each bird.
34 |
35 | In practice, these rules are implemented by calculating the distance between each bird and applying the attraction or repulsion inversely proportional to that distance squared. Similar equations can be used to model the movement of the moon around the earth, the earth around the sun and the sun around the rest of the cosmic expanse, which alludes to the omnipresence of this type of movement, and explains my fascination. But before you go off to scrawl equations on the wall in crayon or start a neo-pythagorean cult, I need to remind you that there is a difference between an ontological claim and a convenient explanatory tool. I won’t even claim that the equations used to create these visualizations have anything to do with the movement of birds or indeed the behaviour of people. But they are at least a useful way of thinking about it.
36 |
37 | To be specific, the code used to create this illustration keeps track of the velocity of each bird and calculates the distance between each other bird sixty times a second. It then updates the velocity of each bird based on the three rules outlined above, each multiplied by a constant value. In the code there are three numbers that represent the relative effect of rules one, two and three, which were predefined by me and used to determine the strength of each rule. If the multiplier for rule one is too big, then the birds will all crowd together into a single point. If the multiplier for rule three is too big, then they will all flee from each other in a chaotic mess. Each would result in something thoroughly uninteresting to look at.
38 |
39 | In the box below you can turn these knobs up or down yourself and see how it changes their behaviour. You will notice that a fine balance needs to be found between these three numbers in order for the flock behaviour to arise.
40 |
41 |
49 |
50 |
51 | We can make things more interesting by giving them something to avoid, by adding in a fourth rule — stay away from the predator. In the box below you can play that role. Move your mouse (or finger if on a phone) into the box and notice how the birds avoid you. I have also added another obstacle for them to avoid in the large, red circle in the middle. If the birds come in contact with that circle, they will be removed from the simulation, or to put it more tersely and metaphorically, they will be killed.
52 |
53 | Let’s say that we have observed catastrophe and our flock has flown straight into a that obstacle. We must now play the role of detective and try to determine what happened to cause such a tragedy. Two explanations jump to mind: either a small number of saboteurs, with malice and forethought, guided the flock to destruction, or all the birds were collectively unable to avoid the obstacle because they were blindsided by other motivators. If we turn down the dial for their desire to stay seperate and turn up the dial for their desire to align themselves,the flock will be too slow at changing course and may fly straight into the obstacle. In the box below you can play with the number of saboteurs (where each saboteur wants to fly into the obstacle, and is represented in green) and the relative weighting of the rule forces. I believe you will find that it is easier to incite destruction of the flock by changing the rule weightings than by added individual saboteurs.
54 |
55 |
56 |
66 | Reset
67 |
68 |
69 | Now by this point you are no doubt tired of birds and small red triangles, and perhaps see what I am getting to, so I will be blunt.
70 |
71 | When you are frustrated with the way society is functioning, I want you to think of these birds. When you are concerned about the rampant inefficiencies of big government I want you to think about which individual well - meaning motivators are creating that emergent behaviour. When you are worried about the rise of the alt-right, I want you to try to understand what is enticing people to join that murmur. When you are disheartened by the slow rate of social progress, I want you to visualize a murmur of starlings a continent wide and imagine it trying to change course. And when something catastrophic happens, I want you to think about the relative probabilities of individual malicious saboteurs, or simple unaligned motivations.
72 |
73 | But more importantly and more urgently I want you to think about everyone around you, those you agree with and those you disagree with alike, as members of the same flock. And I want you to think about yourself embedded within, and what you can do to avoid the wall.
74 |
75 |
76 |
77 |
78 | Share this on →
79 |
Twitter
80 |
Facebook
81 |
Google+
82 |
83 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-minimal
--------------------------------------------------------------------------------
/_includes/header.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nickfrosst/flock_dynamics/92f51ba46e575df9219b828bca8f9d2bb963173e/favicon.ico
--------------------------------------------------------------------------------
/favicon_package_v0.16/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nickfrosst/flock_dynamics/92f51ba46e575df9219b828bca8f9d2bb963173e/favicon_package_v0.16/android-chrome-192x192.png
--------------------------------------------------------------------------------
/favicon_package_v0.16/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nickfrosst/flock_dynamics/92f51ba46e575df9219b828bca8f9d2bb963173e/favicon_package_v0.16/android-chrome-512x512.png
--------------------------------------------------------------------------------
/favicon_package_v0.16/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nickfrosst/flock_dynamics/92f51ba46e575df9219b828bca8f9d2bb963173e/favicon_package_v0.16/apple-touch-icon.png
--------------------------------------------------------------------------------
/favicon_package_v0.16/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/favicon_package_v0.16/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nickfrosst/flock_dynamics/92f51ba46e575df9219b828bca8f9d2bb963173e/favicon_package_v0.16/favicon-16x16.png
--------------------------------------------------------------------------------
/favicon_package_v0.16/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nickfrosst/flock_dynamics/92f51ba46e575df9219b828bca8f9d2bb963173e/favicon_package_v0.16/favicon-32x32.png
--------------------------------------------------------------------------------
/favicon_package_v0.16/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nickfrosst/flock_dynamics/92f51ba46e575df9219b828bca8f9d2bb963173e/favicon_package_v0.16/favicon.ico
--------------------------------------------------------------------------------
/favicon_package_v0.16/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nickfrosst/flock_dynamics/92f51ba46e575df9219b828bca8f9d2bb963173e/favicon_package_v0.16/mstile-150x150.png
--------------------------------------------------------------------------------
/favicon_package_v0.16/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/favicon_package_v0.16/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/simulation/boid.js:
--------------------------------------------------------------------------------
1 | var MAX_SPEED = 4;
2 | var MAX_FORCE = 0.1;
3 | var DESIRED_SEPARATION = 40;
4 | var MOUSE_DESIRED_SEPARATION = 100;
5 | var OBSTACLE_DESIRED_SEPARATION = 200;
6 | var NEIGHBOR_DISTANCE = 60;
7 | var BORDER_OFFSET = 3;
8 | var EPSILON = 0.0000001;
9 | var render_size = 10;
10 | var death_throws = 0;
11 | var avoidanceMultiplier = 1;
12 |
13 |
14 |
15 | function Boid(x, y, simulation) {
16 | var randomAngle = Math.random() * 2 * Math.PI;
17 | this.velocity = new Vector(Math.cos(randomAngle), Math.sin(randomAngle));
18 | this.position = new Vector(x, y);
19 | this.acceleration = new Vector(0, 0);
20 | this.simulation = simulation;
21 | this.render_size = render_size
22 | this.death_throws = death_throws
23 | this.sabateur = false;
24 | }
25 |
26 | Boid.prototype = {
27 |
28 | render: function () {
29 | var directionVector = this.velocity.normalize().multiplyBy(this.render_size);
30 | var inverseVector1 = new Vector(- directionVector.y, directionVector.x);
31 | var inverseVector2 = new Vector(directionVector.y, - directionVector.x);
32 | inverseVector1 = inverseVector1.divideBy(3);
33 | inverseVector2 = inverseVector2.divideBy(3);
34 |
35 | this.simulation.ctx.beginPath();
36 | this.simulation.ctx.moveTo(this.position.x, this.position.y);
37 | this.simulation.ctx.lineTo(this.position.x + inverseVector1.x, this.position.y + inverseVector1.y);
38 | this.simulation.ctx.lineTo(this.position.x + directionVector.x, this.position.y + directionVector.y);
39 | this.simulation.ctx.lineTo(this.position.x + inverseVector2.x, this.position.y + inverseVector2.y);
40 | this.simulation.ctx.lineTo(this.position.x, this.position.y);
41 | this.simulation.ctx.strokeStyle = 'rgba(255, 59, 0, 1)';
42 | this.simulation.ctx.stroke();
43 | if (this.sabateur) {
44 | this.simulation.ctx.fillStyle = 'rgba(22, 236, 22, 0.8)';
45 | } else if (this.death_throws == 0) {
46 | this.simulation.ctx.fillStyle = 'rgba(236, 72, 22, 0.8)';
47 | } else {
48 | this.simulation.ctx.fillStyle = 'rgba(22, 72, 236, 0.8)';
49 | }
50 | this.simulation.ctx.fill();
51 | },
52 |
53 | //
54 | // Rule 1: Boids try to fly towards the centre of mass of neighbouring boids.
55 | getCohesionVector: function (boids) {
56 | var totalPosition = new Vector(0, 0);
57 | var neighborCount = 0;
58 | for (var bi in boids) {
59 | var boid = boids[bi];
60 | if (this == boid) {
61 | continue;
62 | }
63 |
64 | var distance = this.position.getDistance(boid.position) + EPSILON;
65 | if (distance <= NEIGHBOR_DISTANCE) {
66 | totalPosition = totalPosition.add(boid.position);
67 | neighborCount++;
68 | }
69 | }
70 |
71 | if (neighborCount > 0) {
72 | var averagePosition = totalPosition.divideBy(neighborCount);
73 | return this.seek(averagePosition);
74 | } else {
75 | return new Vector(0, 0);
76 | }
77 | },
78 |
79 | seek: function (targetPosition) {
80 | var desiredVector = targetPosition.subtract(this.position);
81 |
82 | // Scale to the maximum speed
83 | desiredVector.iSetMagnitude(MAX_SPEED);
84 |
85 | // Steering = Desired minus Velocity
86 | var steeringVector = desiredVector.subtract(this.velocity);
87 | steeringVector = steeringVector.limit(MAX_FORCE);
88 |
89 | return steeringVector;
90 | },
91 |
92 | //
93 | // Rule 2: Boids try to keep a small distance away from other objects (including other boids).
94 | getSeparationVector: function (boids) {
95 |
96 | var steeringVector = new Vector(0, 0);
97 | var neighborCount = 0;
98 |
99 | for (var bi in boids) {
100 | var boid = boids[bi];
101 | if (this == boid) {
102 | continue;
103 | }
104 |
105 | var distance = this.position.getDistance(boid.position) + EPSILON;
106 | if (distance > 0 && distance < DESIRED_SEPARATION) {
107 | var deltaVector = this.position.subtract(boid.position);
108 | deltaVector.iNormalize();
109 | deltaVector.iDivideBy(distance);
110 | steeringVector.iAdd(deltaVector);
111 | neighborCount++;
112 | }
113 | }
114 |
115 | if (neighborCount > 0) {
116 | var averageSteeringVector = steeringVector.divideBy(neighborCount);
117 | } else {
118 | var averageSteeringVector = new Vector(0, 0);
119 | }
120 |
121 | var distance = this.position.getDistance(mouse_position) + EPSILON;
122 | if (distance > 0 && distance < MOUSE_DESIRED_SEPARATION && this.simulation.avoid_mouse) {
123 | var deltaVector = this.position.subtract(mouse_position);
124 | deltaVector.iNormalize();
125 | deltaVector.iDivideBy(distance ** 2);
126 | deltaVector.iMultiplyBy(5000);
127 | averageSteeringVector.iAdd(deltaVector);
128 | }
129 |
130 | for (var ob in this.simulation.obstacles) {
131 | var obstacle = this.simulation.obstacles[ob];
132 |
133 | var distance = this.position.getDistance(obstacle.position) + EPSILON;
134 | if (distance > 0 && distance < OBSTACLE_DESIRED_SEPARATION) {
135 | var deltaVector = this.position.subtract(obstacle.position);
136 | deltaVector.iNormalize();
137 | deltaVector.iDivideBy(distance);
138 | steeringVector.iAdd(deltaVector);
139 | if (this.sabateur) {
140 | deltaVector.iMultiplyBy(-obstacle.repulsion);
141 | } else {
142 | deltaVector.iMultiplyBy(obstacle.repulsion);
143 | }
144 | averageSteeringVector.iAdd(deltaVector);
145 | }
146 | }
147 |
148 |
149 |
150 | if (averageSteeringVector.getMagnitude() > 0) {
151 | averageSteeringVector.iSetMagnitude(MAX_SPEED);
152 | averageSteeringVector.iSubtract(this.velocity);
153 | averageSteeringVector.iLimit(MAX_FORCE);
154 | }
155 |
156 | return averageSteeringVector;
157 | },
158 |
159 | //
160 | // Rule 3: Boids try to match velocity with near boids.
161 | getAlignmentVector: function (boids) {
162 | var perceivedFlockVelocity = new Vector(0, 0);
163 | var neighborCount = 0;
164 |
165 | for (var bi in boids) {
166 | var boid = boids[bi];
167 | if (this == boid) {
168 | continue;
169 | }
170 |
171 | var distance = this.position.getDistance(boid.position) + EPSILON;
172 | if (distance > 0 && distance < NEIGHBOR_DISTANCE) {
173 | perceivedFlockVelocity.iAdd(boid.velocity);
174 | neighborCount++;
175 | }
176 | }
177 |
178 | if (neighborCount > 0) {
179 |
180 | var averageVelocity = perceivedFlockVelocity.divideBy(neighborCount);
181 | averageVelocity.iSetMagnitude(MAX_SPEED);
182 |
183 | var steeringVector = averageVelocity.subtract(this.velocity);
184 | steeringVector.iLimit(MAX_FORCE);
185 |
186 | return steeringVector;
187 | } else {
188 | return new Vector(0, 0);
189 | }
190 | },
191 |
192 | flock: function (boids) {
193 | var cohesionVector = this.getCohesionVector(boids);
194 | var separationVector = this.getSeparationVector(boids);
195 | var alignmentVector = this.getAlignmentVector(boids);
196 |
197 | separationVector.iMultiplyBy(this.simulation.separationMultiplier);
198 | cohesionVector.iMultiplyBy(this.simulation.cohesionMultiplier);
199 | alignmentVector.iMultiplyBy(this.simulation.alignmentMultiplier);
200 |
201 | this.acceleration.iAdd(cohesionVector);
202 | this.acceleration.iAdd(separationVector);
203 | this.acceleration.iAdd(alignmentVector);
204 | },
205 |
206 | bound: function () {
207 |
208 | if (this.position.x > this.simulation.canvasWidth + BORDER_OFFSET) {
209 | this.position.x = -BORDER_OFFSET;
210 | }
211 |
212 | if (this.position.x < -BORDER_OFFSET) {
213 | this.position.x = this.simulation.canvasWidth + BORDER_OFFSET;
214 | }
215 |
216 | if (this.position.y > this.simulation.canvasHeight + BORDER_OFFSET) {
217 | this.position.y = -BORDER_OFFSET;
218 | }
219 |
220 | if (this.position.y < -BORDER_OFFSET) {
221 | this.position.y = this.simulation.canvasHeight + BORDER_OFFSET;
222 | }
223 | },
224 |
225 | update: function () {
226 | if (this.death_throws == 0) {
227 | this.velocity.iAdd(this.acceleration);
228 |
229 | // Limit speed
230 | this.velocity.iLimit(MAX_SPEED);
231 |
232 | this.position.iAdd(this.velocity);
233 | this.bound();
234 |
235 | // Reset accelertion to 0 each cycle
236 | this.acceleration.iMultiplyBy(0);
237 | }
238 | },
239 |
240 | run: function (boids) {
241 | this.flock(boids);
242 | this.update();
243 | this.render();
244 | },
245 |
246 | set_death_throws: function () {
247 | this.death_throws = 50;
248 | },
249 |
250 | decrease_death_throws: function () {
251 | this.death_throws = this.death_throws - 1;
252 | },
253 |
254 | set_sabateur: function (b) {
255 | this.sabateur = b;
256 | },
257 |
258 | }
259 | ;
260 |
--------------------------------------------------------------------------------
/simulation/boids.css:
--------------------------------------------------------------------------------
1 | div#container{ position: relative; }
2 | canvas{
3 | margin:10px auto;
4 | display: block;
5 | width: 100%;
6 | border-bottom:2px solid grey;
7 | background-color:#555555;
8 | }
9 | .giphy-embed { width: 100%;}
10 | h1 { text-align: center; }
11 | h2 { text-align: center; }
12 | p{text-align: justify;}
13 | .share-page {
14 | background: $secondary-color;
15 | color: $light-color;
16 | padding: 8px 15px;
17 | border-radius: 5px;
18 | margin: 1.5 * $spacing-unit 0;
19 |
20 | a {
21 | font-weight: 700;
22 | color: #fff;
23 | margin-left: 10px;
24 |
25 | &:hover {
26 | border-bottom: 1px dashed #fff;
27 | }
28 | }
29 | }
30 | /* The slider itself */
31 | .slider {
32 | -webkit-appearance: none; /* Override default CSS styles */
33 | appearance: none;
34 | width: 100%; /* Full-width */
35 | height: 25px; /* Specified height */
36 | background: #d3d3d3; /* Grey background */
37 | outline: none; /* Remove outline */
38 | opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */
39 | -webkit-transition: .2s; /* 0.2 seconds transition on hover */
40 | transition: opacity .2s;
41 | }
42 | * Mouse-over effects */
43 | .slider:hover {
44 | opacity: 1; /* Fully shown on mouse-over */
45 | }
46 |
47 | /* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */
48 | .slider::-webkit-slider-thumb {
49 | -webkit-appearance: none; /* Override default look */
50 | appearance: none;
51 | width: 25px; /* Set a specific slider handle width */
52 | height: 25px; /* Slider handle height */
53 | background: #4CAF50; /* Green background */
54 | cursor: pointer; /* Cursor on hover */
55 | }
56 |
57 | .slider::-moz-range-thumb {
58 | width: 25px; /* Set a specific slider handle width */
59 | height: 25px; /* Slider handle height */
60 | background: #4CAF50; /* Green background */
61 | cursor: pointer; /* Cursor on hover */
62 | }
63 | .btn{
64 | border:none;
65 | display:inline-block;
66 | padding:8px 16px;
67 | vertical-align:middle;
68 | overflow:hidden;
69 | text-decoration:none;
70 | color:#f8f8f8;
71 | background-color:#4caf50;
72 | text-align:center;
73 | cursor:pointer;
74 | white-space:nowrap
75 | }
76 |
--------------------------------------------------------------------------------
/simulation/boids.js:
--------------------------------------------------------------------------------
1 | $(function(name) {
2 | console.log(this);
3 |
4 | if (! checkForCanvasSupport()) {
5 | return;
6 | }
7 |
8 | resizeCanvas();
9 |
10 | var initialize_cavas_simulation = function(name,use_obstacle, avoid_mouse, click_to_add){
11 | var simulation = new Simulation(name);
12 | simulation.initialize(use_obstacle,avoid_mouse);
13 | simulation.run();
14 |
15 | if (click_to_add){
16 | $('canvas#' + name).click(function(e) {
17 |
18 | // No need to show the notice once the user has already added a boid
19 | var clickNotice$ = $('div#click_notice');
20 | clickNotice$.fadeOut('fast');
21 |
22 | var rect = this.getBoundingClientRect();
23 | var canvasX = e.clientX - rect.left;
24 | var canvasY = e.clientY - rect.top;
25 |
26 | boid = new Boid(canvasX, canvasY, simulation);
27 | simulation.addBoid(boid);
28 |
29 | return false;
30 | });
31 | }
32 |
33 | $('#reset_button_' + name).click(function(e) {
34 | simulation.initialize(use_obstacle,avoid_mouse);
35 | document.getElementById('num_sabateurs_'+ name).value = 0;
36 | });
37 |
38 | $('canvas#' + name).mousemove(function(e) {
39 | var rect = this.getBoundingClientRect();
40 | simulation.update_mouse_position(e.clientX - rect.left,e.clientY - rect.top);
41 | return false;
42 | });
43 |
44 | $('#num_sabateurs_'+ name).on('change',
45 | function(e){
46 | console.log($(this).val())
47 | simulation.update_sabateurs($(this).val());
48 | });
49 |
50 | $('#separationMultiplier_'+ name).on('change',
51 | function(e){
52 | simulation.update_separationMultiplier($(this).val());
53 | });
54 |
55 | $('#cohesionMultiplier_'+ name).on('change',
56 | function(e){
57 | simulation.update_cohesionMultiplier($(this).val());
58 | });
59 |
60 | $('#alignmentMultiplier_'+ name).on('change',
61 | function(e){
62 | simulation.update_alignmentMultiplier($(this).val());
63 | });
64 | }
65 | initialize_cavas_simulation('boids1',false, true, true)
66 | initialize_cavas_simulation('boids2',false, true, false)
67 | initialize_cavas_simulation('boids4',true, true, false)
68 |
69 | });
70 |
--------------------------------------------------------------------------------
/simulation/canvas_init.js:
--------------------------------------------------------------------------------
1 | function isCanvasSupported() {
2 | var elem = document.createElement('canvas');
3 | return ! !(elem.getContext && elem.getContext('2d'));
4 | }
5 |
6 | function checkForCanvasSupport() {
7 | if (!isCanvasSupported()) {
8 | $('div#container').hide();
9 |
10 | var canvasNotice$ = jQuery('Please update your browser to view this experiment.
');
11 | canvasNotice$.insertAfter($('div#header_wrapper'));
12 |
13 | return false;
14 | } else {
15 | return true;
16 | }
17 | }
18 |
19 | function resizeCanvas(width, height) {
20 | var canvas$ = $('canvas');
21 |
22 | if ('undefined' == typeof (height)) {
23 | height = document.getElementById("starling_gif").height;
24 | }
25 | canvas$.attr('height', height);
26 |
27 | if ('undefined' == typeof (width)) {
28 | width = document.getElementById("starling_gif").width;
29 | console.log(document.getElementById("starling_gif").width)
30 | }
31 | canvas$.attr('width', width);
32 |
33 | }
34 | ;
35 |
--------------------------------------------------------------------------------
/simulation/css:
--------------------------------------------------------------------------------
1 | /* cyrillic-ext */
2 | @font-face {
3 | font-family: 'Source Sans Pro';
4 | font-style: normal;
5 | font-weight: 400;
6 | src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNa7lqDY.woff2) format('woff2');
7 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
8 | }
9 | /* cyrillic */
10 | @font-face {
11 | font-family: 'Source Sans Pro';
12 | font-style: normal;
13 | font-weight: 400;
14 | src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qPK7lqDY.woff2) format('woff2');
15 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
16 | }
17 | /* greek-ext */
18 | @font-face {
19 | font-family: 'Source Sans Pro';
20 | font-style: normal;
21 | font-weight: 400;
22 | src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNK7lqDY.woff2) format('woff2');
23 | unicode-range: U+1F00-1FFF;
24 | }
25 | /* greek */
26 | @font-face {
27 | font-family: 'Source Sans Pro';
28 | font-style: normal;
29 | font-weight: 400;
30 | src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qO67lqDY.woff2) format('woff2');
31 | unicode-range: U+0370-03FF;
32 | }
33 | /* vietnamese */
34 | @font-face {
35 | font-family: 'Source Sans Pro';
36 | font-style: normal;
37 | font-weight: 400;
38 | src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qN67lqDY.woff2) format('woff2');
39 | unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
40 | }
41 | /* latin-ext */
42 | @font-face {
43 | font-family: 'Source Sans Pro';
44 | font-style: normal;
45 | font-weight: 400;
46 | src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNq7lqDY.woff2) format('woff2');
47 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
48 | }
49 | /* latin */
50 | @font-face {
51 | font-family: 'Source Sans Pro';
52 | font-style: normal;
53 | font-weight: 400;
54 | src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2) format('woff2');
55 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
56 | }
57 | /* cyrillic-ext */
58 | @font-face {
59 | font-family: 'Source Sans Pro';
60 | font-style: normal;
61 | font-weight: 700;
62 | src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmhduz8A.woff2) format('woff2');
63 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
64 | }
65 | /* cyrillic */
66 | @font-face {
67 | font-family: 'Source Sans Pro';
68 | font-style: normal;
69 | font-weight: 700;
70 | src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwkxduz8A.woff2) format('woff2');
71 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
72 | }
73 | /* greek-ext */
74 | @font-face {
75 | font-family: 'Source Sans Pro';
76 | font-style: normal;
77 | font-weight: 700;
78 | src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmxduz8A.woff2) format('woff2');
79 | unicode-range: U+1F00-1FFF;
80 | }
81 | /* greek */
82 | @font-face {
83 | font-family: 'Source Sans Pro';
84 | font-style: normal;
85 | font-weight: 700;
86 | src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlBduz8A.woff2) format('woff2');
87 | unicode-range: U+0370-03FF;
88 | }
89 | /* vietnamese */
90 | @font-face {
91 | font-family: 'Source Sans Pro';
92 | font-style: normal;
93 | font-weight: 700;
94 | src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmBduz8A.woff2) format('woff2');
95 | unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
96 | }
97 | /* latin-ext */
98 | @font-face {
99 | font-family: 'Source Sans Pro';
100 | font-style: normal;
101 | font-weight: 700;
102 | src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmRduz8A.woff2) format('woff2');
103 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
104 | }
105 | /* latin */
106 | @font-face {
107 | font-family: 'Source Sans Pro';
108 | font-style: normal;
109 | font-weight: 700;
110 | src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2) format('woff2');
111 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
112 | }
113 |
--------------------------------------------------------------------------------
/simulation/obstacle.js:
--------------------------------------------------------------------------------
1 | function Obstacle(x,y,radius,repulsion, simulation) {
2 | this.position = new Vector(x,y)
3 | this.radius = radius
4 | this.simulation = simulation
5 | this.repulsion = repulsion
6 |
7 | }
8 |
9 | Obstacle.prototype = {
10 | render: function() {
11 | this.simulation.ctx.beginPath();
12 | this.simulation.ctx.moveTo(this.position.x, this.position.y);
13 | this.simulation.ctx.strokeStyle = 'rgba(255, 59, 0, 1)';
14 | this.simulation.ctx.arc(this.position.x, this.position.y,this.radius,0,2*Math.PI);
15 | this.simulation.ctx.fillStyle = 'rgba(236, 72, 22, 0.8)';
16 | this.simulation.ctx.fill();
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/simulation/simulation.js:
--------------------------------------------------------------------------------
1 | var NUM_BOIDS = 60;
2 | var REFRESH_INTERVAL_IN_MS = 15;
3 | var mouse_position = new Vector(0, 0);
4 |
5 | function Simulation(name) {
6 | var canvas = document.getElementById(name);
7 | var canvas$ = $(canvas);
8 |
9 | this.ctx = canvas.getContext('2d');
10 | this.canvasHeight = canvas$.height();
11 | this.canvasWidth = canvas$.width();
12 | this.separationMultiplier = 2;
13 | this.cohesionMultiplier = 1;
14 | this.alignmentMultiplier = 1;
15 | this.avoid_mouse = false;
16 | }
17 |
18 | Simulation.prototype = {
19 | initialize: function (use_obstacle, avoid_mouse) {
20 | this.obstacles = [];
21 | if (use_obstacle) {
22 | this.addObstacle(new Obstacle(this.canvasWidth / 2, this.canvasHeight / 2, 40, 2, this))
23 | }
24 | this.avoid_mouse = avoid_mouse
25 | this.boids = [];
26 | for (var i = 0; i < NUM_BOIDS; i++) {
27 | var boid = new Boid(this.canvasWidth / 4, this.canvasHeight / 4, this);
28 | this.addBoid(boid);
29 | }
30 | },
31 | addBoid: function (boid) {
32 | this.boids.push(boid);
33 | },
34 | addObstacle: function (obstacle) {
35 | this.obstacles.push(obstacle);
36 | },
37 | render: function () {
38 | this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
39 |
40 | for (var bi in this.boids) {
41 | this.boids[bi].run(this.boids);
42 | }
43 | for (var ob in this.obstacles) {
44 | this.obstacles[ob].render(this.obstacles);
45 | }
46 | },
47 | tick: function () {
48 | for (var bi in this.boids) {
49 | var boid = this.boids[bi];
50 | if (boid.death_throws == 0) {
51 | boid.flock(this.boids);
52 | for (var ob in this.obstacles) {
53 | var obstacle = this.obstacles[ob];
54 | if (boid.position.getDistance(obstacle.position) < boid.render_size + obstacle.radius) {
55 | this.boids[bi].set_death_throws();
56 | }
57 | }
58 | } else if (boid.death_throws == 1) {
59 | this.boids.splice(bi, 1);
60 | } else {
61 | this.boids[bi].decrease_death_throws();
62 | }
63 | }
64 |
65 |
66 | },
67 | run: function () {
68 | var self = this;
69 | self.tick();
70 | setInterval(function () {
71 | self.tick();
72 | self.render();
73 | }, REFRESH_INTERVAL_IN_MS);
74 | },
75 |
76 | update_mouse_position: function (x, y) {
77 | mouse_position.update(x, y);
78 | },
79 |
80 | update_sabateurs: function (v) {
81 | for (var bi in this.boids) {
82 | this.boids[bi].set_sabateur(parseInt(bi) < parseInt(v))
83 | }
84 | },
85 |
86 | update_separationMultiplier: function (value) {
87 | this.separationMultiplier = value;
88 | },
89 |
90 | update_cohesionMultiplier: function (value) {
91 | this.cohesionMultiplier = value;
92 | },
93 |
94 | update_alignmentMultiplier: function (value) {
95 | this.alignmentMultiplier = value;
96 | },
97 | }
98 | ;
99 |
--------------------------------------------------------------------------------
/simulation/vector.js:
--------------------------------------------------------------------------------
1 | function Vector(x, y) {
2 | this.x = x;
3 | this.y = y;
4 | }
5 |
6 | Vector.prototype = {
7 |
8 | add: function(vector) {
9 | return new Vector(this.x + vector.x, this.y + vector.y);
10 | },
11 |
12 | update:function(x,y){
13 | this.x = x
14 | this.y = y
15 | },
16 |
17 | iAdd: function(vector) {
18 | this.x += vector.x;
19 | this.y += vector.y;
20 | },
21 |
22 | subtract: function(vector) {
23 | return new Vector(this.x - vector.x, this.y - vector.y);
24 | },
25 |
26 | iSubtract: function(vector) {
27 | this.x -= vector.x;
28 | this.y -= vector.y;
29 | },
30 |
31 | divideBy: function(factor) {
32 | return new Vector(this.x / factor, this.y / factor);
33 | },
34 |
35 | iDivideBy: function(factor) {
36 | this.x /= factor;
37 | this.y /= factor;
38 | },
39 |
40 | multiplyBy: function(factor) {
41 | return new Vector(this.x * factor, this.y * factor);
42 | },
43 |
44 | iMultiplyBy: function(factor) {
45 | this.x *= factor;
46 | this.y *= factor;
47 | },
48 |
49 | multiplyYBy: function(factor) {
50 | return new Vector(this.x, this.y * factor);
51 | },
52 |
53 | normalize: function() {
54 | if (this.getMagnitude() > 0) {
55 | return this.divideBy(this.getMagnitude());
56 | } else {
57 | return new Vector(0, 0);
58 | }
59 | },
60 |
61 | iNormalize: function() {
62 | if (this.getMagnitude() > 0) {
63 | this.iDivideBy(this.getMagnitude());
64 | }
65 | },
66 |
67 | round: function() {
68 | return new Vector(Math.round(this.x), Math.round(this.y));
69 | },
70 |
71 | setMagnitude: function(max) {
72 | return this.normalize().multiplyBy(max);
73 | },
74 |
75 | iSetMagnitude: function(max) {
76 | var unit = this.normalize();
77 | this.x = unit.x * max;
78 | this.y = unit.y * max;
79 | },
80 |
81 | limit: function(max) {
82 | if (this.getMagnitude() > max) {
83 | return this.setMagnitude(max);
84 | } else {
85 | return this;
86 | }
87 | },
88 |
89 | iLimit: function(max) {
90 | if (this.getMagnitude() > max) {
91 | this.iSetMagnitude(max);
92 | }
93 | },
94 |
95 | toString: function() {
96 | return '(' + this.x + ', ' + this.y + ')';
97 | },
98 |
99 | getAngle: function() {
100 | return Math.atan2(this.y, this.x);
101 | },
102 | getAngleInDegrees: function() {
103 | return this.getAngle() * (180 / Math.PI);
104 | },
105 |
106 | getMagnitude: function() {
107 | var origin = new Vector(0, 0);
108 | return origin.getDistance(this);
109 | },
110 |
111 | getDistance: function(vector) {
112 | return Math.sqrt(Math.pow(this.x - vector.x, 2) + Math.pow(this.y - vector.y, 2));
113 | },
114 |
115 | iSetAngle: function(angle) {
116 | this.x = Math.cos(angle);
117 | this.y = Math.sin(angle);
118 | }
119 |
120 | }
121 | ;
122 |
--------------------------------------------------------------------------------
/starlings.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nickfrosst/flock_dynamics/92f51ba46e575df9219b828bca8f9d2bb963173e/starlings.gif
--------------------------------------------------------------------------------