// Single particle movement. // Goal: per-frame decisions // 20 x 20 grid // The trickiest portion of this iteration was animating the curved paths. Calculating arc length (for scalar speed) along an elliptical // geometry is quite difficult, so the current solution uses circular paths, which change radius and sometimes rotation after a random // number of frames have emitted. A smoothstep cubic curve was considered, but maintaining consistent entry and exit angles will affect // performance for large particle counts. // This algorithm travels a random period of time around a random arc. // If a wall is detected, a 180-degree turn is executed. import Rx, { Observable } from 'rxjs'; import AnimationBase from './animation0'; import DOM from './dom'; import Store from './store'; const speed = 4; const grid = {}; const t45 = Math.PI / 4; const t90 = Math.PI / 2; const t270 = 3 * Math.PI / 2; const t360 = Math.PI * 2; const movementCircle = document.createElement('div'); movementCircle.className = 'anim3-movement-circle'; const particle = document.createElement('div'); particle.className = 'anim3-particle'; const visionGridPoints = calculateVisionGridPoints(); function move(store) { let { arc, clockwise, particleX, particleY, } = store.get(); const delta = speed / arc.r; arc.t += (clockwise ? -delta : +delta); arc.t = (arc.t > 0 ? arc.t % t360 : t360 - arc.t); const wall = detectWall(store); if (wall.x !== null) { // console.warn(wall.x, wall.y, particleX, particleY) const x = wall.x - particleX; const y = wall.y - particleY; const v = Math.atan((particleY - wall.y) / (particleX - wall.x)); arc = modifyArc(arc, 40); } else { arc = modifyArc(arc, 400); } particleX = arc.x + arc.r * Math.cos(arc.t); particleY = arc.y - arc.r * Math.sin(arc.t); store.set({ arc, particleX, particleY, }); } // generate next arc: // starting point will be locked // starting angle will be locked // therefore tangential function modifyArc(arc, newRadius) { const r0 = arc.r; const r1 = newRadius; arc.x -= (r1 - r0) * Math.cos(arc.t); arc.y += (r1 - r0) * Math.sin(arc.t); arc.r = r1; return arc; } function changeDirection(arc) { // clockwise = !clockwise; // theta = (theta + Math.PI) % t360; // circleX -= (radius + radius0) * Math.cos(theta); // circleY += (radius + radius0) * Math.sin(theta); } function detectWall(store) { const len = visionGridPoints.length; const { arc, clockwise, particleX, particleY } = store.get(); const r0 = Math.min(arc.t, arc.t - Math.PI); const r1 = Math.max(arc.t, arc.t + Math.PI); const gridX = particleX - particleX % 5; const gridY = particleY - particleY % 5; return visionGridPoints.reduce((acc, { div, x, y, alpha }) => { const xx = x + gridX; const yy = gridY - y; if (grid[xx] && grid[xx][yy] && grid[xx][yy].type === 'wall') { if (alpha >= 0 && alpha <= r0) { if (clockwise === false) { DOM.addClass(div, 'touching'); } return (clockwise ? acc : { x: xx, y: yy }); } else if (alpha >= arc.t && alpha <= r1) { if (clockwise === false) { DOM.addClass(div, 'touching'); } return (clockwise ? acc : { x: xx, y: yy }); } else { if (clockwise === true) { DOM.addClass(div, 'touching'); } return (clockwise ? { x: xx, y: yy } : acc); } } DOM.removeClass(div, 'touching'); return acc; }, { x: null, y: null }); } function transformParticle(store) { const { arc, clockwise, particleX, particleY } = store.get(); const rad = clockwise ? Math.PI - arc.t : t360 - arc.t; particle.style.left = `${particleX}px`; particle.style.top = `${particleY}px`; particle.style.transform = `rotate(${rad}rad)`; } function transformVisionGrid(store) { const { arc, clockwise, particleX, particleY, radius, } = store.get(); const r0 = Math.min(arc.t, arc.t - Math.PI); const r1 = Math.max(arc.t, arc.t + Math.PI); const gridX = particleX - particleX % 5; const gridY = particleY - particleY % 5; visionGridPoints.forEach(({ x, y, alpha, div }, i) => { if (alpha >= 0 && alpha <= r0) { div.style.display = (clockwise ? 'none' : 'block'); // div.className = (clockwise ? 'anim3-dot removed' : 'anim3-dot'); } else if (alpha >= arc.t && alpha <= r1) { div.style.display = (clockwise ? 'none' : 'block'); // div.className = (clockwise ? 'anim3-dot removed' : 'anim3-dot'); } else { div.style.display = (clockwise ? 'block' : 'none'); // div.className = (clockwise ? 'anim3-dot' : 'anim3-dot removed'); } div.style.left = `${x + gridX}px`; div.style.top = `${-y + gridY}px`; }); } function calculateVisionGridPoints() { const gridSize = 5; const visionRadius = 50; const squareGrid = []; for (let x = -visionRadius; x <= visionRadius; x += gridSize) { for (let y = -visionRadius; y <= visionRadius; y += gridSize) { let alpha = Math.atan(y / x); if (x === 0 && y === 0) { alpha = 0; } else if (x === 0 && y < 0) { alpha = t270; } else if (y === 0 && x < 0) { alpha = Math.PI; } else if (x === 0 && y > 0) { alpha = t90; } else if (x < 0 && y < 0) { alpha = alpha + Math.PI; } else if (x <= 0) { alpha = Math.PI + alpha; } else if (y < 0) { alpha = 2 * Math.PI + alpha; } squareGrid.push({ x, y, alpha }); } } const r0 = Math.pow(visionRadius, 2); const r1 = Math.pow(visionRadius - gridSize, 2); return squareGrid.reduce((acc, point) => { const p = Math.pow(point.x, 2) + Math.pow(point.y, 2); if (p > r0 || p < r1) { return acc; } const div = document.createElement('div'); div.className = 'anim3-dot'; acc.push(Object.assign(point, { div })); return acc; }, []); } function reset() { while (DOM.container.childNodes.length) { DOM.container.removeChild(DOM.container.firstChild); } const store = new Store({ arc: { r: Math.round(Math.random() * 200) + 100, t: Math.random() * t360, x: 300, y: 300, }, clockwise: false, }); move(store); transformParticle(store); // transformMovementCircle(store); transformVisionGrid(store); DOM.container.appendChild(particle); DOM.container.appendChild(movementCircle); visionGridPoints.forEach(point => { DOM.container.appendChild(point.div); }); return store; }; function init() { const store = reset(); for (let x = 0; x <= 600; x += 5) { grid[x] = {}; for (let y = 0; y <= 600; y += 5) { grid[x][y] = { type: null }; if (x === 0 || y === 0 || x === 600 || y === 600) { grid[x][y] = { type: 'wall' }; } } } const stop$ = Rx.Observable.fromEvent(DOM.container, 'stop'); const fps$ = Rx.Observable.interval(1000 / 256) .map(_ => store) // .take(300) // .take(15) .takeUntil(stop$); console.error("CLICK TO STOP"); const click$ = Rx.Observable.fromEvent(DOM.container, 'click'); click$.subscribe(() => { DOM.container.dispatchEvent(new CustomEvent('stop')); }); fps$.subscribe(move); fps$.subscribe(transformParticle); fps$.subscribe(transformVisionGrid); // fps$.subscribe(transformMovementCircle); }; const Animation3 = Object.assign({}, AnimationBase, { init, reset }); export default Animation3;