You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

299 lines
7.7 KiB

import Rx, { Observable } from 'rxjs';
import { BEHAVIOR, ENTITIES, RAD } from './enums';
import Arc from './arc';
import Random from './random';
// ===== Constructor =====
function Particle(parent, bounds, config, globalGrid) {
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.leader = null;
this.isLeader = false;
this.arc = Arc.create(bounds, this.grids.global);
this.updateConfig(this.config);
this.nextFrame(globalGrid);
};
// ===== PROTOTYPE =====
Particle.prototype.remove = function() {
this.nodes.parent.removeChild(this.nodes.container);
delete this.nodes;
return this;
}
Particle.prototype.nextFrame = function() {
this.arc = Arc.step(this.arc, this.config.bounds, this.config.speed);
if (this.leader !== null) {
this.arc = Arc.follow(this.arc, this.leader.arc);
} else if (this.arc.length <= 0 && this.config.randomize) {
this.arc = Arc.randomize(this.arc);
}
this.grids.vision = updateVisionGrid(this.arc, this.config, this.grids);
const { hazards, particles } = look(this.arc, this.grids);
if (hazards.length > 0) {
this.arc = Arc.evade(this.arc);
}
this.updateLeader(particles);
repaintContainer(this.nodes.container, this.arc);
repaintBody(this.nodes.body, this.arc, this.isLeader);
repaintCircle(this.nodes.circle, this.arc);
repaintVisionGrid(this.nodes.visionGrid, this.arc, this.grids);
}
Particle.prototype.updateConfig = function(config) {
Object.assign(this.config, config);
const { showMovementCircle, showVisionGrid } = this.config;
if (showMovementCircle === true && this.nodes.circle === undefined) {
this.nodes.circle = createCircleNode(config);
this.nodes.container.appendChild(this.nodes.circle);
}
if (showMovementCircle === false && this.nodes.circle !== undefined) {
this.nodes.container.removeChild(this.nodes.circle);
delete this.nodes.circle;
}
if (showVisionGrid === true && this.nodes.visionGrid === undefined) {
this.nodes.visionGrid = createVisionGridNodes(this.config, this.grids, this.nodes);
}
if (showVisionGrid === false && this.nodes.visionGrid !== undefined) {
delete this.nodex.visionGrid;
}
}
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;
}
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) {
const { global, vision } = grids;
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 });
}
return acc;
}, { hazards: [], particles: [] });
}
// ===== DOM CREATION =====
function createBodyNode(config) {
const node = document.createElement('div');
node.className = 'particle-body';
node.style.backgroundColor = config.color;
return node;
}
function createCircleNode(config) {
if (config.showMovementCircle === false) {
return undefined;
}
const node = document.createElement('div');
node.className = 'particle-movement-circle';
node.style.borderColor = config.color;
return node;
}
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 = 45;
const points = [];
for (let x = -radius; x <= radius; x += side) {
for (let y = -radius; y <= radius; y += side) {
// Omit large slices of unused circle
if (x > y || x < -y) {
continue;
}
// Include vision band
const r = Math.pow(Math.pow(x, 2) + Math.pow(y, 2), 0.5);
if (r > r0 || r < r1) {
continue;
}
let alpha = Math.atan(y / x);
if (x < 0) {
alpha += RAD.t180;
}
points.push({ x, y, r, alpha, touch: false });
}
}
return points;
}
function createVisionGridNodes(config, grids, nodes) {
if (config.showVisionGrid === false) {
return undefined;
}
return grids.vision.reduce((acc, { x, y }) => {
const div = document.createElement('div');
div.className = 'particle-vision-dot';
div.style.backgroundColor = config.color;
nodes.container.appendChild(div);
acc.push(div);
return acc;
}, []);
}
function updateVisionGrid(arc, config, grids) {
const { global, vision } = grids;
return vision.reduce((acc, point) => {
const rad = arc.clockwise
? point.alpha - arc.theta
: point.alpha - arc.theta + RAD.t180;
point.x = point.r * Math.cos(rad);
point.y = point.r * Math.sin(rad);
return acc.concat(point);
}, []);
}
// ===== DOM RENDERING =====
function repaintContainer(node, arc) {
node.style.left = `${arc.endX}px`;
node.style.top = `${arc.endY}px`;
}
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) {
if (node === undefined) {
return;
}
node.style.width = `${2 * arc.radius}px`;
node.style.height = `${2 * arc.radius}px`;
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`;
}
function repaintVisionGrid(nodes, arc, grids) {
if (nodes === undefined) {
return;
}
grids.vision.forEach(({ x, y, touch }, i) => {
nodes[i].style.left = `${x}px`;
nodes[i].style.top = `${y}px`;
nodes[i].style.border = (touch ? '2px solid red' : '0');
});
}
export default Particle;