Initial efforts on cohesion.

master
Ben Burlingham 8 years ago
parent 65b8e275b4
commit ce06797630
  1. 12
      index.html
  2. 279
      js/_ORIGINAL.js
  3. 131
      js/_SCARE.js
  4. 292
      js/_WALLDETECT-BUGGY.js
  5. 9
      js/animation2b.js
  6. 95
      js/animation3a.js
  7. 1348
      js/bundle.js
  8. 14
      js/enums.js
  9. 31
      js/grid.js
  10. 18
      js/index.js
  11. 218
      js/particle.js

@ -15,6 +15,7 @@
<blockquote>
Explore the RxJs API by managing moving particle systems. The systems should:
<ul>
<li>Use self-aware AI, not a global AI</li>
<li>Have particle movement that feels calm and natural</li>
<li>Support large swarms of particles</li>
<li>Be able to evade obstacles</li>
@ -24,7 +25,7 @@
<hr>
<p>
<!-- <p>
<h4>Exploration 1: Organic movement at 32fps</h4>
</p>
@ -70,6 +71,15 @@
<div class='outerContainer'>
<div id="animation2b" class='animationContainer'></div>
<div id="controls2b" class='controlsContainer'></div>
</div> -->
<p>
The last goal was to play around with flocking behavior. Cohesion:
</p>
<div class='outerContainer'>
<div id="animation3a" class='animationContainer'></div>
<div id="controls3a" class='controlsContainer'></div>
</div>
<script src='js/bundle.js'></script>

@ -1,279 +0,0 @@
// Single particle movement.
// Goal: per-frame decisions
// 20 x 20 grid
// The trickiest portion of this iteration was animating the curved paths. Calculating arc length (for scalar speed) along an elliptical
// geometry is quite difficult, so the current solution uses circular paths, which change radius and sometimes rotation after a random
// number of frames have emitted. A smoothstep cubic curve was considered, but maintaining consistent entry and exit angles will affect
// performance for large particle counts.
// This algorithm travels a random period of time around a random arc.
// If a wall is detected, a 180-degree turn is executed.
import Rx, { Observable } from 'rxjs';
import AnimationBase from './animation0';
import DOM from './dom';
import Store from './store';
const speed = 4;
const visionRadius = 50;
const grid = {};
const t45 = Math.PI / 4;
const t90 = Math.PI / 2;
const t270 = 3 * Math.PI / 2;
const t360 = Math.PI * 2;
const movementCircle = document.createElement('div');
movementCircle.className = 'anim3-movement-circle';
const visionGrid = document.createElement('div');
visionGrid.className = 'anim3-vision-grid';
const particle = document.createElement('div');
particle.className = 'anim3-particle';
const visionGridPoints = calculateVisionGridPoints();
function move(store) {
let {
clockwise,
interval,
circleX,
circleY,
particleX,
particleY,
radius,
theta
} = store.get();
// If wall detected, tight 180
if (detectWall(store) && radius !== 20) {
const radius0 = radius;
radius = 20;
interval = Math.PI * 20 / speed;
circleX -= (radius - radius0) * Math.cos(theta);
circleY += (radius - radius0) * Math.sin(theta);
} else if (interval <= 0) {
const radius0 = radius;
interval = Math.round(Math.random() * 10) + 50;
radius = Math.round(Math.random() * 200) + 50;
if (Math.random() < 0.5) {
clockwise = !clockwise;
theta = (theta + Math.PI) % t360;
circleX -= (radius + radius0) * Math.cos(theta);
circleY += (radius + radius0) * Math.sin(theta);
} else {
circleX -= (radius - radius0) * Math.cos(theta);
circleY += (radius - radius0) * Math.sin(theta);
}
}
interval -= 1;
const delta = speed / radius;
theta += (clockwise ? -delta : +delta);
theta = (theta > 0 ? theta % t360 : t360 - theta);
particleX = circleX + radius * Math.cos(theta);
particleY = circleY - radius * Math.sin(theta);
store.set({
clockwise,
interval,
circleX,
circleY,
particleX,
particleY,
radius,
theta
});
}
function detectWall(store) {
const len = visionGridPoints.length;
const { particleX, particleY } = store.get();
const gridX = particleX - particleX % 5;
const gridY = particleY - particleY % 5;
for (let i = 0; i < len; i++) {
const { x, y } = visionGridPoints[i];
const xx = x + gridX;
const yy = y + gridY;
if (grid[xx] && grid[xx][yy] && grid[xx][yy].type === 'wall') {
console.warn("Wall detected", xx, yy);
// TODO this goes somewhere else
// DOM.container.dispatchEvent(new CustomEvent('stop'));
return true;
}
}
return false;
}
function transformMovementCircle(store) {
// TODO UPDATE ONLY IF THETA CHANGED - MUST HAVE PREVSTATE
const { circleX, circleY, radius, theta } = store.get();
const r = Math.abs(radius);
movementCircle.style.left = `${circleX - r}px`;
movementCircle.style.top = `${circleY - r}px`;
movementCircle.style.height = `${2 * r}px`;
movementCircle.style.width = `${2 * r}px`;
movementCircle.style.borderRadius = `${r}px`;
}
function transformParticle(store) {
const { clockwise, particleX, particleY, theta } = store.get();
const rad = clockwise ? Math.PI - theta : t360 - theta;
particle.style.left = `${particleX}px`;
particle.style.top = `${particleY}px`;
particle.style.transform = `rotate(${rad}rad)`;
}
function transformVisionGrid(store) {
const {
circleX,
circleY,
clockwise,
particleX,
particleY,
radius,
theta
} = store.get();
const r0 = Math.min(theta, theta - Math.PI);
const r1 = Math.max(theta, theta + Math.PI);
const gridX = particleX - particleX % 5;
const gridY = particleY - particleY % 5;
visionGridPoints.forEach(({ x, y, alpha, div }, i) => {
if (alpha >= 0 && alpha <= r0) {
div.style.display = (clockwise ? 'none' : 'block');
} else if (alpha >= theta && alpha <= r1) {
div.style.display = (clockwise ? 'none' : 'block');
} else {
div.style.display = (clockwise ? 'block' : 'none');
}
div.style.left = `${x + gridX}px`;
div.style.top = `${-y + gridY}px`;
});
}
function calculateVisionGridPoints() {
const gridSize = 5;
const squareGrid = [];
for (let x = -visionRadius; x <= visionRadius; x += gridSize) {
for (let y = -visionRadius; y <= visionRadius; y += gridSize) {
let alpha = Math.atan(y / x);
if (x === 0 && y === 0) {
alpha = 0;
} else if (x === 0 && y < 0) {
alpha = t270;
} else if (y === 0 && x < 0) {
alpha = Math.PI;
} else if (x === 0 && y > 0) {
alpha = t90;
} else if (x < 0 && y < 0) {
alpha = alpha + Math.PI;
} else if (x <= 0) {
alpha = Math.PI + alpha;
} else if (y < 0) {
alpha = 2 * Math.PI + alpha;
}
squareGrid.push({ x, y, alpha });
}
}
const r0 = Math.pow(visionRadius, 2);
const r1 = Math.pow(visionRadius - gridSize, 2);
return squareGrid.reduce((acc, point) => {
const p = Math.pow(point.x, 2) + Math.pow(point.y, 2);
if (p > r0 || p < r1) {
return acc;
}
const div = document.createElement('div');
div.className = 'anim3-dot';
acc.push(Object.assign(point, { div }));
return acc;
}, []);
}
function reset() {
while (DOM.container.childNodes.length) {
DOM.container.removeChild(DOM.container.firstChild);
}
const store = new Store({
clockwise: false,
interval: 10,
circleX: 300,
circleY: 300,
radius: 270,
theta: Math.PI * 3 / 4
});
move(store);
transformParticle(store);
// transformMovementCircle(store);
// transformVisionGrid(store);
DOM.container.appendChild(particle);
DOM.container.appendChild(movementCircle);
visionGridPoints.forEach(point => {
DOM.container.appendChild(point.div);
});
return store;
};
function init() {
const store = reset();
for (let x = 0; x <= 600; x += 5) {
grid[x] = {};
for (let y = 0; y <= 600; y += 5) {
grid[x][y] = { type: null };
if (x === 0 || y === 0 || x === 600 || y === 600) {
grid[x][y] = { type: 'wall' };
}
}
}
const stop$ = Rx.Observable.fromEvent(DOM.container, 'stop');
const fps$ = Rx.Observable.interval(1000 / 32)
.map(_ => store)
// .take(300)
// .take(15)
.takeUntil(stop$); console.error("CLICK TO STOP");
const click$ = Rx.Observable.fromEvent(DOM.container, 'click');
click$.subscribe(() => {
DOM.container.dispatchEvent(new CustomEvent('stop'));
});
fps$.subscribe(move);
fps$.subscribe(transformParticle);
// fps$.subscribe(transformMovementCircle);
fps$.subscribe(transformVisionGrid);
};
const Animation3 = Object.assign({}, AnimationBase, { init, reset });
export default Animation3;

@ -1,131 +0,0 @@
// Scare mechanic, single particle.
import Rx, { Observable } from 'rxjs';
import AnimationBase from './animationBase';
import DOM from './dom';
import Store from './store';
const evtScare = (scareX, scareY) => new CustomEvent("scare", { detail: { scareX, scareY } });
const particleDiv = document.createElement('div');
particleDiv.className = 'anim2-particle';
function checkScare([evt, store]) {
const state = store.get();
const { evtX, evtY } = DOM.getEventOffsetCoords(evt, DOM.containerBounds);
const diffX = Math.abs(state.x - evtX);
const diffY = Math.abs(state.y - evtY);
if (evt.target === particleDiv) {
DOM.container.dispatchEvent(evtScare(evtX, evtY));
}
};
function move(acc, i) {
let { x, y, dx, dy } = acc;
const east = DOM.containerBounds.width - particleDiv.offsetWidth;
const south = DOM.containerBounds.height - particleDiv.offsetHeight;
x += dx;
y += dy;
if (x < 0) {
x = Math.abs(x);
dx = -dx;
}
if (x > east) {
x = Math.round(2 * east - x);
dx = -dx;
}
if (y < 0) {
y = Math.abs(y);
dy = -dy;
}
if (y > south) {
y = Math.round(2 * south - y);
dy = -dy;
}
return { x, y, dx, dy };
};
function flee([evt, store]) {
const initialState = store.get();
const fleeRadius = 200;
const { scareX, scareY } = evt.detail;
const fps$ = Rx.Observable.interval(1000 / 32);
const frames$ = fps$
.scan(move, initialState)
.takeWhile(state => {
const xDanger = Math.abs(initialState.x - state.x) < fleeRadius;
const yDanger = Math.abs(initialState.y - state.y) < fleeRadius;
return xDanger && yDanger;
})
frames$.last().subscribe(finalState => {
store.set(finalState);
store.set(randomMoveVector());
});
frames$.subscribe(state => {
particleDiv.style.left = `${state.x}px`;
particleDiv.style.top = `${state.y}px`;
});
};
function randomMoveVector() {
const speed = 10;
let dx = Math.round(Math.random() * speed);
let dy = Math.pow(Math.pow(speed, 2) - Math.pow(dx, 2), 0.5);
const negX = Math.random() < 0.5 ? -1 : 1;
const negY = Math.random() < 0.5 ? -1 : 1;
dx *= negX;
dy *= negY;
return { dx, dy };
}
function reset() {
if (particleDiv.parentNode) {
DOM.container.removeChild(particleDiv);
}
const { dx, dy } = randomMoveVector();
const store = new Store({ x: 0, y: 0, dx, dy });
const state = store.get();
particleDiv.style.top = `${state.y}px`;
particleDiv.style.left = `${state.x}px`;
DOM.container.appendChild(particleDiv);
return store;
};
function init() {
const store = reset();
const click$ = Rx.Observable
.fromEvent(DOM.container, 'click')
// .do(DOM.calcBounds)
.do(DOM.highlight)
.map(evt => [evt, store])
.subscribe(checkScare);
Rx.Observable
.fromEvent(DOM.container, 'scare')
.map(evt => [evt, store])
.subscribe(flee);
};
const Animation2 = Object.assign({}, AnimationBase, { init, reset });
export default Animation2;

@ -1,292 +0,0 @@
// Single particle movement.
// Goal: per-frame decisions
// 20 x 20 grid
// The trickiest portion of this iteration was animating the curved paths. Calculating arc length (for scalar speed) along an elliptical
// geometry is quite difficult, so the current solution uses circular paths, which change radius and sometimes rotation after a random
// number of frames have emitted. A smoothstep cubic curve was considered, but maintaining consistent entry and exit angles will affect
// performance for large particle counts.
// Another tricky part was the wall avoidance and vision grid.
// This algorithm travels a random period of time around a random arc.
// If a wall is detected, a 180-degree turn is executed.
import Rx, { Observable } from 'rxjs';
import AnimationBase from './animation0';
import DOM from './dom';
import Store from './store';
const speed = 4;
const grid = {};
const t45 = Math.PI / 4;
const t90 = Math.PI / 2;
const t270 = 3 * Math.PI / 2;
const t360 = Math.PI * 2;
const movementCircle = document.createElement('div');
movementCircle.className = 'anim3-movement-circle';
const particle = document.createElement('div');
particle.className = 'anim3-particle';
// const visionGridPoints = calculateVisionGridPoints();
function move(store) {
let {
arc,
clockwise,
frame,
particleX,
particleY,
} = store.get();
const delta = speed / arc.r;
arc.t += (clockwise ? -delta : +delta);
arc.t = (arc.t > 0 ? arc.t % t360 : t360 - arc.t);
// const intersections = detectWall(store);
//
// if (intersections.length > 0) {
// const { xs, ys } = intersections.reduce(
// ({ xs, ys }, {x, y}) => ({ xs: xs + x, ys: ys + y }),
// { xs: 0, ys: 0 }
// );
//
// const avgX = xs / intersections.length;
// const avgY = ys / intersections.length;
//
// const v = Math.atan((particleY - avgY) / (particleX - avgX));
// // console.warn(Math.round(v * 180 / Math.PI))
//
// const modifier = Math.max(Math.round(v * 180 / Math.PI), 20);
//
// arc = modifyArc(arc, 20);
// // } else if (Math.random() < 0.005) {
// // console.warn('changing direction')
// // clockwise = !clockwise;
// // arc = changeDirection(arc);
// } else {
// arc = modifyArc(arc, 300);
// }
particleX = arc.x + arc.r * Math.cos(arc.t);
particleY = arc.y - arc.r * Math.sin(arc.t);
frame += 1;
store.set({
arc,
clockwise,
frame,
particleX,
particleY,
});
}
// generate next arc:
// starting point will be locked
// starting angle will be locked
// therefore tangential
function modifyArc(arc, newRadius) {
const r0 = arc.r;
const r1 = newRadius;
arc.x -= (r1 - r0) * Math.cos(arc.t);
arc.y += (r1 - r0) * Math.sin(arc.t);
arc.r = r1;
return arc;
}
function changeDirection(arc) {
arc.t = (arc.t + Math.PI) % t360;
arc.x -= (2 * arc.r) * Math.cos(arc.t);
arc.y += (2 * arc.r) * Math.sin(arc.t);
return arc;
}
// function detectWall(store) {
// const len = visionGridPoints.length;
//
// const { arc, clockwise, particleX, particleY } = store.get();
//
// const r0 = Math.min(arc.t, arc.t - Math.PI);
// const r1 = Math.max(arc.t, arc.t + Math.PI);
//
// const gridX = particleX - particleX % 5;
// const gridY = particleY - particleY % 5;
//
// return visionGridPoints.reduce((acc, point) => {
// const xx = gridX + point.x;
// const yy = gridY - point.y;
// const alpha = point.alpha;
//
// if (grid[xx] && grid[xx][yy] && grid[xx][yy].type === 'wall') {
// if (clockwise === false && alpha >= 0 && alpha <= r0) {
// acc.push(point);
// } else if (clockwise === false && alpha >= arc.t && alpha <= r1) {
// acc.push(point);
// } else if (clockwise === true) {
// acc.push(point);
// }
// }
//
// return acc;
// }, []);
// }
function transformParticle(store) {
const { arc, clockwise, particleX, particleY } = store.get();
const rad = clockwise ? Math.PI - arc.t : t360 - arc.t;
particle.style.left = `${particleX}px`;
particle.style.top = `${particleY}px`;
particle.style.transform = `rotate(${rad}rad)`;
}
function transformVisionGrid(store) {
const {
arc,
clockwise,
particleX,
particleY,
radius,
} = store.get();
const r0 = Math.min(arc.t, arc.t - Math.PI);
const r1 = Math.max(arc.t, arc.t + Math.PI);
const gridX = particleX - particleX % 5;
const gridY = particleY - particleY % 5;
visionGridPoints.forEach(({ x, y, alpha, div }, i) => {
if (alpha >= 0 && alpha <= r0) {
div.style.display = (clockwise ? 'none' : 'block');
// div.className = (clockwise ? 'anim3-dot removed' : 'anim3-dot');
} else if (alpha >= arc.t && alpha <= r1) {
div.style.display = (clockwise ? 'none' : 'block');
// div.className = (clockwise ? 'anim3-dot removed' : 'anim3-dot');
} else {
div.style.display = (clockwise ? 'block' : 'none');
// div.className = (clockwise ? 'anim3-dot' : 'anim3-dot removed');
}
div.style.left = `${x + gridX}px`;
div.style.top = `${-y + gridY}px`;
});
}
// function calculateVisionGridPoints() {
// const gridSize = 5;
// const visionRadius = 50;
//
// const squareGrid = [];
// for (let x = -visionRadius; x <= visionRadius; x += gridSize) {
// for (let y = -visionRadius; y <= visionRadius; y += gridSize) {
// let alpha = Math.atan(y / x);
//
// if (x === 0 && y === 0) {
// alpha = 0;
// } else if (x === 0 && y < 0) {
// alpha = t270;
// } else if (y === 0 && x < 0) {
// alpha = Math.PI;
// } else if (x === 0 && y > 0) {
// alpha = t90;
// } else if (x < 0 && y < 0) {
// alpha = alpha + Math.PI;
// } else if (x <= 0) {
// alpha = Math.PI + alpha;
// } else if (y < 0) {
// alpha = 2 * Math.PI + alpha;
// }
//
// squareGrid.push({ x, y, alpha });
// }
// }
//
// const r0 = Math.pow(visionRadius, 2);
// const r1 = Math.pow(visionRadius - gridSize, 2);
//
// return squareGrid.reduce((acc, point) => {
// const p = Math.pow(point.x, 2) + Math.pow(point.y, 2);
// if (p > r0 || p < r1) {
// return acc;
// }
//
// const div = document.createElement('div');
// div.className = 'anim3-dot';
//
// acc.push(Object.assign(point, { div }));
// return acc;
// }, []);
// }
function reset() {
while (DOM.container.childNodes.length) {
DOM.container.removeChild(DOM.container.firstChild);
}
const store = new Store({
arc: {
r: Math.round(Math.random() * 200) + 100,
t: Math.random() * t360,
x: 300,
y: 300,
},
clockwise: false,
});
move(store);
transformParticle(store);
// transformMovementCircle(store);
// transformVisionGrid(store);
DOM.container.appendChild(particle);
DOM.container.appendChild(movementCircle);
// visionGridPoints.forEach(point => {
// DOM.container.appendChild(point.div);
// });
return store;
};
function init() {
const store = reset();
for (let x = 0; x <= 600; x += 5) {
grid[x] = {};
for (let y = 0; y <= 600; y += 5) {
grid[x][y] = { type: null };
if (x === 0 || y === 0 || x === 600 || y === 600) {
grid[x][y] = { type: 'wall' };
}
}
}
const stop$ = Rx.Observable.fromEvent(DOM.container, 'stop');
const fps$ = Rx.Observable.interval(1000 / 32)
.map(i => store.bind(null, i))
// .take(300)
// .take(15)
.takeUntil(stop$); console.error("CLICK TO STOP");
const click$ = Rx.Observable.fromEvent(DOM.container, 'click');
click$.subscribe(() => {
DOM.container.dispatchEvent(new CustomEvent('stop'));
});
fps$.subscribe(move);
fps$.subscribe(transformParticle);
fps$.subscribe(transformVisionGrid);
// fps$.subscribe(transformMovementCircle);
};
const Animation3 = Object.assign({}, AnimationBase, { init, reset });
export default Animation3;

@ -26,15 +26,6 @@ function Animation2b() {
this.updateAnimating(this.options.animating);
this.updateCount(this.options.count);
// TODO X dimension modified by core UI, maybe recalc grid in animation start?
// TODO remove bottom padding from Disqus
// TODO ANIM2ab randomize hazards
// TODO fix "hangup" small radius evade bug
// TODO ANIM2b perf Scale vision grid to 1000 particles
// TODO ANIM3 flocking
};
Animation2b.prototype.subscriber = function({ key, value }) {

@ -0,0 +1,95 @@
import Rx, { Observable } from 'rxjs';
import Grid from './grid';
import Particle from './particle';
import Store from './store';
import Controls from './controls';
import { CONTROLS, ENTITIES } from './enums';
function Animation3a() {
this.options = {
cohesion: true,
count: 2,
maxCount: 5,
showVisionGrid: true,
speed: 4
};
this.container = document.getElementById('animation3a');
this.bounds = this.container.getBoundingClientRect();
this.particles = [];
this.globalGrid = new Grid();
const controls = new Controls(
document.getElementById('controls3a'),
this.options
);
controls.mount().subscribe(this.subscriber.bind(this));
this.updateAnimating(this.options.animating);
this.updateCount(this.options.count);
// TODO extract Arc
// TODO X dimension modified by core UI, maybe recalc grid in animation start?
// TODO remove bottom padding from Disqus
// TODO ANIM2ab randomize hazards
// TODO fix "hangup" small radius evade bug
// TODO ANIM3a perf Scale vision grid to 1000 particles
// TODO hazard grid, particles grid
// TODO completely seal Particle
// TODO ANIM3a cohesion
// TODO ANIM3b separation
// TODO ANIM3c alignment
};
Animation3a.prototype.subscriber = function({ key, value }) {
switch(key) {
case CONTROLS.ANIMATING: this.updateAnimating(value); break;
case CONTROLS.COUNT: this.updateCount(value); break;
case CONTROLS.SPEED: this.updateSpeed(value); break;
}
}
Animation3a.prototype.nextFrame = function() {
this.particles.forEach(p => {
const prevX = p.arc.endX;
const prevY = p.arc.endY;
p.nextFrame(this.globalGrid);
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);
});
}
Animation3a.prototype.updateAnimating = function(isAnimating) {
this.options.animating = isAnimating;
if (isAnimating) {
const fps$ = Rx.Observable.interval(1000 / 32)
.takeWhile(_ => this.options.animating);
fps$.subscribe(this.nextFrame.bind(this));
}
}
Animation3a.prototype.updateCount = function(count) {
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);
this.particles.push(p);
}
}
Animation3a.prototype.updateSpeed = function(value) {
this.options.speed = value;
this.particles.forEach(p => p.updateConfig({ speed: value }));
}
export default Animation3a;

File diff suppressed because one or more lines are too long

@ -14,4 +14,16 @@ const CONTROLS = {
SPEED: 'speed'
};
export { CONTROLS, RAD };
const BEHAVIOR = {
ALIGNMENT: 'alignment',
COHESION: 'cohesion',
FREE: 'free',
SEPARATION: 'separation'
};
const ENTITIES = {
PARTICLE: 'particle',
HAZARD: 'hazard'
};
export { BEHAVIOR, CONTROLS, ENTITIES, RAD };

@ -0,0 +1,31 @@
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.getPoint = function({ x, y, type }) {
return this.points[getKey({ x, y, type })];
}
Grid.prototype.deletePoint = function({ x, y, type }) {
delete this.points[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 });
// }
// }
// };

@ -1,16 +1,18 @@
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 Animation1a from './animation1a';
// import Animation1b from './animation1b';
// 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();
// new Animation1a();
// new Animation1b();
// new Animation2a();
// new Animation2b();
new Animation3a();

@ -1,47 +1,69 @@
import Rx, { Observable } from 'rxjs';
import { RAD } from './enums';
import { BEHAVIOR, ENTITIES, RAD } from './enums';
import Store from './store';
const random = {
bool: (weight) => Math.random() < (weight || 0.5),
color: () => `rgb(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)})`,
num: (min, max) => min + Math.round(Math.random() * max),
color: () => `rgb(
${Math.floor(Math.random() * 170)},
${Math.floor(Math.random() * 170)},
${Math.floor(Math.random() * 170)}
)`,
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)
),
num: (min, max) => min + Math.round(Math.random() * (max - min)),
}
// ===== Constructor =====
function Particle(parent, bounds, config, globalGrid) {
this.config = Object.assign({
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)
};
Object.defineProperty(this, 'config', {
value: Object.assign({}, {
behavior: BEHAVIOR.COHESION,
bounds,
color: random.color(),
gridSize: 10,
randomize: true,
showMovementCircle: false,
showVisionGrid: false,
speed: 4,
visionRadius: 200
}, config)
});
Object.defineProperty(this, 'grids', {
value: {
global: globalGrid || {},
vision: createVisionGrid(this.config)
}
});
this.arc = createArc(bounds, this.grids, this.config); // TODO no need to pass config after testing
Object.defineProperty(this, 'id', {
value: random.id(6)
});
this.nodes = {
body: createBodyNode(this.config),
circle: undefined,
container: createContainerNode(this.config),
parent,
visionGrid: undefined,
};
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.nodes.container.appendChild(this.nodes.body);
parent.appendChild(this.nodes.container);
this.arc = createArc(bounds, this.grids);
this.updateConfig(this.config);
this.nextFrame();
this.nextFrame(globalGrid);
};
// ===== PROTOTYPE =====
@ -51,14 +73,46 @@ Particle.prototype.remove = function() {
return this;
}
Particle.prototype.nextFrame = function() {
this.arc = stepArc(this.arc, this.config);
Particle.prototype.nextFrame = function(globalGrid) {
// Randomly change radius and rotation direction.
if (this.arc.length <= 0 && this.config.randomize) {
this.arc = randomizeArc(this.arc);
}
this.arc = step(this.arc, this.config);
if (this.leader !== null) {
this.arc = followArc(this.arc, this.leader.arc);
}
this.grids.global = globalGrid;
this.grids.vision = updateVisionGrid(this.arc, this.config, this.grids);
this.arc = evade(this.arc, this.grids.vision);
const { hazards, particles } = look(this.arc, this.grids);
if (this.leader === null && particles.length > 0) {
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.warn(`${particles[0].id} is now a leader`);
console.log(`${this.id} is now following ${leader.id}`);
this.isLeader = false;
this.leader = leader;
}
}
// if (hazards.length) {
// this.arc = evade(this.arc, this.grids.vision);
// }
repaintContainer(this.nodes.container, this.arc);
repaintBody(this.nodes.body, this.arc);
repaintBody(this.nodes.body, this.arc, this.isLeader);
repaintCircle(this.nodes.circle, this.arc);
repaintVisionGrid(this.nodes.visionGrid, this.arc, this.grids);
}
@ -89,7 +143,7 @@ Particle.prototype.updateConfig = function(config) {
// ===== CREATION =====
function createArc(bounds, grids, config) {
function createArc(bounds, grids) {
let arc = {
centerX: random.num(0, bounds.width),
centerY: random.num(0, bounds.height),
@ -111,7 +165,7 @@ function createArc(bounds, grids, config) {
// If starting in a hazard, recurse.
if (grids.global[x] !== undefined && grids.global[x][y] !== undefined) {
arc = createArc(bounds, grids, config);
arc = createArc(bounds, grids);
}
return arc;
@ -135,16 +189,17 @@ function createCircleNode(config) {
return node;
}
function createContainerNode(config) {
function createContainerNode(config, id) {
const node = document.createElement('div');
node.className = 'particle-container';
node.id = id;
return node;
}
function createVisionGrid(config) {
const { gridSize: side, visionRadius: radius } = config;
const r0 = radius;
const r1 = radius - side;
const r1 = 20;
const points = [];
@ -192,21 +247,7 @@ function createVisionGridNodes(config, grids, nodes) {
// ===== CALCULATIONS =====
function stepArc(arc, { bounds, randomize, speed }) {
// Randomly change radius and rotation direction.
if (arc.length <= 0) {
arc.length = random.num(RAD.t90, RAD.t360);
if (randomize === true) {
arc = moveArc(arc, random.num(100, 200));
if (random.bool(0.8)) {
arc.clockwise = !arc.clockwise;
arc = reverseArc(arc);
}
}
}
function step(arc, { bounds, speed }) {
// Ensure constant velocity and theta between 0 and 2π.
const delta = speed / arc.radius;
arc.length -= delta;
@ -223,6 +264,18 @@ function stepArc(arc, { bounds, randomize, speed }) {
return arc;
}
function randomizeArc(arc) {
arc.length = random.num(RAD.t90, RAD.t360);
arc = moveArc(arc, random.num(100, 200));
if (random.bool(0.8)) {
arc = reverseArc(arc);
}
return arc;
}
function overflowArc(arc, bounds) {
if (arc.endX < 0) {
arc.endX += bounds.width;
@ -256,13 +309,30 @@ function moveArc(arc, newRadius) {
}
function reverseArc(arc) {
arc.clockwise = !arc.clockwise;
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
return arc;
}
function followArc(arc, arcToFollow) {
if (arc.clockwise !== arcToFollow.clockwise) {
arc = reverseArc(arc);
}
// if (Math.abs(arc.theta - arcToFollow.theta) > 0.1) {
// arc = moveArc(arc, 20);
// } else {
// arc = moveArc(arc, arcToFollow.radius);
// }
return arc;
}
function updateVisionGrid(arc, config, grids) {
const { global, vision } = grids;
@ -274,30 +344,42 @@ function updateVisionGrid(arc, config, grids) {
point.x = point.r * Math.cos(rad);
point.y = point.r * Math.sin(rad);
const x = arc.endX + point.x;
const y = arc.endY + point.y;
const gridX = x - x % 5;
const gridY = y - y % 5;
point.touch = (global[gridX] !== undefined && global[gridX][gridY] !== undefined);
return acc.concat(point);
}, []);
}
// ===== ACTIONS =====
function evade(arc, visionGrid) {
const danger = visionGrid.reduce((acc, v) => acc || v.touch, false);
function look(arc, grids) {
const { global, vision } = grids;
if (danger === false) {
return arc;
}
return vision.reduce((acc, point) => {
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);
}
// if (global.getPoint({ x, y, type: ENTITIES.HAZARD })) {
// acc.hazards.push({ x, y });
// }
const evasionArc = moveArc(arc, 20);
evasionArc.length = 1;
return acc;
}, { hazards: [], particles: [] });
}
return evasionArc;
function evade(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;
}
// ===== RENDERING =====
@ -306,12 +388,14 @@ function repaintContainer(node, arc) {
node.style.top = `${arc.endY}px`;
}
function repaintBody(node, arc) {
function repaintBody(node, arc, isLeader) {
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 = '';
}
function repaintCircle(node, arc) {

Loading…
Cancel
Save