import Rx, { Observable } from 'rxjs'; import { RAD } from './enums'; import Store from './store'; const random = { bool: (weight) => Math.random() < (weight || 0.5), num: (min, max) => min + Math.round(Math.random() * max), color: () => `rgb(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)})` } /* ===== Constructor ===== */ function Particle(parent, bounds, options) { this.options = Object.assign({ randomize: false, showMovementCircle: false, showVisionGrid: false, speed: 4 }, options); this.arc = { r: random.num(100, 200), t: random.num(RAD.t90, RAD.t360), x: random.num(0, bounds.width), y: random.num(0, bounds.height) } this.particle = { clockwise: random.bool(), color: random.color(), x: 0, y: 0 } this.visionGridPoints = []; this.interval = random.num(RAD.t90, RAD.t360); this.bounds = bounds; this.parent = parent; this.container = document.createElement('div'); this.container.className = 'particle-container'; this.body = document.createElement('div'); this.body.className = 'particle-body'; this.body.style.backgroundColor = this.particle.color; this.container.appendChild(this.body); this.parent.appendChild(this.container); this.updateOptions(this.options); this.nextFrame(); }; Particle.prototype.remove = function() { this.parent.removeChild(this.container); return this; } Particle.prototype.nextFrame = function() { this.move(); repaintContainer(this.container, this.particle); repaintBody(this.arc, this.body, this.particle); repaintCircle(this.arc, this.circle, this.particle); repaintVision(this.arc, this.vision, this.particle); } Particle.prototype.updateBounds = function(bounds) { this.bounds = bounds; } Particle.prototype.updateOptions = function(options) { Object.assign(this.options, options); if (options.showMovementCircle === true && this.circle === undefined) { this.circle = document.createElement('div'); this.circle.className = 'particle-movement-circle'; this.circle.style.borderColor = this.particle.color; this.container.appendChild(this.circle); } if (options.showMovementCircle === false && this.circle !== undefined) { this.container.removeChild(this.circle); delete this.circle; } if (options.showVisionGrid === true && this.vision === undefined) { this.vision = document.createElement('div'); this.vision.className = 'particle-vision'; this.container.appendChild(this.vision); this.visionGridPoints = calculateVisionGridPoints(this.particle); this.visionGridPoints.forEach(point => { this.vision.appendChild(point.div); }); } if (options.showVisionGrid === false && this.vision !== undefined) { // this.container.removeChild(this.vision); // delete this.vision; // // this.visionGridPoints.forEach(point => { // this.vision.removeChild(point.div); // }); // this.visionGridPoints = []; } } Particle.prototype.move = function() { // Randomly change radius and rotation direction. if (this.interval <= 0) { this.interval = random.num(RAD.t90, RAD.t360); if (this.options.randomize === true) { this.arc = moveArc(this.arc, random.num(100, 200)); if (random.bool(0.8)) { this.particle.clockwise = !this.particle.clockwise; this.arc = changeDirection(this.arc); } } } // Ensure constant velocity and theta between 0 and 2π. const delta = this.options.speed / this.arc.r; this.interval -= delta; this.arc.t += (this.particle.clockwise ? -delta : +delta); this.arc.t = (this.arc.t > 0 ? this.arc.t % RAD.t360 : RAD.t360 + this.arc.t); this.particle.x = this.arc.x + this.arc.r * Math.cos(this.arc.t); this.particle.y = this.arc.y - this.arc.r * Math.sin(this.arc.t); // Overflow. if (this.particle.x < 0) { this.particle.x += this.bounds.width; this.arc.x += this.bounds.width } else if (this.particle.x > this.bounds.width) { this.particle.x -= this.bounds.width; this.arc.x -= this.bounds.width } if (this.particle.y < 0) { this.particle.y += this.bounds.height; // TODO size of area this.arc.y += this.bounds.height } else if (this.particle.y > this.bounds.height) { this.particle.y -= this.bounds.height; this.arc.y -= this.bounds.height } } function moveArc(arc, newRadius) { const r0 = arc.r; const r1 = newRadius; // Moves arc center to new radius while keeping theta constant. arc.x -= (r1 - r0) * Math.cos(arc.t); arc.y += (r1 - r0) * Math.sin(arc.t); arc.r = r1; return arc; } function changeDirection(arc) { arc.t = (arc.t + RAD.t180) % RAD.t360; arc.x -= (2 * arc.r) * Math.cos(arc.t); arc.y += (2 * arc.r) * Math.sin(arc.t); return arc; } function calculateVisionGridPoints(particle) { const gridSize = 5; const visionRadius = 50; const r0 = Math.pow(visionRadius, 2); const r1 = Math.pow(visionRadius - gridSize, 2); const points = []; for (let x = -visionRadius; x <= visionRadius; x += gridSize) { for (let y = -visionRadius; y <= visionRadius; y += gridSize) { // Half of triangle if (x > -y) { continue; } // Vision band const p = Math.pow(x, 2) + Math.pow(y, 2); if (p > r0 || p < r1) { continue; } const div = document.createElement('div'); div.className = 'particle-vision-dot'; div.style.backgroundColor = particle.color; div.style.left = `${x + visionRadius}px`; div.style.top = `${y + visionRadius}px`; points.push({ x, y, div }); } } return points; } function repaintContainer(containerNode, particle) { containerNode.style.left = `${particle.x}px`; containerNode.style.top = `${particle.y}px`; } function repaintBody(arc, bodyNode, particle) { const rad = particle.clockwise ? RAD.t180 - arc.t : RAD.t360 - arc.t; bodyNode.style.transform = `rotate(${rad + RAD.t45}rad)`; } function repaintVision(arc, visionNode, particle) { if (visionNode === undefined) { return; } const rad = particle.clockwise ? RAD.t180 - arc.t : RAD.t360 - arc.t; visionNode.style.transform = `rotate(${rad + RAD.t45}rad)`; } function repaintCircle(arc, circleNode) { if (circleNode === undefined) { return; } circleNode.style.width = `${2 * arc.r}px`; circleNode.style.height = `${2 * arc.r}px`; circleNode.style.left = `-${arc.r + arc.r * Math.cos(arc.t)}px`; circleNode.style.top = `-${arc.r - arc.r * Math.sin(arc.t)}px`; circleNode.style.borderRadius = `${arc.r}px`; } export default Particle;