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 = { 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), }; this.nodes = { body: createBodyNode(this.config), circle: undefined, container: createContainerNode(this.config), parent, vision: undefined, 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 = updateArc(this.arc, this.config); this.grids.vision = updateVisionGrid(this.arc, this.config, this.grids); repaintContainer(this.nodes.container, this.arc); repaintBody(this.nodes.body, this.arc); repaintCircle(this.nodes.circle, this.arc); repaintVision(this.nodes.vision, 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.vision === undefined) { this.nodes.vision = createVisionNode(config, this.grids); this.nodes.visionGrid = createVisionGridNodes(this.config, this.grids, this.nodes); this.nodes.container.appendChild(this.nodes.vision); } if (showVisionGrid === false && this.nodes.vision !== undefined) { this.nodes.container.removeChild(this.nodes.vision); delete this.nodes.vision; delete this.nodex.visionGrid; } } // ===== 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) { const node = document.createElement('div'); node.className = 'particle-container'; return node; } function createVisionNode(config) { if (config.showVisionGrid === false) { return undefined; } const node = document.createElement('div'); node.className = 'particle-vision'; return node; } function createVisionGrid(config) { if (config.showVisionGrid === false) { return []; } 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 lower half if (y < 0) { 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 + alpha; } 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.vision.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. 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 updateVisionGrid(arc, config, grids) { const { global, vision } = grids; return vision.reduce((acc, point) => { const rad = arc.clockwise ? arc.theta - point.alpha : arc.theta - point.alpha + RAD.t180; point.x = point.r * Math.cos(rad) + config.visionRadius; point.y = -point.r * Math.sin(rad) + config.visionRadius; // console.warn(point.alpha, point.alpha + arc.theta) // 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 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; } // ===== 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 repaintVision(node, arc) { if (node === undefined) { return; } const rad = arc.clockwise ? RAD.t180 - arc.theta : RAD.t360 - arc.theta; } 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;