Hazard avoidance restored.

master
Ben Burlingham 8 years ago
parent 779afc5aa6
commit 529db9b626
  1. 4
      index.html
  2. 11
      js/animation1a.js
  3. 9
      js/animation1b.js
  4. 60
      js/animation2a.js
  5. 58
      js/animation2b.js
  6. 29
      js/animation3a.js
  7. 58
      js/arc.js
  8. 31
      js/bundle.js
  9. 69
      js/grid.js
  10. 17
      js/index.js
  11. 137
      js/particle.js
  12. 4
      js/random.js

@ -24,7 +24,7 @@
<hr>
<!-- <p>
<p>
<h4>Exploration 1: Organic movement at 32fps</h4>
</p>
@ -70,7 +70,7 @@
<div class='outerContainer'>
<div id="animation2b" class='animationContainer'></div>
<div id="controls2b" class='controlsContainer'></div>
</div> -->
</div>
<p>
The last goal was to play around with flocking behavior. Cohesion:

@ -1,6 +1,6 @@
import Rx, { Observable } from 'rxjs';
import Grid from './grid';
import Particle from './particle';
import Store from './store';
import Controls from './controls';
import { CONTROLS } from './enums';
@ -14,13 +14,12 @@ function Animation1a() {
};
this.container = document.getElementById('animation1a');
this.bounds = this.container.getBoundingClientRect();
this.particles = [];
this.grid = new Grid();
this.movementCircleCtrl = createMovementCircleControl();
this.randomizeCtrl = createRandomizeControl();
this.particles = [];
const controls = new Controls(
document.getElementById('controls1a'),
this.options,
@ -105,12 +104,14 @@ Animation1a.prototype.updateAnimating = function(isAnimating) {
}
Animation1a.prototype.updateCount = function(count) {
const bounds = this.container.getBoundingClientRect();
while (this.particles.length > count) {
delete this.particles.pop().remove();
}
while (this.particles.length < count) {
const p = new Particle(this.container, this.bounds, this.options);
const p = new Particle(this.container, bounds, this.options, this.grid);
this.particles.push(p);
}
}

@ -1,6 +1,6 @@
import Rx, { Observable } from 'rxjs';
import Grid from './grid';
import Particle from './particle';
import Store from './store';
import Controls from './controls';
import { CONTROLS } from './enums';
@ -12,9 +12,8 @@ function Animation1b() {
};
this.container = document.getElementById('animation1b');
this.bounds = this.container.getBoundingClientRect();
this.particles = [];
this.grid = new Grid();
const controls = new Controls(
document.getElementById('controls1b'),
@ -50,12 +49,14 @@ Animation1b.prototype.updateAnimating = function(isAnimating) {
}
Animation1b.prototype.updateCount = function(count) {
const bounds = this.container.getBoundingClientRect();
while (this.particles.length > count) {
delete this.particles.pop().remove();
}
while (this.particles.length < count) {
const p = new Particle(this.container, this.bounds, this.options);
const p = new Particle(this.container, bounds, this.options, this.grid);
this.particles.push(p);
}
}

@ -1,22 +1,20 @@
import Rx, { Observable } from 'rxjs';
import Particle from './particle';
import Store from './store';
import Grid from './grid';
import Controls from './controls';
import { CONTROLS } from './enums';
import { CONTROLS, ENTITIES } from './enums';
function Animation2a() {
this.options = {
count: 1,
count: 3,
maxCount: 10,
showVisionGrid: true,
speed: 4
};
this.container = document.getElementById('animation2a');
this.bounds = this.container.getBoundingClientRect();
this.particles = [];
this.globalGrid = createGlobalGrid(this.container, this.bounds);
this.grid = createGlobalGrid(this.container, this.bounds);
const controls = new Controls(
document.getElementById('controls2a'),
@ -53,12 +51,14 @@ Animation2a.prototype.updateAnimating = function(isAnimating) {
}
Animation2a.prototype.updateCount = function(count) {
const bounds = this.container.getBoundingClientRect();
while (this.particles.length > count) {
delete this.particles.pop().remove();
}
while (this.particles.length < count) {
const p = new Particle(this.container, this.bounds, this.options, this.globalGrid);
const p = new Particle(this.container, bounds, this.options, this.grid);
this.particles.push(p);
}
}
@ -69,46 +69,12 @@ Animation2a.prototype.updateSpeed = function(value) {
}
function createGlobalGrid(container, bounds) {
const grid = {};
const gridSize = 5;
const hazards = [
{ x: 100, y: 100, w: 200, h: 200 },
{ x: 600, y: 200, w: 200, h: 200 },
];
return hazards.reduce((acc, { x, y, w, h }) => {
const div = document.createElement('div');
div.className = 'hazard';
div.style.left = `${x}px`;
div.style.top = `${y}px`;
div.style.height = `${h}px`;
div.style.width = `${w}px`;
container.appendChild(div);
for (let i = x; i <= (x + w); i += gridSize) {
for (let j = y; j <= (y + h); j += gridSize) {
if (acc[i] === undefined) {
acc[i] = {};
}
if (acc[i][j] !== undefined) {
continue;
}
const dot = document.createElement('dot');
dot.className = 'hazard-dot';
dot.style.left = `${i - x}px`;
dot.style.top = `${j - y}px`;
div.appendChild(dot);
acc[i][j] = true;
}
}
return acc;
}, {});
const grid = new Grid();
grid.setArea({ x: 100, y: 100, w: 200, h: 200, type: ENTITIES.HAZARD }, container);
grid.setArea({ x: 600, y: 200, w: 200, h: 200, type: ENTITIES.HAZARD }, container);
return grid;
}
export default Animation2a;

@ -1,8 +1,8 @@
import Rx, { Observable } from 'rxjs';
import Particle from './particle';
import Store from './store';
import Grid from './grid';
import Controls from './controls';
import { CONTROLS } from './enums';
import { CONTROLS, ENTITIES } from './enums';
function Animation2b() {
this.options = {
@ -12,10 +12,8 @@ function Animation2b() {
};
this.container = document.getElementById('animation2b');
this.bounds = this.container.getBoundingClientRect();
this.particles = [];
this.globalGrid = createGlobalGrid(this.container, this.bounds);
this.grid = createGlobalGrid(this.container, this.bounds);
const controls = new Controls(
document.getElementById('controls2b'),
@ -52,12 +50,14 @@ Animation2b.prototype.updateAnimating = function(isAnimating) {
}
Animation2b.prototype.updateCount = function(count) {
const bounds = this.container.getBoundingClientRect();
while (this.particles.length > count) {
delete this.particles.pop().remove();
}
while (this.particles.length < count) {
const p = new Particle(this.container, this.bounds, this.options, this.globalGrid);
const p = new Particle(this.container, bounds, this.options, this.grid);
this.particles.push(p);
}
}
@ -68,46 +68,12 @@ Animation2b.prototype.updateSpeed = function(value) {
}
function createGlobalGrid(container, bounds) {
const grid = {};
const gridSize = 5;
const hazards = [
{ x: 100, y: 100, w: 200, h: 200 },
{ x: 600, y: 200, w: 200, h: 200 },
];
return hazards.reduce((acc, { x, y, w, h }) => {
const div = document.createElement('div');
div.className = 'hazard';
div.style.left = `${x}px`;
div.style.top = `${y}px`;
div.style.height = `${h}px`;
div.style.width = `${w}px`;
container.appendChild(div);
for (let i = x; i <= (x + w); i += gridSize) {
for (let j = y; j <= (y + h); j += gridSize) {
if (acc[i] === undefined) {
acc[i] = {};
}
if (acc[i][j] !== undefined) {
continue;
}
const dot = document.createElement('dot');
dot.className = 'hazard-dot';
dot.style.left = `${i - x}px`;
dot.style.top = `${j - y}px`;
div.appendChild(dot);
acc[i][j] = true;
}
}
return acc;
}, {});
const grid = new Grid();
grid.setArea({ x: 100, y: 100, w: 200, h: 200, type: ENTITIES.HAZARD }, container);
grid.setArea({ x: 600, y: 200, w: 200, h: 200, type: ENTITIES.HAZARD }, container);
return grid;
}
export default Animation2b;

@ -7,7 +7,7 @@ import { CONTROLS, ENTITIES } from './enums';
function Animation3a() {
this.options = {
cohesion: true,
count: 10,
count: 400,
maxCount: 1000,
showVisionGrid: false,
speed: 4
@ -15,7 +15,7 @@ function Animation3a() {
this.container = document.getElementById('animation3a');
this.particles = [];
this.globalGrid = new Grid();
this.grid = new Grid();
const controls = new Controls(
document.getElementById('controls3a'),
@ -27,17 +27,15 @@ function Animation3a() {
this.updateAnimating(this.options.animating);
this.updateCount(this.options.count);
// TODO get "top" leader, once...don't update each time?
// TODO if leader sees follower, assign to this leader?
// TODO remove bottom padding from Disqus
// TODO ANIM2ab randomize hazards
// TODO fix "hangup" small radius evade bug
// TODO don't load simulation until requested
// TODO sort out particle nextframe
// TODO ANIM3a perf Scale vision grid to 1000 particles
// TODO hazard grid, particles grid
// TODO completely seal Particle
// TODO ANIM1ab free movement
// TODO ANIM2ab randomize hazards
// TODO ANIM3a cohesion
// TODO ANIM3a streamline updateLeader
// TODO ANIM3b separation
// TODO ANIM3c alignment
};
@ -55,10 +53,10 @@ Animation3a.prototype.nextFrame = function() {
const prevX = p.arc.endX;
const prevY = p.arc.endY;
p.nextFrame(this.globalGrid);
p.nextFrame();
this.globalGrid.deletePoint({ x: prevX, y: prevY, type: ENTITIES.PARTICLE });
this.globalGrid.setPoint({ x: p.arc.endX, y: p.arc.endY, type: ENTITIES.PARTICLE }, p);
this.grid.deletePoint({ x: prevX, y: prevY, type: ENTITIES.PARTICLE });
this.grid.setPoint({ x: p.arc.endX, y: p.arc.endY, type: ENTITIES.PARTICLE }, p);
});
}
@ -77,11 +75,14 @@ Animation3a.prototype.updateCount = function(count) {
const bounds = this.container.getBoundingClientRect();
while (this.particles.length > count) {
delete this.particles.pop().remove();
const p = this.particles.pop();
this.grid.deletePoint({ x: p.arc.endX, y: p.arc.endY, type: ENTITIES.PARTICLE });
p.remove();
}
while (this.particles.length < count) {
const p = new Particle(this.container, bounds, this.options, this.globalGrid);
const p = new Particle(this.container, bounds, this.options, this.grid);
this.grid.setPoint({ x: p.arc.endX, y: p.arc.endY, type: ENTITIES.PARTICLE }, p);
this.particles.push(p);
}
}

@ -1,8 +1,8 @@
import { RAD } from './enums';
import { ENTITIES, RAD } from './enums';
import Random from './random';
const Arc = {
create: function(bounds, grids) {
create: function(bounds, grid) {
let arc = {
centerX: Random.num(0, bounds.width),
centerY: Random.num(0, bounds.height),
@ -14,18 +14,18 @@ const Arc = {
theta: Random.num(RAD.t90, RAD.t360)
};
arc.endX = arc.centerX + arc.radius * Math.cos(arc.theta);
arc.endY = arc.centerY - arc.radius * Math.sin(arc.theta);
arc.cosTheta = Math.cos(arc.theta);
arc.sinTheta = Math.sin(arc.theta);
arc = Arc.overflow(arc, bounds);
arc.endX = arc.centerX + arc.radius * arc.cosTheta;
arc.endY = arc.centerY - arc.radius * arc.sinTheta;
const x = arc.endX - arc.endX % 5;
const y = arc.endY - arc.endY % 5;
arc = Arc.overflow(arc, bounds);
// If starting in a hazard, recurse.
// if (grids.global[x] !== undefined && grids.global[x][y] !== undefined) {
// arc = createArc(bounds, grids);
// }
if (grid.getPoint({ x: arc.endX, y: arc.endY, type: ENTITIES.HAZARD })) {
arc = Arc.create(bounds, grid);
}
return arc;
},
@ -38,8 +38,11 @@ const Arc = {
arc.theta += (arc.clockwise ? -delta : +delta);
arc.theta = (arc.theta > 0 ? arc.theta % RAD.t360 : RAD.t360 + arc.theta);
arc.endX = arc.centerX + arc.radius * Math.cos(arc.theta); // TODO perf here
arc.endY = arc.centerY - arc.radius * Math.sin(arc.theta); // TODO perf here
arc.cosTheta = Math.cos(arc.theta);
arc.sinTheta = Math.sin(arc.theta);
arc.endX = arc.centerX + arc.radius * arc.cosTheta;
arc.endY = arc.centerY - arc.radius * arc.sinTheta;
// Overflow.
arc = Arc.overflow(arc, bounds);
@ -84,8 +87,8 @@ const Arc = {
const r1 = newRadius;
// Moves arc center to new radius while keeping theta constant.
arc.centerX -= (r1 - r0) * Math.cos(arc.theta); // TODO perf here
arc.centerY += (r1 - r0) * Math.sin(arc.theta); // TODO perf here
arc.centerX -= (r1 - r0) * arc.cosTheta;
arc.centerY += (r1 - r0) * arc.sinTheta;
arc.radius = r1;
return arc;
@ -96,8 +99,11 @@ const Arc = {
arc.theta = (arc.theta + RAD.t180) % RAD.t360;
arc.centerX -= (2 * arc.radius) * Math.cos(arc.theta); // TODO perf here
arc.centerY += (2 * arc.radius) * Math.sin(arc.theta); // TODO perf here
arc.cosTheta = Math.cos(arc.theta);
arc.sinTheta = Math.sin(arc.theta);
arc.centerX -= (2 * arc.radius) * arc.cosTheta;
arc.centerY += (2 * arc.radius) * arc.sinTheta;
return arc;
},
@ -107,8 +113,8 @@ const Arc = {
arc = Arc.reverse(arc);
}
if (Math.abs(arc.theta - arcToFollow.theta) > 0.1) {
arc = Arc.changeRadius(arc, 20);
if (Math.abs(arc.theta - arcToFollow.theta) > 0.2) {
arc = Arc.changeRadius(arc, 50);
} else {
arc = Arc.changeRadius(arc, arcToFollow.radius);
}
@ -116,17 +122,11 @@ const Arc = {
return arc;
},
evade: function(arc, visionGrid) {
// const danger = visionGrid.reduce((acc, v) => acc || v.touch, false);
//
// if (danger === false) {
// return arc;
// }
//
// const evasionArc = moveArc(arc, 20);
// evasionArc.length = 1;
//
// return evasionArc;
evade: function(arc) {
arc = Arc.changeRadius(arc, 20);
arc.length = 1;
return arc;
}
}

File diff suppressed because one or more lines are too long

@ -1,46 +1,49 @@
function getKey({ x, y, type }) {
const gridX = x - x % 5;
const gridY = y - y % 5;
return `${gridX}-${gridY}`;
}
export default function Grid() {
this.points = {};
this.gridSize = 5;
};
Grid.prototype.setPoint = function({ x, y, type }, detail) {
this.points[getKey({ x, y, type })] = detail;
Grid.prototype.getKey = function({ x, y, type }) {
const gridX = x - x % this.gridSize;
const gridY = y - y % this.gridSize;
return `${gridX}-${gridY}-${type}`;
}
Grid.prototype.setPoint = function({ x, y, type }, value) {
this.points[this.getKey({ x, y, type })] = value;
};
Grid.prototype.getPoint = function({ x, y, type }) {
return this.points[getKey({ x, y, type })];
return this.points[this.getKey({ x, y, type })];
}
Grid.prototype.deletePoint = function({ x, y, type }) {
delete this.points[getKey({ x, y, type })];
delete this.points[this.getKey({ x, y, type })];
};
// Grid.prototype.setArea = function({ x, y, w, h, type }) {
// for (let i = x; i <= (x + w); i += this.gridSize) {
// for (let j = y; j <= (y + h); j += this.gridSize) {
// this.setPoint({ x: i, y: j, type });
// }
// }
// };
// const Store = function(initialProps) {
// this.state = Object.freeze(initialProps);
// }
//
// Store.prototype.set = function(props) {
// this.state = Object.freeze(Object.assign({}, this.state, props));
// return this.state;
// };
//
// Store.prototype.get = function() {
// return this.state;
// }
//
// export default Store;
Grid.prototype.setArea = function({ x, y, w, h, type }, container) {
for (let i = x; i <= (x + w); i += this.gridSize) {
for (let j = y; j <= (y + h); j += this.gridSize) {
this.setPoint({ x: i, y: j, type }, true);
}
}
const div = document.createElement('div');
div.className = 'hazard';
div.style.left = `${x}px`;
div.style.top = `${y}px`;
div.style.height = `${h}px`;
div.style.width = `${w}px`;
container.appendChild(div);
for (let i = x; i <= (x + w); i += this.gridSize) {
for (let j = y; j <= (y + h); j += this.gridSize) {
const dot = document.createElement('dot');
dot.className = 'hazard-dot';
dot.style.left = `${i - x}px`;
dot.style.top = `${j - y}px`;
div.appendChild(dot);
}
}
};

@ -2,20 +2,19 @@ import Rx, { Observable } from 'rxjs';
import Controls from './controls';
// import Animation1a from './animation1a';
// import Animation1b from './animation1b';
// import Animation2a from './animation2a';
// import Animation2b from './animation2b';
import Animation3a from './animation3a';
import Animation2a from './animation2a';
import Animation2b from './animation2b';
// import Animation3a from './animation3a';
require('../css/reset.scss');
require('../css/index.scss');
require('../css/particle.scss');
require('../css/controls.scss');
// new Animation1a();
// new Animation1b();
// new Animation2a();
// new Animation2b();
window.addEventListener('load', () => {
new Animation3a();
// new Animation1a();
// new Animation1b();
new Animation2a();
new Animation2b();
// new Animation3a();
});

@ -6,49 +6,40 @@ import Random from './random';
// ===== Constructor =====
function Particle(parent, bounds, config, globalGrid) {
Object.defineProperty(this, 'config', {
value: Object.assign({}, {
behavior: BEHAVIOR.COHESION,
bounds,
color: Random.color(),
gridSize: 5,
randomize: true,
showMovementCircle: false,
showVisionGrid: false,
speed: 4,
visionRadius: 50
}, config)
});
Object.defineProperty(this, 'grids', {
value: {
global: globalGrid || {},
vision: createVisionGrid(this.config)
}
});
Object.defineProperty(this, 'id', {
value: Random.id(6)
});
Object.defineProperty(this, 'nodes', {
value: {
body: createBodyNode(this.config),
circle: undefined,
container: createContainerNode(this.config, this.id),
parent,
visionGrid: undefined,
}
});
// TODO encapsulate better
this.isLeader = false;
this.leader = null;
this.config = Object.assign({}, {
behavior: BEHAVIOR.FREE,
bounds,
color: Random.color(),
gridSize: 5,
randomize: true,
showMovementCircle: false,
showVisionGrid: false,
speed: 4,
visionRadius: 50
}, config);
this.grids = {
global: globalGrid || {},
vision: createVisionGrid(this.config)
};
this.id = Random.id(6);
this.nodes = {
body: createBodyNode(this.config),
circle: undefined,
container: createContainerNode(this.config, this.id),
parent,
visionGrid: undefined,
};
this.nodes.container.appendChild(this.nodes.body);
parent.appendChild(this.nodes.container);
this.arc = Arc.create(bounds, this.grids);
this.leader = null;
this.isLeader = false;
this.arc = Arc.create(bounds, this.grids.global);
this.updateConfig(this.config);
this.nextFrame(globalGrid);
};
@ -57,10 +48,11 @@ function Particle(parent, bounds, config, globalGrid) {
Particle.prototype.remove = function() {
this.nodes.parent.removeChild(this.nodes.container);
delete this.nodes;
return this;
}
Particle.prototype.nextFrame = function(globalGrid) {
Particle.prototype.nextFrame = function() {
this.arc = Arc.step(this.arc, this.config.bounds, this.config.speed);
if (this.leader !== null) {
@ -69,31 +61,14 @@ Particle.prototype.nextFrame = function(globalGrid) {
this.arc = Arc.randomize(this.arc);
}
this.grids.global = globalGrid;
this.grids.vision = updateVisionGrid(this.arc, this.config, this.grids);
const { hazards, particles } = look(this.arc, this.grids);
if (this.leader === null && particles.length > 0) {
// Beware of circular leadership, where a leader sees its tail.
const candidates = particles
.filter(v => v.leader ? (v.leader.id !== this.id) : true);
const leader = candidates.find(v => v.isLeader) || candidates[0];
if (leader !== undefined) {
leader.isLeader = true;
// console.log(`${this.id} is now following ${leader.id}`);
this.leader = leader;
}
if (hazards.length > 0) {
this.arc = Arc.evade(this.arc);
}
this.updateLeader();
// if (hazards.length) {
// this.arc = evade(this.arc, this.grids.vision);
// }
this.updateLeader(particles);
repaintContainer(this.nodes.container, this.arc);
repaintBody(this.nodes.body, this.arc, this.isLeader);
@ -125,19 +100,45 @@ Particle.prototype.updateConfig = function(config) {
}
}
Particle.prototype.updateLeader = function() {
Particle.prototype.updateLeader = function(particles) {
if (this.config.behavior !== BEHAVIOR.COHESION) {
return;
}
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];
if (leader !== undefined) {
leader.isLeader = true;
this.leader = leader;
}
}
if (this.leader === null) {
return;
}
if (this.leader.nodes === undefined) {
this.leader = null;
return;
}
if (this.leader.leader !== null) {
this.leader = this.leader.leader;
// console.warn(`${this.id} is now following ${this.leader.id}`);
}
if (this.leader !== null && this.isLeader) {
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;
}
}
function look(arc, grids) {
@ -152,9 +153,9 @@ function look(arc, grids) {
acc.particles.push(p);
}
// if (global.getPoint({ x, y, type: ENTITIES.HAZARD })) {
// acc.hazards.push({ x, y });
// }
if (global.getPoint({ x, y, type: ENTITIES.HAZARD })) {
acc.hazards.push({ x, y });
}
return acc;
}, { hazards: [], particles: [] });
@ -276,8 +277,8 @@ function repaintCircle(node, arc) {
node.style.width = `${2 * arc.radius}px`;
node.style.height = `${2 * arc.radius}px`;
node.style.left = `-${arc.radius + arc.radius * Math.cos(arc.theta)}px`; // TODO perf here
node.style.top = `-${arc.radius - arc.radius * Math.sin(arc.theta)}px`; // TODO perf here
node.style.left = `-${arc.radius + arc.radius * arc.cosTheta}px`;
node.style.top = `-${arc.radius - arc.radius * arc.sinTheta}px`;
node.style.borderRadius = `${arc.radius}px`;
}

@ -6,8 +6,8 @@ const random = {
${Math.floor(Math.random() * 230)}
)`,
id: () => String.fromCharCode(
random.num(65, 90), random.num(97, 122), random.num(97, 122)
// random.num(97, 122), random.num(97, 122), random.num(97, 122)
random.num(65, 90), random.num(97, 122), random.num(97, 122),
random.num(97, 122), random.num(97, 122), random.num(97, 122)
),
num: (min, max) => min + Math.round(Math.random() * (max - min)),
};

Loading…
Cancel
Save