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.
362 lines
9.6 KiB
362 lines
9.6 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) => {
|
|
point.touch = false;
|
|
|
|
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);
|
|
point.touch = true;
|
|
}
|
|
|
|
if (global.getPoint({ x, y, type: ENTITIES.HAZARD })) {
|
|
acc.hazards.push({ x, y });
|
|
point.touch = true;
|
|
}
|
|
|
|
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;
|
|
|