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) { this.config = { behavior: BEHAVIOR.COHESION, bounds, color: Random.color(), gridSize: 5, randomize: true, showArc: false, showVision: false, speed: 4, visionRadius: 50 }; 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), 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.grids.global.setPoint({ x: p.arc.endX, y: p.arc.endY, type: ENTITIES.PARTICLE }, p); // USE ID? this.remove$ = new Rx.Subject(); observables.fps$ .takeUntil(this.remove$) .subscribe(this.subscribeNextFrame.bind(this)); observables.speed$ .takeUntil(this.remove$) .subscribe(this.subscribeSpeed.bind(this)); observables.circle$ && observables.circle$ .takeUntil(this.remove$) .subscribe(this.subscribeCircle.bind(this)); // observables.randomize$.subscribe(this.subscribeRandomize.bind(this)); }; // ===== PROTOTYPE ===== Particle.prototype.remove = function() { // this.grids.globals.deletePoint({ x: p.arc.endX, y: p.arc.endY, type: ENTITIES.PARTICLE }); const parent = this.nodes.container.parentNode; parent.removeChild(this.nodes.container); this.remove$.next(); delete this.nodes; } Particle.prototype.subscribeNextFrame = function() { // this.arc = Arc.goto(this.arc, 200, 200, this.config.speed) if (this.nodes === undefined) { console.warn('no nodes in', this.id); return; } 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); // const prevX = p.arc.endX; // const prevY = p.arc.endY; // this.grid.deletePoint({ x: prevX, y: prevY, type: ENTITIES.PARTICLE }); // this.grid.setPoint({ x: p.arc.endX, y: p.arc.endY, type: ENTITIES.PARTICLE }, p); 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.subscribeSpeed = function(value) { this.config.speed = value; } // if (showVision === true && this.nodes.visionGrid === undefined) { // this.nodes.visionGrid = createVisionGridNodes(this.config, this.grids, this.nodes); // } // // if (showVision === false && this.nodes.visionGrid !== undefined) { // delete this.nodex.visionGrid; // } 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.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) { 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.showVision === 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;