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.
 
 

347 lines
9.1 KiB

import Rx, { Observable } from 'rxjs';
import { RAD } from './enums';
import Store from './store';
const random = {
bool: (weight) => Math.random() < (weight || 0.5),
color: () => `rgb(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)})`,
num: (min, max) => min + Math.round(Math.random() * max),
}
// ===== Constructor =====
function Particle(parent, bounds, config, globalGrid) {
this.config = Object.assign({
bounds,
color: random.color(),
gridSize: 5,
randomize: false,
showMovementCircle: false,
showVisionGrid: false,
speed: 4,
visionRadius: 50
}, config);
this.grids = {
global: globalGrid || {},
vision: createVisionGrid(this.config)
};
this.arc = createArc(bounds, globalGrid, this.config); // TODO no need to pass config after testing
this.nodes = {
body: createBodyNode(this.config),
circle: undefined,
container: createContainerNode(this.config),
parent,
visionGrid: undefined,
};
this.nodes.container.appendChild(this.nodes.body);
parent.appendChild(this.nodes.container);
this.updateConfig(this.config);
this.nextFrame();
};
// ===== PROTOTYPE =====
Particle.prototype.remove = function() {
this.nodes.parent.removeChild(this.nodes.container);
this.nodes.visionGrid.forEach(node => this.nodes.parent.removeChild(node));
return this;
}
Particle.prototype.nextFrame = function() {
this.arc = updateArc(this.arc, this.config);
this.grids.vision = updateVisionGrid(this.arc, this.config, this.grids);
this.arc = evade(this.arc, this.grids.vision);
repaintContainer(this.nodes.container, this.arc);
repaintBody(this.nodes.body, this.arc);
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;
}
}
// ===== CREATION =====
function createArc(bounds, globalGrid, config) {
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 (globalGrid[x] !== undefined && globalGrid[x][y] !== undefined) {
arc = createArc(bounds, globalGrid, config);
}
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) {
const node = document.createElement('div');
node.className = 'particle-container';
return node;
}
function createVisionGrid(config) {
const { gridSize: side, visionRadius: radius } = config;
const r0 = radius;
const r1 = radius - side;
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.parent.appendChild(div);
acc.push(div);
return acc;
}, []);
}
// ===== CALCULATIONS =====
function updateArc(arc, { bounds, randomize, speed }) {
// Randomly change radius and rotation direction.
if (arc.length <= 0) {
arc.length = random.num(RAD.t90, RAD.t360);
if (randomize === true) {
arc = moveArc(arc, random.num(100, 200));
if (random.bool(0.8)) {
arc.clockwise = !arc.clockwise;
arc = changeDirection(arc);
}
}
}
// 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);
arc.endY = arc.centerY - arc.radius * Math.sin(arc.theta);
// Overflow.
arc = overflowArc(arc, bounds);
return arc;
}
function updateVisionGrid(arc, config, grids) {
const { global, vision } = grids;
return vision.reduce((acc, point) => {
const rad = arc.clockwise
? arc.theta + point.alpha + RAD.t180
: arc.theta + point.alpha;
const x = point.r * Math.cos(rad);
const y = point.r * Math.sin(rad);
point.x = arc.endX + x;
point.y = arc.endY - y;
const gridX = point.x - point.x % 5;
const gridY = point.y - point.y % 5;
point.touch = (global[gridX] !== undefined && global[gridX][gridY] !== undefined);
return acc.concat(point);
}, []);
}
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);
arc.centerY += (r1 - r0) * Math.sin(arc.theta);
arc.radius = r1;
return arc;
}
function changeDirection(arc) {
arc.theta = (arc.theta + RAD.t180) % RAD.t360;
arc.centerX -= (2 * arc.radius) * Math.cos(arc.theta);
arc.centerY += (2 * arc.radius) * Math.sin(arc.theta);
return arc;
}
// ===== ACTIONS =====
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) {
const rad = arc.clockwise
? RAD.t180 - arc.theta
: RAD.t360 - arc.theta;
node.style.transform = `rotate(${rad + RAD.t45}rad)`;
}
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`;
node.style.top = `-${arc.radius - arc.radius * Math.sin(arc.theta)}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;