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.
 
 

361 lines
9.5 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, globalGrid, observables, behavior) {
this.config = {
behavior: behavior || BEHAVIOR.FREE,
bounds,
color: Random.color(),
gridSize: 5,
randomize: true,
showArc: false,
showVision: false,
visionRadius: 50
};
this.grids = {
global: globalGrid || {},
vision: createVisionGrid(this.config)
};
this.id = Random.id(12);
this.nodes = {
body: createBodyNode(this.config),
circle: undefined,
container: createContainerNode(this.config, this.id),
visionGrid: undefined,
};
this.nodes.container.appendChild(this.nodes.body);
parent.appendChild(this.nodes.container);
this.leader = null;
this.leaderTime = 0;
this.arc = Arc.create(bounds, this.grids.global);
this.arc.length = 3;
// If starting in a hazard, recurse.
while (this.grids.global.getPoint({ x: this.arc.endX, y: this.arc.endY, type: ENTITIES.HAZARD}) !== undefined) {
this.arc = Arc.create(bounds, this.grids.global);
}
this.grids.global.setPoint({
x: this.arc.endX,
y: this.arc.endY,
type: ENTITIES.PARTICLE
}, this);
this.remove$ = new Rx.Subject();
const frames = observables.fps$.takeUntil(this.remove$);
frames.subscribe(this.subscribeFrameMove.bind(this));
frames.subscribe(this.subscribeFrameRepaint.bind(this));
observables.speed$
.takeUntil(this.remove$)
.subscribe(this.subscribeSpeed.bind(this));
observables.randomize$ && observables.randomize$
.takeUntil(this.remove$)
.subscribe(this.subscribeRandomize.bind(this));
observables.circle$ && observables.circle$
.takeUntil(this.remove$)
.subscribe(this.subscribeCircle.bind(this));
observables.vision$ && observables.vision$
.takeUntil(this.remove$)
.subscribe(this.subscribeVision.bind(this));
};
// ===== PROTOTYPE =====
Particle.prototype.remove = function() {
this.grids.global.deletePoint({
x: this.arc.endX,
y: this.arc.endY,
type: ENTITIES.PARTICLE
});
const parent = this.nodes.container.parentNode;
parent.removeChild(this.nodes.container);
this.remove$.next();
delete this.nodes;
}
Particle.prototype.subscribeFrameMove = function(n) {
this.grids.global.deletePoint({
x: this.arc.endX,
y: this.arc.endY,
type: ENTITIES.PARTICLE
});
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);
this.updateLeader(particles);
if (hazards.length > 0) {
this.arc = Arc.evade(this.arc);
}
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);
}
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);
}
Particle.prototype.subscribeSpeed = function(value) {
Arc.changeSpeed(this.arc, value);
}
Particle.prototype.subscribeCircle = function(show) {
if (show === false) {
this.nodes.container.removeChild(this.nodes.circle);
delete this.nodes.circle;
} else {
this.nodes.circle = createCircleNode(this.config);
this.nodes.container.appendChild(this.nodes.circle);
}
}
Particle.prototype.subscribeRandomize = function(value) {
this.config.randomize = value;
}
Particle.prototype.subscribeVision = function(show) {
if (show === false) {
this.nodes.visionGrid.forEach(n => n.parentNode.removeChild(n));
delete this.nodes.visionGrid;
} else {
this.nodes.visionGrid = createVisionGridNodes(this.config, this.grids, this.nodes);
}
}
Particle.prototype.updateLeader = function(particles) {
if (this.config.behavior !== BEHAVIOR.COHESION) {
return;
}
// Head-to-head: particles see eachother but shouldn't both lead.
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.leaderTime > 0)) || candidates[0];
if (leader !== undefined) {
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.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;
}
// 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 });
point.touch = true;
} else {
point.touch = false;
}
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) {
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) {
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, leaderTime) {
node.style.left = `${arc.endX}px`;
node.style.top = `${arc.endY}px`;
// node.style.zIndex = (leaderTime > 0 ? 2000 : 2);
}
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)`;
// node.style.border = (leaderTime > 0 ? '3px dotted #fff' : '');
}
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 ? '1px solid red' : '0');
});
}
export default Particle;