Compare commits

..

5 Commits

  1. 25
      index.html
  2. 10
      js/animation.js
  3. 1
      js/animation1b.js
  4. 2
      js/animation2a.js
  5. 3
      js/animation2b.js
  6. 11
      js/animation3a.js
  7. 9
      js/animation3b.js
  8. 15
      js/animation3c.js
  9. 51
      js/arc.js
  10. 32
      js/bundle.js
  11. 13
      js/controls.js
  12. 21
      js/index.js
  13. 69
      js/particle.js

@ -18,7 +18,7 @@
<li>Move in organic, natural arcs, with no sudden pivots or swerves</li> <li>Move in organic, natural arcs, with no sudden pivots or swerves</li>
<li>Support large swarms of particles</li> <li>Support large swarms of particles</li>
<li>Be able to evade obstacles</li> <li>Be able to evade obstacles</li>
<li>Exhibit flocking behavior</li> <li>Simulate cohesive flocking behavior</li>
</ul> </ul>
</blockquote> </blockquote>
@ -67,23 +67,36 @@
<div class='outerContainer' id='2b'></div> <div class='outerContainer' id='2b'></div>
<p> <p>
The last goal was to play around with flocking behaviors: cohesion, separation, and alignment. <h4>Exploration 3: Cohesive Flocking Behavior</h4>
Here's each of the three, with the vision grid visualized:
</p> </p>
<p> <p>
<h4>Exploration 3: Flocking patterns</h4> Using the vision grid and the circular geometry, particles determine and follow a leader. At the start of the animation,
there are no leaders. When particles see each other, one of them is chosen as a leader. The others follow. If other particles
see a leader, they follow it.
</p>
<p>
Each particle recalls its previous location, and compares it to their current location to see if they're moving
towards the leader or not. Each frame iterates the particle closer to its leader. The leader(s) continue
moving freely.
</p> </p>
<div class='outerContainer' id='3a'></div> <div class='outerContainer' id='3a'></div>
<p> <p>
The exploration is now complete: arc-based movement, independent AIs, grid-based vision, On a larger scale, the particles are starting to take on a personality.
following patterns of cohesion, separation, alignment, with hazards, on a large scale:
</p> </p>
<div class='outerContainer' id='3b'></div> <div class='outerContainer' id='3b'></div>
<p>
The exploration is now complete: arc-based movement, independent AIs, grid-based vision,
hazards, and cohesive flocking behavior, on a large scale:
</p>
<div class='outerContainer' id='3c'></div>
<script src='js/bundle.js'></script> <script src='js/bundle.js'></script>
<script src='/core/js/ui.js'></script> <script src='/core/js/ui.js'></script>
</body> </body>

@ -3,14 +3,15 @@ import Grid from './grid';
import Particle from './particle'; import Particle from './particle';
import Controls from './controls'; import Controls from './controls';
import Random from './random'; import Random from './random';
import { CONTROLS, ENTITIES } from './enums'; import { BEHAVIOR, CONTROLS, ENTITIES } from './enums';
function Animation(observables, id, showHazards) { function Animation(observables, id, behavior) {
this.id = id; this.id = id;
this.observables = observables; this.observables = observables;
this.particles = []; this.particles = [];
this.grid = new Grid(); this.grid = new Grid();
this.fpsInterval = null; this.fpsInterval = null;
this.behavior = behavior || BEHAVIOR.FREE;
this.container = document.createElement('div'); this.container = document.createElement('div');
this.container.className = 'animationContainer'; this.container.className = 'animationContainer';
@ -38,15 +39,14 @@ Animation.prototype.subscribeCount = function(count) {
} }
while (this.particles.length < count) { while (this.particles.length < count) {
const p = new Particle(this.container, bounds, this.grid, this.observables); const p = new Particle(this.container, bounds, this.grid, this.observables, this.behavior);
this.particles.push(p); this.particles.push(p);
} }
} }
Animation.prototype.addHazards = function() { Animation.prototype.addHazards = function(n) {
const bounds = this.container.getBoundingClientRect(); const bounds = this.container.getBoundingClientRect();
const n = Random.num(1, 3);
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
const w = Random.num(50, 200); const w = Random.num(50, 200);
const h = Random.num(50, 200); const h = Random.num(50, 200);

@ -5,6 +5,7 @@ export default function(destroy$) {
const id = '1b'; const id = '1b';
const config = { const config = {
id, id,
count: 200,
maxCount: 1000 maxCount: 1000
}; };

@ -10,5 +10,5 @@ export default function(destroy$) {
}; };
const observables = Controls(destroy$, config); const observables = Controls(destroy$, config);
(new Animation(observables, id, true)).addHazards(); (new Animation(observables, id, true)).addHazards(2);
} }

@ -5,9 +5,10 @@ export default function(destroy$) {
const id = '2b'; const id = '2b';
const config = { const config = {
id, id,
count: 200,
maxCount: 1000 maxCount: 1000
}; };
const observables = Controls(destroy$, config); const observables = Controls(destroy$, config);
new Animation(observables, id).addHazards(); new Animation(observables, id).addHazards(2);
} }

@ -1,18 +1,15 @@
import Animation from './animation'; import Animation from './animation';
import Controls from './controls'; import Controls from './controls';
import { BEHAVIOR } from './enums';
export default function(destroy$) { export default function(destroy$) {
const id = '3a'; const id = '3a';
const config = { const config = {
id, id,
count: 5, maxCount: 10,
maxCount: 1000, showVisionGridControl: true
showAlignmentControl: true,
showCohesionControl: true,
showSeparationControl: true,
// showVisionGridControl: true
}; };
const observables = Controls(destroy$, config); const observables = Controls(destroy$, config);
new Animation(observables, id); new Animation(observables, id, BEHAVIOR.COHESION).addHazards();
} }

@ -1,16 +1,15 @@
import Animation from './animation'; import Animation from './animation';
import Controls from './controls'; import Controls from './controls';
import { BEHAVIOR } from './enums';
export default function(destroy$) { export default function(destroy$) {
const id = '3b'; const id = '3b';
const config = { const config = {
id, id,
maxCount: 10, count: 200,
showAlignmentControl: true, maxCount: 1000
showCohesionControl: true,
showSeparationControl: true
}; };
const observables = Controls(destroy$, config); const observables = Controls(destroy$, config);
new Animation(observables, id).addHazards(); new Animation(observables, id, BEHAVIOR.COHESION);
} }

@ -0,0 +1,15 @@
import Animation from './animation';
import Controls from './controls';
import { BEHAVIOR } from './enums';
export default function(destroy$) {
const id = '3c';
const config = {
id,
count: 200,
maxCount: 1000
};
const observables = Controls(destroy$, config);
new Animation(observables, id, BEHAVIOR.COHESION).addHazards(1);
}

@ -1,8 +1,13 @@
import { ENTITIES, RAD } from './enums'; import { ENTITIES, RAD } from './enums';
import Random from './random'; import Random from './random';
// "How much of movement is in the correct direction"
const rigidity = 0.9;
// "How close to the leader is enough"
const sensitivity = 30;
const Arc = { const Arc = {
create: function(bounds, grid) { create: function(bounds) {
let arc = { let arc = {
centerX: Random.num(0, bounds.width), centerX: Random.num(0, bounds.width),
centerY: Random.num(0, bounds.height), centerY: Random.num(0, bounds.height),
@ -25,11 +30,6 @@ const Arc = {
arc = Arc.overflow(arc, bounds); arc = Arc.overflow(arc, bounds);
// If starting in a hazard, recurse.
// if (grid.getPoint({ x: arc.endX, y: arc.endY, type: ENTITIES.HAZARD })) {
// arc = Arc.create(bounds, grid);
// }
return arc; return arc;
}, },
@ -50,7 +50,6 @@ const Arc = {
arc.endX = arc.centerX + arc.radius * arc.cosTheta; arc.endX = arc.centerX + arc.radius * arc.cosTheta;
arc.endY = arc.centerY - arc.radius * arc.sinTheta; arc.endY = arc.centerY - arc.radius * arc.sinTheta;
// Overflow.
arc = Arc.overflow(arc, bounds); arc = Arc.overflow(arc, bounds);
return arc; return arc;
@ -119,13 +118,7 @@ const Arc = {
return arc; return arc;
}, },
match: function(arc, arcToMatch) {
},
follow: function(arc, arcToFollow) { follow: function(arc, arcToFollow) {
arc = (arc.clockwise !== arcToFollow.clockwise ? Arc.reverse(arc) : arc);
const prevD = Math.pow( const prevD = Math.pow(
Math.pow(arcToFollow.endX - arc.prevEndX, 2) + Math.pow(arcToFollow.endX - arc.prevEndX, 2) +
Math.pow(arcToFollow.endY - arc.prevEndY, 2) Math.pow(arcToFollow.endY - arc.prevEndY, 2)
@ -136,25 +129,30 @@ const Arc = {
Math.pow(arcToFollow.endY - arc.endY, 2) Math.pow(arcToFollow.endY - arc.endY, 2)
, 0.5); , 0.5);
// "How much of movement is in the correct direction" const rigidityCoeff = (prevD - currD) / arc.speed;
const ratio = (prevD - currD) / arc.speed;
// TODO adjust speed if (currD < sensitivity) {
// TODO turn in the correct direction arc = (arc.clockwise !== arcToFollow.clockwise ? Arc.reverse(arc) : arc);
if (currD < 20) {
// if (Math.abs(arc.centerX - arcToFollow.centerX) < 10 && Math.abs(arc.centerY - arcToFollow.centerX) < 10) {
arc = Arc.changeRadius(arc, arcToFollow.radius); arc = Arc.changeRadius(arc, arcToFollow.radius);
if (arc.speed > arcToFollow.speed) { if (arc.speed > arcToFollow.speed) {
arc = Arc.changeSpeed(arc, arc.speed - 1); arc = Arc.changeSpeed(arc, arc.speed - 1);
} }
} else if (ratio < 0.8) {
// arc = (ratio < 0 ? Arc.reverse(arc) : arc); if (arc.speed < arcToFollow.speed) {
arc = Arc.changeSpeed(arc, arc.speed + 1);
}
} else if (rigidityCoeff < rigidity) {
arc = Arc.changeRadius(arc, 20); arc = Arc.changeRadius(arc, 20);
if (arc.speed > arcToFollow.speed) {
arc = Arc.changeSpeed(arc, arc.speed - 1);
}
} else { } else {
arc = Arc.changeRadius(arc, 400); arc = Arc.changeRadius(arc, 4000);
if (arc.speed < (arcToFollow.speed + 2)) { if (arc.speed < arcToFollow.speed + 2) {
arc = Arc.changeSpeed(arc, arc.speed + 1); arc = Arc.changeSpeed(arc, arc.speed + 1);
} }
} }
@ -163,8 +161,9 @@ const Arc = {
}, },
evade: function(arc) { evade: function(arc) {
arc = Arc.changeRadius(arc, 20); // Randomness here mitigates sticking in corners and between walls.
arc.length = 1; arc = Arc.changeRadius(arc, Random.num(15, 30));
arc.length = 0.3;
return arc; return arc;
} }

File diff suppressed because one or more lines are too long

13
js/controls.js vendored

@ -16,7 +16,7 @@ export default function(destroy$, {
const observables = { const observables = {
fps$: new Rx.Subject(), fps$: new Rx.Subject(),
count$: createCountControl(container, 0, maxCount), count$: createCountControl(container, count, maxCount),
speed$: createSpeedControl(container), speed$: createSpeedControl(container),
circle$: showCircleControl && createCircleControl(container), circle$: showCircleControl && createCircleControl(container),
randomize$: showRandomizeControl && createRandomizeControl(container), randomize$: showRandomizeControl && createRandomizeControl(container),
@ -27,10 +27,7 @@ export default function(destroy$, {
observables.animating$.subscribe((isAnimating) => { observables.animating$.subscribe((isAnimating) => {
if (isAnimating === true) { if (isAnimating === true) {
destroy$.next(id); destroy$.next(id);
observables.count$.next(count);
if (observables.count$.getValue() === 0) {
observables.count$.next(count);
}
} }
}); });
@ -80,7 +77,7 @@ function createCountControl(container, initialValue, max) {
label.className = 'controls-range'; label.className = 'controls-range';
const text = document.createElement('span'); const text = document.createElement('span');
text.innerHTML = (initialValue == 1) ? '1 particle' : `${initialValue} particles`; // text.innerHTML = (initialValue == 1) ? '1 particle' : `${initialValue} particles`;
text.className = 'controls-range-text'; text.className = 'controls-range-text';
const slider = document.createElement('input'); const slider = document.createElement('input');
@ -102,7 +99,7 @@ function createCountControl(container, initialValue, max) {
}); });
count$.subscribe((value) => { count$.subscribe((value) => {
text.innerHTML = (value == 1) ? '1 particle' : `${value} particles`; text.innerHTML = (value === 1) ? '1 particle' : `${value} particles`;
}); });
return count$; return count$;
@ -119,7 +116,7 @@ function createSpeedControl(container) {
const slider = document.createElement('input'); const slider = document.createElement('input');
slider.type = 'range'; slider.type = 'range';
slider.min = 1; slider.min = 1;
slider.max = 15; slider.max = 10;
slider.value = 4; slider.value = 4;
slider.className = 'controls-range-input'; slider.className = 'controls-range-input';

@ -5,6 +5,7 @@ import Animation2a from './animation2a';
import Animation2b from './animation2b'; import Animation2b from './animation2b';
import Animation3a from './animation3a'; import Animation3a from './animation3a';
import Animation3b from './animation3b'; import Animation3b from './animation3b';
import Animation3c from './animation3c';
require('../css/reset.scss'); require('../css/reset.scss');
require('../css/index.scss'); require('../css/index.scss');
@ -14,26 +15,18 @@ require('../css/controls.scss');
window.addEventListener('load', () => { window.addEventListener('load', () => {
const destroy$ = new Rx.BehaviorSubject(null); const destroy$ = new Rx.BehaviorSubject(null);
// window.addEventListener('blur', () => {
// destroy$.next('all');
// });
Animation1a(destroy$); Animation1a(destroy$);
Animation1b(destroy$); Animation1b(destroy$);
Animation2a(destroy$); Animation2a(destroy$);
Animation2b(destroy$); Animation2b(destroy$);
Animation3a(destroy$); Animation3a(destroy$);
Animation3b(destroy$); Animation3b(destroy$);
Animation3c(destroy$);
}); });
// TODO remove bottom padding from Disqus // TODO remove bottom padding from Disqus
// TODO fix "hangup" small radius evade bug // TODO TURN THE CORRECT DIRECTION - HUGE EFFICIENCY INCREASE
// TODO sort out particle nextframe
// TODO abs positioning on controls elements so order doesn't matter
// TODO BehaviorSubject listener on bounds change
// TODO are vision grid nodes removed properly
// TODO grid touches
// TODO start with n particles doesn't update slider
// TODO leader not quite right, if 2 particles, sometimes ignored
// TODO ANIM1ab free movement
// TODO ANIM3a streamline updateLeader
// TODO ANIM3b separation
// TODO ANIM3c alignment

@ -5,9 +5,9 @@ import Random from './random';
// ===== Constructor ===== // ===== Constructor =====
function Particle(parent, bounds, globalGrid, observables) { function Particle(parent, bounds, globalGrid, observables, behavior) {
this.config = { this.config = {
behavior: BEHAVIOR.COHESION, behavior: behavior || BEHAVIOR.FREE,
bounds, bounds,
color: Random.color(), color: Random.color(),
gridSize: 5, gridSize: 5,
@ -35,7 +35,7 @@ function Particle(parent, bounds, globalGrid, observables) {
parent.appendChild(this.nodes.container); parent.appendChild(this.nodes.container);
this.leader = null; this.leader = null;
this.isLeader = false; this.leaderTime = 0;
this.arc = Arc.create(bounds, this.grids.global); this.arc = Arc.create(bounds, this.grids.global);
this.arc.length = 3; this.arc.length = 3;
@ -53,9 +53,9 @@ function Particle(parent, bounds, globalGrid, observables) {
this.remove$ = new Rx.Subject(); this.remove$ = new Rx.Subject();
observables.fps$ const frames = observables.fps$.takeUntil(this.remove$);
.takeUntil(this.remove$) frames.subscribe(this.subscribeFrameMove.bind(this));
.subscribe(this.subscribeNextFrame.bind(this)); frames.subscribe(this.subscribeFrameRepaint.bind(this));
observables.speed$ observables.speed$
.takeUntil(this.remove$) .takeUntil(this.remove$)
@ -89,7 +89,7 @@ Particle.prototype.remove = function() {
delete this.nodes; delete this.nodes;
} }
Particle.prototype.subscribeNextFrame = function() { Particle.prototype.subscribeFrameMove = function(n) {
this.grids.global.deletePoint({ this.grids.global.deletePoint({
x: this.arc.endX, x: this.arc.endX,
y: this.arc.endY, y: this.arc.endY,
@ -102,25 +102,27 @@ Particle.prototype.subscribeNextFrame = function() {
this.arc = Arc.randomize(this.arc); this.arc = Arc.randomize(this.arc);
} }
this.arc = Arc.step(this.arc, this.config.bounds);
this.grids.vision = updateVisionGrid(this.arc, this.config, this.grids); this.grids.vision = updateVisionGrid(this.arc, this.config, this.grids);
const { hazards, particles } = look(this.arc, this.grids); const { hazards, particles } = look(this.arc, this.grids);
this.updateLeader(particles);
if (hazards.length > 0) { if (hazards.length > 0) {
// this.arc = Arc.evade(this.arc); this.arc = Arc.evade(this.arc);
} }
this.updateLeader(particles); this.arc = Arc.step(this.arc, this.config.bounds);
this.grids.global.setPoint({ this.grids.global.setPoint({
x: this.arc.endX, x: this.arc.endX,
y: this.arc.endY, y: this.arc.endY,
type: ENTITIES.PARTICLE type: ENTITIES.PARTICLE
}, this); }, this);
}
repaintContainer(this.nodes.container, this.arc); Particle.prototype.subscribeFrameRepaint = function(n) {
repaintBody(this.nodes.body, this.arc, this.isLeader); repaintContainer(this.nodes.container, this.arc, this.leaderTime);
repaintBody(this.nodes.body, this.arc, this.leaderTime);
repaintCircle(this.nodes.circle, this.arc); repaintCircle(this.nodes.circle, this.arc);
repaintVisionGrid(this.nodes.visionGrid, this.arc, this.grids); repaintVisionGrid(this.nodes.visionGrid, this.arc, this.grids);
} }
@ -157,36 +159,47 @@ Particle.prototype.updateLeader = function(particles) {
return; return;
} }
// Head-to-head: particles see eachother but shouldn't both lead.
if (this.leader === null && particles.length > 0) { if (this.leader === null && particles.length > 0) {
// Head-to-head: particles see eachother but shouldn't both lead.
const candidates = particles const candidates = particles
.filter(v => v.leader ? (v.leader.id !== this.id) : true); .filter(v => v.leader ? (v.leader.id !== this.id) : true);
const leader = candidates.find(v => v.isLeader) || candidates[0]; const leader = candidates.find(v => (v.leaderTime > 0)) || candidates[0];
if (leader !== undefined) { if (leader !== undefined) {
leader.isLeader = true; leader.leaderTime = 1;
this.leaderTime = 0;
this.leader = leader; this.leader = leader;
} }
} }
if (this.leader === null) { if (this.leader === null) {
if (this.leaderTime > 0) {
this.leaderTime++;
}
// Reset leader after a bit. (320 frames is 10 seconds)
if (this.leaderTime > 3000) {
this.leaderTime = 0;
}
// This particle may now be leading - break execution.
return; return;
} }
if (this.leader.nodes === undefined) { // if (this.leader.nodes === undefined) {
if (this.leader.leaderTime === 0 && this.leader.leader === null) {
this.leader = null; this.leader = null;
return; return;
} }
this.leaderTime = 0;
// Visit next node to keep chains of leaders short.
if (this.leader.leader !== null) { if (this.leader.leader !== null) {
this.leader = this.leader.leader; this.leader = this.leader.leader;
} }
if (this.isLeader) {
this.isLeader = false;
}
// Beware of circular leadership, where a leader sees its tail. // Beware of circular leadership, where a leader sees its tail.
if (this.leader.id === this.id) { if (this.leader.id === this.id) {
this.leader = null; this.leader = null;
@ -197,16 +210,20 @@ function look(arc, grids) {
const { global, vision } = grids; const { global, vision } = grids;
return vision.reduce((acc, point) => { return vision.reduce((acc, point) => {
point.touch = false;
const x = arc.endX + point.x; const x = arc.endX + point.x;
const y = arc.endY + point.y; const y = arc.endY + point.y;
const p = global.getPoint({ x, y, type: ENTITIES.PARTICLE }); const p = global.getPoint({ x, y, type: ENTITIES.PARTICLE });
if (p) { if (p) {
acc.particles.push(p); acc.particles.push(p);
point.touch = true;
} }
if (global.getPoint({ x, y, type: ENTITIES.HAZARD })) { if (global.getPoint({ x, y, type: ENTITIES.HAZARD })) {
acc.hazards.push({ x, y }); acc.hazards.push({ x, y });
point.touch = true;
} }
return acc; return acc;
@ -298,19 +315,21 @@ function updateVisionGrid(arc, config, grids) {
} }
// ===== DOM RENDERING ===== // ===== DOM RENDERING =====
function repaintContainer(node, arc) { function repaintContainer(node, arc, leaderTime) {
node.style.left = `${arc.endX}px`; node.style.left = `${arc.endX}px`;
node.style.top = `${arc.endY}px`; node.style.top = `${arc.endY}px`;
// node.style.zIndex = (leaderTime > 0 ? 2000 : 2);
} }
function repaintBody(node, arc, isLeader) { function repaintBody(node, arc, leaderTime) {
const rad = arc.clockwise const rad = arc.clockwise
? RAD.t180 - arc.theta ? RAD.t180 - arc.theta
: RAD.t360 - arc.theta; : RAD.t360 - arc.theta;
node.style.transform = `rotate(${rad + RAD.t45}rad)`; node.style.transform = `rotate(${rad + RAD.t45}rad)`;
isLeader ? node.style.outline = '1px solid red' : node.style.outline = ''; // node.style.border = (leaderTime > 0 ? '3px dotted #fff' : '');
} }
function repaintCircle(node, arc) { function repaintCircle(node, arc) {
@ -336,7 +355,7 @@ function repaintVisionGrid(nodes, arc, grids) {
nodes[i].style.left = `${x}px`; nodes[i].style.left = `${x}px`;
nodes[i].style.top = `${y}px`; nodes[i].style.top = `${y}px`;
nodes[i].style.border = (touch ? '2px solid red' : '0'); nodes[i].style.border = (touch ? '1px solid red' : '0');
}); });
} }

Loading…
Cancel
Save