import Rx, { Observable } from 'rxjs'; import { BEHAVIOR, ENTITIES, RAD } from './enums'; import Arc from './arc'; import Random from './random'; // ===== Constructor ===== function Particle(parent, bounds, config, globalGrid) { this.config = Object.assign({}, { behavior: BEHAVIOR.FREE, 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.id = Random.id(6); this.nodes = { body: createBodyNode(this.config), circle: undefined, container: createContainerNode(this.config, this.id), parent, visionGrid: undefined, }; this.nodes.container.appendChild(this.nodes.body); parent.appendChild(this.nodes.container); this.leader = null; this.isLeader = false; this.arc = Arc.create(bounds, this.grids.global); this.updateConfig(this.config); this.nextFrame(globalGrid); }; // ===== PROTOTYPE ===== Particle.prototype.remove = function() { this.nodes.parent.removeChild(this.nodes.container); delete this.nodes; return this; } Particle.prototype.nextFrame = function() { this.arc = Arc.step(this.arc, this.config.bounds, this.config.speed); 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); if (hazards.length > 0) { this.arc = Arc.evade(this.arc); } this.updateLeader(particles); 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(particles) { if (this.config.behavior !== BEHAVIOR.COHESION) { return; } if (this.leader === null && particles.length > 0) { // Head-to-head: particles see eachother but shouldn't both lead. 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; this.leader = leader; } } if (this.leader === null) { return; } if (this.leader.nodes === undefined) { this.leader = null; return; } if (this.leader.leader !== null) { this.leader = this.leader.leader; } if (this.isLeader) { this.isLeader = false; } // 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 }); } 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) { 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 = 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) { 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; }, []); } 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) { 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 * 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 ? '2px solid red' : '0'); }); } export default Particle;