parent
6544b3b5f6
commit
905921ceb0
6 changed files with 489 additions and 200 deletions
@ -0,0 +1,279 @@ |
|||||||
|
// 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 visionRadius = 50; |
||||||
|
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 visionGrid = document.createElement('div'); |
||||||
|
visionGrid.className = 'anim3-vision-grid'; |
||||||
|
|
||||||
|
const particle = document.createElement('div'); |
||||||
|
particle.className = 'anim3-particle'; |
||||||
|
|
||||||
|
const visionGridPoints = calculateVisionGridPoints(); |
||||||
|
|
||||||
|
function move(store) { |
||||||
|
let { |
||||||
|
clockwise, |
||||||
|
interval, |
||||||
|
circleX, |
||||||
|
circleY, |
||||||
|
particleX, |
||||||
|
particleY, |
||||||
|
radius, |
||||||
|
theta |
||||||
|
} = store.get(); |
||||||
|
|
||||||
|
// If wall detected, tight 180
|
||||||
|
if (detectWall(store) && radius !== 20) { |
||||||
|
const radius0 = radius; |
||||||
|
radius = 20; |
||||||
|
interval = Math.PI * 20 / speed; |
||||||
|
circleX -= (radius - radius0) * Math.cos(theta); |
||||||
|
circleY += (radius - radius0) * Math.sin(theta); |
||||||
|
} else if (interval <= 0) { |
||||||
|
const radius0 = radius; |
||||||
|
|
||||||
|
interval = Math.round(Math.random() * 10) + 50; |
||||||
|
radius = Math.round(Math.random() * 200) + 50; |
||||||
|
|
||||||
|
if (Math.random() < 0.5) { |
||||||
|
clockwise = !clockwise; |
||||||
|
theta = (theta + Math.PI) % t360; |
||||||
|
circleX -= (radius + radius0) * Math.cos(theta); |
||||||
|
circleY += (radius + radius0) * Math.sin(theta); |
||||||
|
} else { |
||||||
|
circleX -= (radius - radius0) * Math.cos(theta); |
||||||
|
circleY += (radius - radius0) * Math.sin(theta); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
interval -= 1; |
||||||
|
|
||||||
|
const delta = speed / radius; |
||||||
|
theta += (clockwise ? -delta : +delta); |
||||||
|
theta = (theta > 0 ? theta % t360 : t360 - theta); |
||||||
|
|
||||||
|
particleX = circleX + radius * Math.cos(theta); |
||||||
|
particleY = circleY - radius * Math.sin(theta); |
||||||
|
|
||||||
|
store.set({ |
||||||
|
clockwise, |
||||||
|
interval, |
||||||
|
circleX, |
||||||
|
circleY, |
||||||
|
particleX, |
||||||
|
particleY, |
||||||
|
radius, |
||||||
|
theta |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
function detectWall(store) { |
||||||
|
const len = visionGridPoints.length; |
||||||
|
|
||||||
|
const { particleX, particleY } = store.get(); |
||||||
|
|
||||||
|
const gridX = particleX - particleX % 5; |
||||||
|
const gridY = particleY - particleY % 5; |
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) { |
||||||
|
const { x, y } = visionGridPoints[i]; |
||||||
|
const xx = x + gridX; |
||||||
|
const yy = y + gridY; |
||||||
|
|
||||||
|
if (grid[xx] && grid[xx][yy] && grid[xx][yy].type === 'wall') { |
||||||
|
console.warn("Wall detected", xx, yy); |
||||||
|
// TODO this goes somewhere else
|
||||||
|
// DOM.container.dispatchEvent(new CustomEvent('stop'));
|
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
function transformMovementCircle(store) { |
||||||
|
// TODO UPDATE ONLY IF THETA CHANGED - MUST HAVE PREVSTATE
|
||||||
|
const { circleX, circleY, radius, theta } = store.get(); |
||||||
|
const r = Math.abs(radius); |
||||||
|
|
||||||
|
movementCircle.style.left = `${circleX - r}px`; |
||||||
|
movementCircle.style.top = `${circleY - r}px`; |
||||||
|
movementCircle.style.height = `${2 * r}px`; |
||||||
|
movementCircle.style.width = `${2 * r}px`; |
||||||
|
movementCircle.style.borderRadius = `${r}px`; |
||||||
|
} |
||||||
|
|
||||||
|
function transformParticle(store) { |
||||||
|
const { clockwise, particleX, particleY, theta } = store.get(); |
||||||
|
const rad = clockwise ? Math.PI - theta : t360 - theta; |
||||||
|
|
||||||
|
particle.style.left = `${particleX}px`; |
||||||
|
particle.style.top = `${particleY}px`; |
||||||
|
particle.style.transform = `rotate(${rad}rad)`; |
||||||
|
} |
||||||
|
|
||||||
|
function transformVisionGrid(store) { |
||||||
|
const { |
||||||
|
circleX, |
||||||
|
circleY, |
||||||
|
clockwise, |
||||||
|
particleX, |
||||||
|
particleY, |
||||||
|
radius, |
||||||
|
theta |
||||||
|
} = store.get(); |
||||||
|
|
||||||
|
const r0 = Math.min(theta, theta - Math.PI); |
||||||
|
const r1 = Math.max(theta, theta + 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'); |
||||||
|
} else if (alpha >= theta && alpha <= r1) { |
||||||
|
div.style.display = (clockwise ? 'none' : 'block'); |
||||||
|
} else { |
||||||
|
div.style.display = (clockwise ? 'block' : 'none'); |
||||||
|
} |
||||||
|
|
||||||
|
div.style.left = `${x + gridX}px`; |
||||||
|
div.style.top = `${-y + gridY}px`; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
function calculateVisionGridPoints() { |
||||||
|
const gridSize = 5; |
||||||
|
|
||||||
|
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({ |
||||||
|
clockwise: false, |
||||||
|
interval: 10, |
||||||
|
circleX: 300, |
||||||
|
circleY: 300, |
||||||
|
radius: 270, |
||||||
|
theta: Math.PI * 3 / 4 |
||||||
|
}); |
||||||
|
|
||||||
|
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 / 32) |
||||||
|
.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(transformMovementCircle);
|
||||||
|
fps$.subscribe(transformVisionGrid); |
||||||
|
}; |
||||||
|
|
||||||
|
const Animation3 = Object.assign({}, AnimationBase, { init, reset }); |
||||||
|
|
||||||
|
export default Animation3; |
Loading…
Reference in new issue