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.
 
 

446 lines
12 KiB

import Rx, { Observable } from 'rxjs';
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() * 230)},
${Math.floor(Math.random() * 230)},
${Math.floor(Math.random() * 230)}
)`,
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) {
Object.defineProperty(this, 'config', {
value: Object.assign({}, {
behavior: BEHAVIOR.COHESION,
bounds,
color: random.color(),
gridSize: 5,
randomize: true,
showMovementCircle: false,
showVisionGrid: false,
speed: 4,
visionRadius: 50
}, config)
});
Object.defineProperty(this, 'grids', {
value: {
global: globalGrid || {},
vision: createVisionGrid(this.config)
}
});
Object.defineProperty(this, 'id', {
value: random.id(6)
});
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(globalGrid);
};
// ===== PROTOTYPE =====
Particle.prototype.remove = function() {
this.nodes.parent.removeChild(this.nodes.container);
return this;
}
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);
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.log(`${this.id} is now following ${leader.id}`);
this.isLeader = false;
this.leader = leader;
}
}
this.updateLeader();
// if (hazards.length) {
// this.arc = evade(this.arc, this.grids.vision);
// }
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() {
if (this.leader === null) {
return;
}
while (this.leader.leader !== null) {
this.leader.isLeader = false;
this.leader = this.leader.leader;
// console.error(this.id, 'is now following', this.leader.id)
}
// Prevents circular leadership, where a leader sees its tail.
if (this.leader.id === this.id) {
this.leader = null;
}
}
// ===== CREATION =====
function createArc(bounds, grids) {
let arc = {
centerX: random.num(0, bounds.width),
centerY: random.num(0, bounds.height),
clockwise: random.bool(),
endX: 0,
endY: 0,
length: random.num(RAD.t90, RAD.t360),
radius: random.num(100, 200),
theta: random.num(RAD.t90, RAD.t360),
};
arc.endX = arc.centerX + arc.radius * Math.cos(arc.theta);
arc.endY = arc.centerY - arc.radius * Math.sin(arc.theta);
arc = overflowArc(arc, bounds);
const x = arc.endX - arc.endX % 5;
const y = arc.endY - arc.endY % 5;
// If starting in a hazard, recurse.
if (grids.global[x] !== undefined && grids.global[x][y] !== undefined) {
arc = createArc(bounds, grids);
}
return arc;
}
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 = 30;
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;
}, []);
}
// ===== CALCULATIONS =====
function step(arc, { bounds, speed }) {
// Ensure constant velocity and theta between 0 and 2π.
const delta = speed / arc.radius;
arc.length -= delta;
arc.theta += (arc.clockwise ? -delta : +delta);
arc.theta = (arc.theta > 0 ? arc.theta % RAD.t360 : RAD.t360 + arc.theta);
arc.endX = arc.centerX + arc.radius * Math.cos(arc.theta); // TODO perf here
arc.endY = arc.centerY - arc.radius * Math.sin(arc.theta); // TODO perf here
// Overflow.
arc = overflowArc(arc, bounds);
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;
arc.centerX += bounds.width
} else if (arc.endX > bounds.width) {
arc.endX -= bounds.width;
arc.centerX -= bounds.width
}
if (arc.endY < 0) {
arc.endY += bounds.height;
arc.centerY += bounds.height
} else if (arc.endY > bounds.height) {
arc.endY -= bounds.height;
arc.centerY -= bounds.height
}
return arc;
}
function moveArc(arc, newRadius) {
const r0 = arc.radius;
const r1 = newRadius;
// Moves arc center to new radius while keeping theta constant.
arc.centerX -= (r1 - r0) * Math.cos(arc.theta); // TODO perf here
arc.centerY += (r1 - r0) * Math.sin(arc.theta); // TODO perf here
arc.radius = r1;
return arc;
}
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;
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);
}, []);
}
// ===== ACTIONS =====
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: [] });
}
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 =====
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 * Math.cos(arc.theta)}px`; // TODO perf here
node.style.top = `-${arc.radius - arc.radius * Math.sin(arc.theta)}px`; // TODO perf here
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;