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
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;
|
|
|