Preparing for Controls UI. Multiple particles now supported.

master
Ben Burlingham 8 years ago
parent 545c632300
commit 5e74dc90af
  1. 13
      css/animation2.scss
  2. 20
      css/animation5.scss
  3. 71
      css/index.scss
  4. 66
      css/particle.scss
  5. 78
      css/style.css
  6. 49
      index.html
  7. 6
      js/animation0.js
  8. 81
      js/animation1.js
  9. 2
      js/animation2.js
  10. 214
      js/animation3-WALLDETECT-BUGGY.js
  11. 2
      js/animation4.js
  12. 117
      js/animation5.js
  13. 2688
      js/bundle.js
  14. 23
      js/controls.js
  15. 39
      js/dom.js
  16. 9
      js/enums.js
  17. 20
      js/index.js
  18. 171
      js/particle.js

@ -1,13 +0,0 @@
.anim2-particle {
$side: 100px;
background: url('../res/seahorse.svg') no-repeat center center;
background-size: 20px 20px;
border-color: salmon;
border-style: dashed;
border-radius: 50px;
border-width: 1px;
height: $side;
position: absolute;
width: $side;
}

@ -1,20 +0,0 @@
.anim3-particle {
$side: 20px;
background: url('../res/seahorse.svg') no-repeat center top #aaa;
background-size: 20px 20px;
border-color: purple;
color: #fff;
// border-style: dashed;
border-radius: $side / 2;
// border-width: 1px;
height: $side;
line-height: $side;
position: absolute;
text-align: center;
width: $side;
&.scared {
background: #f00;
}
}

@ -3,41 +3,42 @@
// background: url('../res/seigaiha.svg');
// background-size: 100px 50px;
// border-radius: 50px;
box-shadow: 2px 2px 0 #aaa;
height: 600px;
// box-shadow: 2px 2px 0 #aaa;
height: 400px;
margin: 10px auto;
overflow: hidden;
position: relative;
width: 600px;
}
.palette {
background: url('../res/palette.svg');
background-size: 167px 100px;
height: 100px;
position: absolute;
width: 167px;
}
.highlight {
$h: 30px;
animation: pulse 0.5s 1;
border-radius: $h / 2;
position: absolute;
}
@keyframes pulse {
$h: 30px;
from {
border: 4px solid lightgreen;
height: $h;
margin: (-1 * $h / 2) 0 0 (-1 * $h / 2);
width: $h;
}
to {
height: 0px;
margin: 0;
width: 0px;
}
width: 90%;
}
//
// .palette {
// background: url('../res/palette.svg');
// background-size: 167px 100px;
// height: 100px;
// position: absolute;
// width: 167px;
// }
//
// .highlight {
// $h: 30px;
// animation: pulse 0.5s 1;
// border-radius: $h / 2;
// position: absolute;
// }
//
// @keyframes pulse {
// $h: 30px;
//
// from {
// border: 4px solid lightgreen;
// height: $h;
// margin: (-1 * $h / 2) 0 0 (-1 * $h / 2);
// width: $h;
// }
//
// to {
// height: 0px;
// margin: 0;
// width: 0px;
// }
// }

@ -1,4 +1,4 @@
.anim3-particle {
.particle {
$side: 20px;
background: url('../res/seahorse.svg') no-repeat center center #aaa;
@ -13,7 +13,7 @@
width: $side;
z-index: 1;
&::after {
&.has-vision::after {
border: 50px solid;
border-color: lightgreen transparent transparent turquoise;
border-radius: 50px;
@ -29,7 +29,7 @@
}
}
.anim3-movement-circle {
.particle-movement-circle {
border: 2px dotted darkturquoise;
position: absolute;
transition: left 0.2s, top 0.2s, height 0.2s, width 0.2s;
@ -48,33 +48,33 @@
width: 4px;
}
}
.anim3-vision-grid {
height: 100px;
// margin-top: 50px;
// margin-left: 50px;
position: absolute;
transform-origin: left top;
width: 100px;
z-index: 99;
}
.anim3-dot {
$s: 2px;
background: red;
// border-radius: 2px;
height: $s;
position: absolute;
width: $s;
z-index: 99;
&.removed {
// background: purple;
background: green;
}
&.touching {
outline: 2px solid yellow;
}
}
//
// .anim3-vision-grid {
// height: 100px;
// // margin-top: 50px;
// // margin-left: 50px;
// position: absolute;
// transform-origin: left top;
// width: 100px;
// z-index: 99;
// }
//
// .anim3-dot {
// $s: 2px;
//
// background: red;
// // border-radius: 2px;
// height: $s;
// position: absolute;
// width: $s;
// z-index: 99;
//
// &.removed {
// // background: purple;
// background: green;
// }
//
// &.touching {
// outline: 2px solid yellow;
// }
// }

@ -7,45 +7,12 @@ body {
font-family: sans-serif; }
.particles {
background: rgba(102, 51, 153, 0.1);
box-shadow: 2px 2px 0 #aaa;
height: 600px;
height: 400px;
margin: 10px auto;
overflow: hidden;
position: relative;
width: 600px; }
.palette {
background: url(../res/palette.svg);
background-size: 167px 100px;
height: 100px;
position: absolute;
width: 167px; }
.highlight {
animation: pulse 0.5s 1;
border-radius: 15px;
position: absolute; }
@keyframes pulse {
from {
border: 4px solid lightgreen;
height: 30px;
margin: -15px 0 0 -15px;
width: 30px; }
to {
height: 0px;
margin: 0;
width: 0px; } }
.anim2-particle {
background: url(../res/seahorse.svg) no-repeat center center;
background-size: 20px 20px;
border-color: salmon;
border-style: dashed;
border-radius: 50px;
border-width: 1px;
height: 100px;
position: absolute;
width: 100px; }
.anim3-particle {
width: 90%; }
.particle {
background: url(../res/seahorse.svg) no-repeat center center #aaa;
background-size: 20px 20px;
border-radius: 10px;
@ -57,7 +24,7 @@ body {
text-align: center;
width: 20px;
z-index: 1; }
.anim3-particle::after {
.particle.has-vision::after {
border: 50px solid;
border-color: lightgreen transparent transparent turquoise;
border-radius: 50px;
@ -71,12 +38,12 @@ body {
top: -50px;
width: 0; }
.anim3-movement-circle {
.particle-movement-circle {
border: 2px dotted darkturquoise;
position: absolute;
transition: left 0.2s, top 0.2s, height 0.2s, width 0.2s;
z-index: 0; }
.anim3-movement-circle:after {
.particle-movement-circle:after {
background: darkturquoise;
border-radius: 2px;
content: ' ';
@ -87,34 +54,3 @@ body {
position: absolute;
top: 50%;
width: 4px; }
.anim3-vision-grid {
height: 100px;
position: absolute;
transform-origin: left top;
width: 100px;
z-index: 99; }
.anim3-dot {
background: red;
height: 2px;
position: absolute;
width: 2px;
z-index: 99; }
.anim3-dot.removed {
background: green; }
.anim3-dot.touching {
outline: 2px solid yellow; }
.anim3-particle {
background: url(../res/seahorse.svg) no-repeat center top #aaa;
background-size: 20px 20px;
border-color: purple;
color: #fff;
border-radius: 10px;
height: 20px;
line-height: 20px;
position: absolute;
text-align: center;
width: 20px; }
.anim3-particle.scared {
background: #f00; }

@ -3,44 +3,55 @@
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="css/style.css">
<script src='/core/js/ui.js'></script>
<!-- <script src='/core/js/ui.js'></script> STARTING ANIMATIOn RIGHT AWAY PERF HIT IF ENABLED -->
</head>
<body>
<h1>Dust</h1>
<h2>Studying AI movement along arcs with RxJs</h2>
<h2>AI Swarm Movement with RxJs</h2>
<hr>
<div class="controls">
Restart
Change direction
Change animation speed
Show movement circle
Show vision grid
Show touching points
Start / stop
</div>
<h3>Project Goal</h3>
<div class="particles"></div>
<blockquote>
Explore the RxJs API by managing moving particle systems. The systems should:
<ul>
<li>Have particle movement that feels calm and natural</li>
<li>Support large swarms of particles</li>
<li>Be able to evade obstacles</li>
</ul>
</blockquote>
<hr>
<p>
The trickiest portion of this iteration was animating the curved paths. I explored elliptical geometry,
but calculating arc length (to maintain a scalar speed) is quite difficult. Smoothstep cubic curves
were also an option, but maintaining consistent entry and exit angles could affect performance for large
groups. The current design uses circular paths that smoothly change direction and rotation.
</p>
<div class='particles' id="animation1"></div>
<!--
// Another tricky part was the wall avoidance and vision grid.
The ultimate goal is to simulate particles that move in swarms and calculate their
position independently, similar to flocks of birds.
They should take into account disturbances, walls, individual initiative, and landing areas.
<h2>Animation 1</h2>
Goal: Animate several seahorses over an interval.
Key points:
- range() used to create N divs
- interval() used to create FPS
<h2>Animation 2</h2>
Goal: Scare a seahorse with a click. Move her to a safe distance away.
Key points:
- interval() used to create FPS
- last() used with takeWhile() to only update state store at end
- scan() re-emits on each emission. reduce() only emits after last emission (take()).
- CustomEvent can bridge streams
- State store passed between streams
- Random movement vector, collision detection
- Random movement vector, collision detection -->
<script src='js/bundle.js'></script>
</body>

@ -1,6 +0,0 @@
const Animation0 = {
init: () => { console.error('init() not implemented.'); },
reset: () => { console.error('reset() not implemented.'); },
};
export default Animation0;

@ -1,33 +1,58 @@
// Simple frame-based movement.
import Rx, { Observable } from 'rxjs';
import Particle from './particle';
import Store from './store';
const Animation1 = {
// const particleDivs = [];
//
// const instanceCount = Rx.Observable.range(0, 3);
//
// const createDivs = instanceCount.subscribe((i) => {
// console.warn("creating divs")
// const container = document.querySelector('.particles');
// const div = document.createElement('div');
// div.className = 'particle';
// div.style.top = `${i * 75}px`;
// div.style.left = 0;
//
// container.appendChild(div);
// particleDivs.push(div);
// });
//
// Rx.Observable
// .interval(1000 / 32)
// .do(frameIndex => {
// instanceCount.subscribe(i => {
// particleDivs[i].style.left = `${frameIndex * 10}px`;
// });
// // particleDivs.do(div => { div.style.left = `${n * 75}px`; }).subscribe();
// })
// .take(16)
// .subscribe(console.info)
function Animation1(node) {
this.container = node;
this.bounds = node.getBoundingClientRect();
}
// const grid = {};
// 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' };
// }
// }
// }
Animation1.prototype.nextFrame = function() {
this.particles.forEach(p => p.nextFrame());
}
Animation1.prototype.reset = () => {
// while (DOM.container.childNodes.length) {
// DOM.container.removeChild(DOM.container.firstChild);
// }
};
Animation1.prototype.init = function() {
this.particles = Array(1).fill().map(_ => new Particle(this.container, this.bounds));
const stop$ = Rx.Observable.fromEvent(this.container, 'stop');
// Change animation speed
// Change animal pic
// Enable random radius changes
// Enable random rotation changes
// Show movement circle
// Show vision grid (including touches!)
// Start / stop
console.error("Click container to stop.");
const fps$ = Rx.Observable.interval(1000 / 32)
.takeUntil(stop$)
.finally(() => { console.error("Stopped."); })
const click$ = Rx.Observable.fromEvent(this.container, 'click');
click$.subscribe(() => {
this.container.dispatchEvent(new CustomEvent('stop'));
});
fps$.subscribe(this.nextFrame.bind(this));
}
export default Animation1;

@ -1,6 +1,6 @@
// Scare mechanic, single particle.
import Rx, { Observable } from 'rxjs';
import AnimationBase from './animation0';
import AnimationBase from './animationBase';
import DOM from './dom';
import Store from './store';

@ -30,12 +30,13 @@ movementCircle.className = 'anim3-movement-circle';
const particle = document.createElement('div');
particle.className = 'anim3-particle';
const visionGridPoints = calculateVisionGridPoints();
// const visionGridPoints = calculateVisionGridPoints();
function move(store) {
let {
arc,
clockwise,
frame,
particleX,
particleY,
} = store.get();
@ -44,37 +45,40 @@ function move(store) {
arc.t += (clockwise ? -delta : +delta);
arc.t = (arc.t > 0 ? arc.t % t360 : t360 - arc.t);
const intersections = detectWall(store);
if (intersections.length > 0) {
const { xs, ys } = intersections.reduce(
({ xs, ys }, {x, y}) => ({ xs: xs + x, ys: ys + y }),
{ xs: 0, ys: 0 }
);
const avgX = xs / intersections.length;
const avgY = ys / intersections.length;
const v = Math.atan((particleY - avgY) / (particleX - avgX));
// console.warn(Math.round(v * 180 / Math.PI))
const modifier = Math.max(Math.round(v * 180 / Math.PI), 20);
arc = modifyArc(arc, 20);
// } else if (Math.random() < 0.005) {
// console.warn('changing direction')
// clockwise = !clockwise;
// arc = changeDirection(arc);
} else {
arc = modifyArc(arc, 300);
}
// const intersections = detectWall(store);
//
// if (intersections.length > 0) {
// const { xs, ys } = intersections.reduce(
// ({ xs, ys }, {x, y}) => ({ xs: xs + x, ys: ys + y }),
// { xs: 0, ys: 0 }
// );
//
// const avgX = xs / intersections.length;
// const avgY = ys / intersections.length;
//
// const v = Math.atan((particleY - avgY) / (particleX - avgX));
// // console.warn(Math.round(v * 180 / Math.PI))
//
// const modifier = Math.max(Math.round(v * 180 / Math.PI), 20);
//
// arc = modifyArc(arc, 20);
// // } else if (Math.random() < 0.005) {
// // console.warn('changing direction')
// // clockwise = !clockwise;
// // arc = changeDirection(arc);
// } else {
// arc = modifyArc(arc, 300);
// }
particleX = arc.x + arc.r * Math.cos(arc.t);
particleY = arc.y - arc.r * Math.sin(arc.t);
frame += 1;
store.set({
arc,
clockwise,
frame,
particleX,
particleY,
});
@ -103,35 +107,35 @@ function changeDirection(arc) {
return arc;
}
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, point) => {
const xx = gridX + point.x;
const yy = gridY - point.y;
const alpha = point.alpha;
if (grid[xx] && grid[xx][yy] && grid[xx][yy].type === 'wall') {
if (clockwise === false && alpha >= 0 && alpha <= r0) {
acc.push(point);
} else if (clockwise === false && alpha >= arc.t && alpha <= r1) {
acc.push(point);
} else if (clockwise === true) {
acc.push(point);
}
}
return acc;
}, []);
}
// 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, point) => {
// const xx = gridX + point.x;
// const yy = gridY - point.y;
// const alpha = point.alpha;
//
// if (grid[xx] && grid[xx][yy] && grid[xx][yy].type === 'wall') {
// if (clockwise === false && alpha >= 0 && alpha <= r0) {
// acc.push(point);
// } else if (clockwise === false && alpha >= arc.t && alpha <= r1) {
// acc.push(point);
// } else if (clockwise === true) {
// acc.push(point);
// }
// }
//
// return acc;
// }, []);
// }
function transformParticle(store) {
const { arc, clockwise, particleX, particleY } = store.get();
@ -174,51 +178,51 @@ function transformVisionGrid(store) {
});
}
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 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) {
@ -239,14 +243,14 @@ function reset() {
transformParticle(store);
// transformMovementCircle(store);
transformVisionGrid(store);
// transformVisionGrid(store);
DOM.container.appendChild(particle);
DOM.container.appendChild(movementCircle);
visionGridPoints.forEach(point => {
DOM.container.appendChild(point.div);
});
// visionGridPoints.forEach(point => {
// DOM.container.appendChild(point.div);
// });
return store;
};
@ -266,8 +270,8 @@ function init() {
}
const stop$ = Rx.Observable.fromEvent(DOM.container, 'stop');
const fps$ = Rx.Observable.interval(1000 / 128)
.map(_ => store)
const fps$ = Rx.Observable.interval(1000 / 32)
.map(i => store.bind(null, i))
// .take(300)
// .take(15)
.takeUntil(stop$); console.error("CLICK TO STOP");

@ -7,7 +7,7 @@
// find if palette nearby
import Rx, { Observable } from 'rxjs';
import AnimationBase from './animation0';
import AnimationBase from './animationBase';
import DOM from './dom';
import Store from './store';

@ -1,117 +0,0 @@
// Join mechanic, multiple particles.
// Goal: per-frame decisions
// 20 x 20 grid
import Rx, { Observable } from 'rxjs';
import AnimationBase from './animation0';
import DOM from './dom';
import Store from './store';
//
// const evtScare = (detail) => new CustomEvent('scare', { detail });
// const evtMove = (detail) => new CustomEvent('move', { detail });
//
// const [particles, state] = (new Array(5)).fill(null).reduce((acc, v, i) => {
// // const div = document.createElement('div');
// // div.className = 'anim3-particle';
// // div.innerHTML = ''
// //
// // const x = 0;
// // const y = i * 20;
// //
// // div.style.left = 0
// // div.style.top = `${y}px`;
// //
// // acc[0].push(div);
// // acc[1].push({ x, y });
// //
// // acc[1][`${x}-${y}`] = { occupied: true, type: 'palette', x, y, i };
// //
// // return acc;
// }, [[], []]);
//
// const palettes = (new Array(1)).fill(null).reduce((acc, v, i) => {
// const initialX = 200;
// const initialY = 200;
// const w = 167;
// const h = 100;
// const maxX = initialX + w;
// const maxY = initialY + h;
// const s = 20;
//
// for (let y = initialY; y < maxY; y += s) {
// for (let x = initialX; x < maxX; x += s) {
// state[`${x}-${y}`] = { occupied: true, type: 'palette', i };
// }
// }
//
// const div = document.createElement('div');
// div.className = 'palette';
// div.style.left = `${initialX}px`;
// div.style.top = `${initialY}px`;
//
// acc.push(div);
//
// return acc;
// }, []);
//
// function scare(evt) {
// const bounds = DOM.container.getBoundingClientRect();
// const { evtX: x, evtY: y } = DOM.getEventOffsetCoords(evt, bounds);
// const scareRadius = 50;
//
// state.forEach((coord, i) => {
// const diffX = Math.abs(coord.x - x + 10);
// const diffY = Math.abs(coord.y - y + 10);
//
// if (diffX < scareRadius && diffY < scareRadius) {
// coord.lastScare = { x, y } // TODO set state with last scare, then judge per frame based on that number to avoid jump
// DOM.container.dispatchEvent(evtScare({ x, y, i }));
// }
// });
// }
//
// function move(evt) {
//
// }
//
// function flee(evt) {
// particles[evt.detail.i].innerHTML = 'S'
// DOM.addClass(particles[evt.detail.i], 'scared');
// const p = particles[evt.detail.i];
// DOM.container.dispatchEvent(evtMove(evt.detail));
//
// setTimeout(() => {
// p.innerHTML = '';
// DOM.removeClass(p, 'scared');
// }, 1000);
// }
//
function reset() {
// while (DOM.container.childNodes.length) {
// DOM.container.removeChild(DOM.container.firstChild);
// }
//
// particles.forEach((div) => {
// div.innerHTML = '';
// DOM.container.appendChild(div)
// });
//
// palettes.forEach((div) => {
// DOM.container.appendChild(div)
// });
};
function init() {
// reset();
//
// const click$ = Rx.Observable.fromEvent(DOM.container, 'click');
// const scare$ = Rx.Observable.fromEvent(DOM.container, 'scare').auditTime(100);
// const move$ = Rx.Observable.fromEvent(DOM.container, 'move');
//
// click$.subscribe(scare);
// scare$.subscribe(flee);
// move$.subscribe(console.info);
};
const Animation5 = Object.assign({}, AnimationBase, { init, reset });
export default Animation5;

File diff suppressed because it is too large Load Diff

23
js/controls.js vendored

@ -0,0 +1,23 @@
function Controls(node, animation) {
this.node = node;
this.animation = new animation(node);
if (this.animation.init === undefined) {
console.error("Animation passed to Control doesn't have an init() method.");
}
if (this.animation.reset === undefined) {
console.error("Animation passed to Control doesn't have a reset() method.");
}
this.animation.init();
}
Controls.prototype.mount = function() {
// this.node.style.border = '10px solid purple'; WORKING
// right aligned panel, pass in extra custom controls array of nodes
// set of prescribed styles
}
export default Controls;

@ -1,10 +1,9 @@
const container = document.querySelector('.particles');
const containerBounds = container.getBoundingClientRect();
// const DOM = (container) => {
// this.container = container;
// this.containerBounds = container.getBoundingClientRect();
// }
const DOM = {
container,
containerBounds,
getEventOffsetCoords: (evt, bounds) => {
const { pageX, pageY } = evt;
@ -14,18 +13,6 @@ const DOM = {
};
},
highlight: (evt) => {
const { evtX, evtY } = DOM.getEventOffsetCoords(evt, DOM.containerBounds);
const highlightDiv = document.createElement('div');
highlightDiv.className = 'highlight';
highlightDiv.style.left = `${evtX}px`;
highlightDiv.style.top = `${evtY}px`;
DOM.container.appendChild(highlightDiv);
setTimeout(() => { DOM.container.removeChild(highlightDiv); }, 500);
},
addClass: (node, str) => {
node.className = node.className.split(' ').concat(str).join(' ');
},
@ -34,11 +21,19 @@ const DOM = {
const arr = node.className.split(' ');
const i = arr.indexOf(str);
node.className = arr.slice(0, i).concat(arr.slice(i + 1)).join(' ');
},
// calcBounds: () => {
// DOM.containerBounds = container.getBoundingClientRect();
// },
}
};
// DOM.prototype.highlight = (evt) => {
// const { evtX, evtY } = DOM.getEventOffsetCoords(evt, DOM.containerBounds);
//
// const highlightDiv = document.createElement('div');
// highlightDiv.className = 'highlight';
// highlightDiv.style.left = `${evtX}px`;
// highlightDiv.style.top = `${evtY}px`;
//
// DOM.container.appendChild(highlightDiv);
// setTimeout(() => { DOM.container.removeChild(highlightDiv); }, 500);
// }
export default DOM;

@ -0,0 +1,9 @@
const RAD = {
t45: Math.PI / 4,
t90: Math.PI / 2,
t180: Math.PI,
t270: 3 * Math.PI / 2,
t360: Math.PI * 2
};
export { RAD };

@ -1,30 +1,16 @@
import Rx, { Observable } from 'rxjs';
import Controls from './controls';
import Animation1 from './animation1';
import Animation2 from './animation2';
import Animation3 from './animation3';
import Animation5 from './animation5';
require('../css/reset.scss');
require('../css/index.scss');
require('../css/animation2.scss');
require('../css/animation3.scss');
require('../css/animation5.scss');
require('../css/particle.scss');
Animation3.init();
(new Controls(document.getElementById('animation1'), Animation1)).mount();
// TODO ANIM 2 clicking several times on seahorse creates jumpiness
// TODO display file contents in page
// TODO adding core UI breaks bounds
//
// TODO ANIM 3 birds entering, land on palette, find next one if full
// TODO ANIM 4 dog chasing
//
// TODO PR: https://github.com/ReactiveX/rxjs/blob/master/doc/decision-tree-widget/tree.yml#L122 "...time past since the last..."
//
// INTERMEDIATE TOPICS
// === I have one existing Observable and
// I want to group the values based on another Observable for opening a group, and an Observable for closing a group...
// I want to start a new Observable for each value...
// I want to share a subscription between multiple subscribers...
// I want to change the scheduler...

@ -0,0 +1,171 @@
import Rx, { Observable } from 'rxjs';
// import DOM from './dom';
import { RAD } from './enums';
import Store from './store';
const random = {
bool: () => Math.random() < 0.5,
num: (min, max) => min + Math.round(Math.random() * max)
}
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 + Math.PI) % RAD.t360;
arc.x -= (2 * arc.r) * Math.cos(arc.t);
arc.y += (2 * arc.r) * Math.sin(arc.t);
return arc;
}
// function transformVisionGrid(store) {
// const {
// arc,
// clockwise,
// particle.x,
// particle.y,
// 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 = particle.x - particle.x % 5;
// const gridY = particle.y - particle.y % 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 Particle(container, bounds, options = {}) {
this.container = container;
this.bounds = bounds;
this.node = document.createElement('div');
this.node.className = 'particle has-vision';
this.circle = document.createElement('div');
this.circle.className = 'particle-movement-circle';
this.container.appendChild(this.node);
this.container.appendChild(this.circle);
this.arc = {
r: random.num(100, 200),
t: random.num(0, RAD.t360),
x: random.num(0, bounds.width),
y: random.num(0, bounds.height)
}
this.particle = {
clockwise: random.bool(),
speed: 4,
x: 0,
y: 0
}
this.interval = 0;
this.updateOptions(options);
this.nextFrame();
};
Particle.prototype.nextFrame = function() {
this.move();
this.repaintParticle();
this.repaintCircle();
}
Particle.prototype.updateOptions = function(options) {
this.particleImage = 'seahorse';
this.randomlyChangeRadius = (new Boolean(options.randomlyChangeRadius)) || true;
this.randomlyChangeRotation = (new Boolean(options.randomlyChangeRotation)) || true;
this.showCircle = (new Boolean(options.showCircle)) || false;
this.showVision = (new Boolean(options.showVision)) || false;
}
Particle.prototype.repaintParticle = function() {
const rad = this.particle.clockwise
? RAD.t180 - this.arc.t
: RAD.t360 - this.arc.t;
this.node.style.left = `${this.particle.x}px`;
this.node.style.top = `${this.particle.y}px`;
this.node.style.transform = `rotate(${rad}rad)`;
}
Particle.prototype.repaintCircle = function() {
this.circle.style.width = `${2 * this.arc.r}px`;
this.circle.style.height = `${2 * this.arc.r}px`;
this.circle.style.left = `${this.arc.x - this.arc.r}px`;
this.circle.style.top = `${this.arc.y - this.arc.r}px`;
this.circle.style.borderRadius = `${this.arc.r}px`;
}
Particle.prototype.move = function(store) {
// Randomly change radius and rotation direction.
this.interval -= 1;
if (this.interval <= 0) {
this.interval = random.num(50, 100);
this.arc = moveArc(this.arc, random.num(100, 200));
if (random.bool()) {
this.particle.clockwise = !this.particle.clockwise;
this.arc = changeDirection(this.arc);
}
}
// Ensure constant velocity and theta between 0 and 2π.
const delta = this.particle.speed / this.arc.r;
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
}
}
export default Particle;
Loading…
Cancel
Save