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>Support large swarms of particles</li>
<li>Be able to evade obstacles</li>
<li>Exhibit flocking behavior</li>
<li>Simulate cohesive flocking behavior</li>
</ul>
</blockquote>
@ -67,23 +67,36 @@
<div class='outerContainer' id='2b'></div>
<p>
The last goal was to play around with flocking behaviors: cohesion, separation, and alignment.
Here's each of the three, with the vision grid visualized:
<h4>Exploration 3: Cohesive Flocking Behavior</h4>
</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>
<div class='outerContainer' id='3a'></div>
<p>
The exploration is now complete: arc-based movement, independent AIs, grid-based vision,
following patterns of cohesion, separation, alignment, with hazards, on a large scale:
On a larger scale, the particles are starting to take on a personality.
</p>
<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='/core/js/ui.js'></script>
</body>

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

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

@ -10,5 +10,5 @@ export default function(destroy$) {
};
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 config = {
id,
count: 200,
maxCount: 1000
};
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 Controls from './controls';
import { BEHAVIOR } from './enums';
export default function(destroy$) {
const id = '3a';
const config = {
id,
count: 5,
maxCount: 1000,
showAlignmentControl: true,
showCohesionControl: true,
showSeparationControl: true,
// showVisionGridControl: true
maxCount: 10,
showVisionGridControl: true
};
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 Controls from './controls';
import { BEHAVIOR } from './enums';
export default function(destroy$) {
const id = '3b';
const config = {
id,
maxCount: 10,
showAlignmentControl: true,
showCohesionControl: true,
showSeparationControl: true
count: 200,
maxCount: 1000
};
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 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 = {
create: function(bounds, grid) {
create: function(bounds) {
let arc = {
centerX: Random.num(0, bounds.width),
centerY: Random.num(0, bounds.height),
@ -25,11 +30,6 @@ const Arc = {
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;
},
@ -50,7 +50,6 @@ const Arc = {
arc.endX = arc.centerX + arc.radius * arc.cosTheta;
arc.endY = arc.centerY - arc.radius * arc.sinTheta;
// Overflow.
arc = Arc.overflow(arc, bounds);
return arc;
@ -119,13 +118,7 @@ const Arc = {
return arc;
},
match: function(arc, arcToMatch) {
},
follow: function(arc, arcToFollow) {
arc = (arc.clockwise !== arcToFollow.clockwise ? Arc.reverse(arc) : arc);
const prevD = Math.pow(
Math.pow(arcToFollow.endX - arc.prevEndX, 2) +
Math.pow(arcToFollow.endY - arc.prevEndY, 2)
@ -136,25 +129,30 @@ const Arc = {
Math.pow(arcToFollow.endY - arc.endY, 2)
, 0.5);
// "How much of movement is in the correct direction"
const ratio = (prevD - currD) / arc.speed;
const rigidityCoeff = (prevD - currD) / arc.speed;
// TODO adjust speed
// TODO turn in the correct direction
if (currD < 20) {
// if (Math.abs(arc.centerX - arcToFollow.centerX) < 10 && Math.abs(arc.centerY - arcToFollow.centerX) < 10) {
if (currD < sensitivity) {
arc = (arc.clockwise !== arcToFollow.clockwise ? Arc.reverse(arc) : arc);
arc = Arc.changeRadius(arc, arcToFollow.radius);
if (arc.speed > arcToFollow.speed) {
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);
if (arc.speed > arcToFollow.speed) {
arc = Arc.changeSpeed(arc, arc.speed - 1);
}
} 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);
}
}
@ -163,8 +161,9 @@ const Arc = {
},
evade: function(arc) {
arc = Arc.changeRadius(arc, 20);
arc.length = 1;
// Randomness here mitigates sticking in corners and between walls.
arc = Arc.changeRadius(arc, Random.num(15, 30));
arc.length = 0.3;
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 = {
fps$: new Rx.Subject(),
count$: createCountControl(container, 0, maxCount),
count$: createCountControl(container, count, maxCount),
speed$: createSpeedControl(container),
circle$: showCircleControl && createCircleControl(container),
randomize$: showRandomizeControl && createRandomizeControl(container),
@ -27,10 +27,7 @@ export default function(destroy$, {
observables.animating$.subscribe((isAnimating) => {
if (isAnimating === true) {
destroy$.next(id);
if (observables.count$.getValue() === 0) {
observables.count$.next(count);
}
observables.count$.next(count);
}
});
@ -80,7 +77,7 @@ function createCountControl(container, initialValue, max) {
label.className = 'controls-range';
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';
const slider = document.createElement('input');
@ -102,7 +99,7 @@ function createCountControl(container, initialValue, max) {
});
count$.subscribe((value) => {
text.innerHTML = (value == 1) ? '1 particle' : `${value} particles`;
text.innerHTML = (value === 1) ? '1 particle' : `${value} particles`;
});
return count$;
@ -119,7 +116,7 @@ function createSpeedControl(container) {
const slider = document.createElement('input');
slider.type = 'range';
slider.min = 1;
slider.max = 15;
slider.max = 10;
slider.value = 4;
slider.className = 'controls-range-input';

@ -5,6 +5,7 @@ import Animation2a from './animation2a';
import Animation2b from './animation2b';
import Animation3a from './animation3a';
import Animation3b from './animation3b';
import Animation3c from './animation3c';
require('../css/reset.scss');
require('../css/index.scss');
@ -14,26 +15,18 @@ require('../css/controls.scss');
window.addEventListener('load', () => {
const destroy$ = new Rx.BehaviorSubject(null);
// window.addEventListener('blur', () => {
// destroy$.next('all');
// });
Animation1a(destroy$);
Animation1b(destroy$);
Animation2a(destroy$);
Animation2b(destroy$);
Animation3a(destroy$);
Animation3b(destroy$);
Animation3c(destroy$);
});
// TODO remove bottom padding from Disqus
// TODO fix "hangup" small radius evade bug
// 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
// TODO TURN THE CORRECT DIRECTION - HUGE EFFICIENCY INCREASE

@ -5,9 +5,9 @@ import Random from './random';
// ===== Constructor =====
function Particle(parent, bounds, globalGrid, observables) {
function Particle(parent, bounds, globalGrid, observables, behavior) {
this.config = {
behavior: BEHAVIOR.COHESION,
behavior: behavior || BEHAVIOR.FREE,
bounds,
color: Random.color(),
gridSize: 5,
@ -35,7 +35,7 @@ function Particle(parent, bounds, globalGrid, observables) {
parent.appendChild(this.nodes.container);
this.leader = null;
this.isLeader = false;
this.leaderTime = 0;
this.arc = Arc.create(bounds, this.grids.global);
this.arc.length = 3;
@ -53,9 +53,9 @@ function Particle(parent, bounds, globalGrid, observables) {
this.remove$ = new Rx.Subject();
observables.fps$
.takeUntil(this.remove$)
.subscribe(this.subscribeNextFrame.bind(this));
const frames = observables.fps$.takeUntil(this.remove$);
frames.subscribe(this.subscribeFrameMove.bind(this));
frames.subscribe(this.subscribeFrameRepaint.bind(this));
observables.speed$
.takeUntil(this.remove$)
@ -89,7 +89,7 @@ Particle.prototype.remove = function() {
delete this.nodes;
}
Particle.prototype.subscribeNextFrame = function() {
Particle.prototype.subscribeFrameMove = function(n) {
this.grids.global.deletePoint({
x: this.arc.endX,
y: this.arc.endY,
@ -102,25 +102,27 @@ Particle.prototype.subscribeNextFrame = function() {
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);
const { hazards, particles } = look(this.arc, this.grids);
this.updateLeader(particles);
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({
x: this.arc.endX,
y: this.arc.endY,
type: ENTITIES.PARTICLE
}, this);
}
repaintContainer(this.nodes.container, this.arc);
repaintBody(this.nodes.body, this.arc, this.isLeader);
Particle.prototype.subscribeFrameRepaint = function(n) {
repaintContainer(this.nodes.container, this.arc, this.leaderTime);
repaintBody(this.nodes.body, this.arc, this.leaderTime);
repaintCircle(this.nodes.circle, this.arc);
repaintVisionGrid(this.nodes.visionGrid, this.arc, this.grids);
}
@ -157,36 +159,47 @@ Particle.prototype.updateLeader = function(particles) {
return;
}
// Head-to-head: particles see eachother but shouldn't both lead.
if (this.leader === null && particles.length > 0) {
// Head-to-head: particles see eachother but shouldn't both lead.
const candidates = particles
.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) {
leader.isLeader = true;
leader.leaderTime = 1;
this.leaderTime = 0;
this.leader = leader;
}
}
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;
}
if (this.leader.nodes === undefined) {
// if (this.leader.nodes === undefined) {
if (this.leader.leaderTime === 0 && this.leader.leader === null) {
this.leader = null;
return;
}
this.leaderTime = 0;
// Visit next node to keep chains of leaders short.
if (this.leader.leader !== null) {
this.leader = this.leader.leader;
}
if (this.isLeader) {
this.isLeader = false;
}
// Beware of circular leadership, where a leader sees its tail.
if (this.leader.id === this.id) {
this.leader = null;
@ -197,16 +210,20 @@ function look(arc, grids) {
const { global, vision } = grids;
return vision.reduce((acc, point) => {
point.touch = false;
const x = arc.endX + point.x;
const y = arc.endY + point.y;
const p = global.getPoint({ x, y, type: ENTITIES.PARTICLE });
if (p) {
acc.particles.push(p);
point.touch = true;
}
if (global.getPoint({ x, y, type: ENTITIES.HAZARD })) {
acc.hazards.push({ x, y });
point.touch = true;
}
return acc;
@ -298,19 +315,21 @@ function updateVisionGrid(arc, config, grids) {
}
// ===== DOM RENDERING =====
function repaintContainer(node, arc) {
function repaintContainer(node, arc, leaderTime) {
node.style.left = `${arc.endX}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
? RAD.t180 - arc.theta
: RAD.t360 - arc.theta;
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) {
@ -336,7 +355,7 @@ function repaintVisionGrid(nodes, arc, grids) {
nodes[i].style.left = `${x}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