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.
344 lines
9.2 KiB
344 lines
9.2 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: true,
|
|
showMovementCircle: false,
|
|
showVisionGrid: false,
|
|
speed: 4,
|
|
visionRadius: 50
|
|
}, config);
|
|
|
|
this.grids = {
|
|
global: globalGrid || {},
|
|
vision: createVisionGrid(this.config)
|
|
};
|
|
|
|
this.arc = createArc(bounds, this.grids, 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);
|
|
return this;
|
|
}
|
|
|
|
Particle.prototype.nextFrame = function() {
|
|
this.arc = stepArc(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, grids, 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 (grids.global[x] !== undefined && grids.global[x][y] !== undefined) {
|
|
arc = createArc(bounds, grids, 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.container.appendChild(div);
|
|
|
|
acc.push(div);
|
|
|
|
return acc;
|
|
}, []);
|
|
}
|
|
|
|
// ===== CALCULATIONS =====
|
|
|
|
function stepArc(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 = reverseArc(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); // TODO perf here
|
|
arc.endY = arc.centerY - arc.radius * Math.sin(arc.theta); // TODO perf here
|
|
|
|
// Overflow.
|
|
arc = overflowArc(arc, bounds);
|
|
|
|
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.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 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);
|
|
|
|
const x = arc.endX + point.x;
|
|
const y = arc.endY + point.y;
|
|
|
|
const gridX = x - x % 5;
|
|
const gridY = y - y % 5;
|
|
|
|
point.touch = (global[gridX] !== undefined && global[gridX][gridY] !== undefined);
|
|
|
|
return acc.concat(point);
|
|
}, []);
|
|
}
|
|
|
|
// ===== 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`; // 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;
|
|
|