Controls in place.

master
Ben Burlingham 8 years ago
parent 5e74dc90af
commit 7bf8ec67a2
  1. 38
      css/controls.scss
  2. 31
      css/index.scss
  3. 60
      css/particle.scss
  4. 66
      css/style.css
  5. 11
      index.html
  6. 99
      js/animation1.js
  7. 12
      js/animation4.js
  8. 1969
      js/bundle.js
  9. 123
      js/controls.js
  10. 10
      js/index.js
  11. 112
      js/particle.js

@ -0,0 +1,38 @@
.controls-label {
cursor: pointer;
font-size: 13px;
margin-bottom: 5px;
padding: 10px;
&:hover {
background: #fff;
}
}
.controls-label-checkbox {
margin-right: 5px;
}
.controls-label-text {
}
.controls-slider {
cursor: pointer;
width: 100%;
}
.controls-button {
border: 0;
cursor: pointer;
font-size: 18px;
padding: 10px;
&:hover {
background: #fff;
}
}
.controls-bottom {
margin-top: auto;
}

@ -1,16 +1,33 @@
.particles {
background: rgba(102, 51, 153, 0.1);
// background: url('../res/seigaiha.svg');
// background-size: 100px 50px;
// border-radius: 50px;
// box-shadow: 2px 2px 0 #aaa;
.outerContainer {
height: 400px;
margin: 10px auto;
overflow: hidden;
position: relative;
width: 90%;
}
//
.controlsContainer {
background: #ccc;
border-left: 2px groove #c0c0c0;
bottom: 0;
display: flex;
flex-direction: column;
padding: 10px;
position: absolute;
right: 0;
top: 0;
width: 170px;
z-index: 1;
}
.animationContainer {
background: rgba(102, 51, 153, 0.1);
height: 100%;
margin-right: 170px;
position: relative;
z-index: 0;
}
// .palette {
// background: url('../res/palette.svg');
// background-size: 167px 100px;

@ -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;
// }
// }
.particle-vision-grid {
height: 100px;
// margin-top: 50px;
// margin-left: 50px;
position: absolute;
transform-origin: left top;
width: 100px;
z-index: 99;
}
.particle-vision-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;
}
}

@ -5,13 +5,32 @@
body {
font-family: sans-serif; }
.particles {
background: rgba(102, 51, 153, 0.1);
.outerContainer {
height: 400px;
margin: 10px auto;
overflow: hidden;
position: relative;
width: 90%; }
.controlsContainer {
background: #ccc;
border-left: 2px groove #c0c0c0;
bottom: 0;
display: flex;
flex-direction: column;
padding: 10px;
position: absolute;
right: 0;
top: 0;
width: 170px;
z-index: 1; }
.animationContainer {
background: rgba(102, 51, 153, 0.1);
height: 100%;
margin-right: 170px;
position: relative;
z-index: 0; }
.particle {
background: url(../res/seahorse.svg) no-repeat center center #aaa;
background-size: 20px 20px;
@ -54,3 +73,46 @@ body {
position: absolute;
top: 50%;
width: 4px; }
.particle-vision-grid {
height: 100px;
position: absolute;
transform-origin: left top;
width: 100px;
z-index: 99; }
.particle-vision-dot {
background: red;
height: 2px;
position: absolute;
width: 2px;
z-index: 99; }
.particle-vision-dot.removed {
background: green; }
.particle-vision-dot.touching {
outline: 2px solid yellow; }
.controls-label {
cursor: pointer;
font-size: 13px;
margin-bottom: 5px;
padding: 10px; }
.controls-label:hover {
background: #fff; }
.controls-label-checkbox {
margin-right: 5px; }
.controls-slider {
cursor: pointer;
width: 100%; }
.controls-button {
border: 0;
cursor: pointer;
font-size: 18px;
padding: 10px; }
.controls-button:hover {
background: #fff; }
.controls-bottom {
margin-top: auto; }

@ -7,7 +7,7 @@
</head>
<body>
<h1>Dust</h1>
<h2>AI Swarm Movement with RxJs</h2>
<h2>AI Swarms with RxJs</h2>
<hr>
<h3>Project Goal</h3>
@ -24,13 +24,16 @@
<hr>
<p>
The trickiest portion of this iteration was animating the curved paths. I explored elliptical geometry,
The trickiest portion of this iteration was animating the curved paths. Elliptical geometry was the initial goal,
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.
groups. The current design uses circular paths that allow smooth changes in both direction and rotation.
</p>
<div class='particles' id="animation1"></div>
<div class='outerContainer'>
<div id="animation1" class='animationContainer'></div>
<div id="controls1" class='controlsContainer'></div>
</div>
<!--

@ -1,58 +1,71 @@
import Rx, { Observable } from 'rxjs';
import Particle from './particle';
import Store from './store';
import Controls from './controls';
function Animation1(node) {
this.container = node;
this.bounds = node.getBoundingClientRect();
}
function Animation1() {
this.options = {
count: 1
};
// 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' };
// }
// }
// }
this.container = document.getElementById('animation1');
this.bounds = this.container.getBoundingClientRect();
this.controls = new Controls(document.getElementById('controls1'), this.options);
Animation1.prototype.nextFrame = function() {
this.particles.forEach(p => p.nextFrame());
}
const eventStack = this.controls.mount();
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));
eventStack.subscribe(this.subscriber);
const stop$ = Rx.Observable.fromEvent(this.container, 'stop');
this.particles = [];
// Change animation speed
// Change animal pic
// Enable random radius changes
// Enable random rotation changes
// Show movement circle
// Show vision grid (including touches!)
// Start / stop
this.updateCount();
console.error("Click container to stop.");
const fps$ = Rx.Observable.interval(1000 / 32)
.takeUntil(stop$)
.finally(() => { console.error("Stopped."); })
// this.particles = Array(5).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
// // TODO add core UI
//
// 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));
};
const click$ = Rx.Observable.fromEvent(this.container, 'click');
click$.subscribe(() => {
this.container.dispatchEvent(new CustomEvent('stop'));
});
Animation1.prototype.subscriber = function({ key, value }) {
switch(key) {
case 'count': this.updateCount(); break;
}
}
fps$.subscribe(this.nextFrame.bind(this));
Animation1.prototype.nextFrame = function() {
this.particles.forEach(p => p.nextFrame());
}
Animation1.prototype.updateCount = function() {
while (this.particles.length >= this.options.count) {
const p = this.particles.pop();
this.container.removeChild(p.node);
}
while (this.particles.length < this.options.count) {
const p = new Particle(this.container, this.bounds);
this.particles.push(p);
}
};
export default Animation1;

@ -6,6 +6,18 @@
// find if going to hit a wall
// find if palette nearby
// 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' };
// }
// }
// }
import Rx, { Observable } from 'rxjs';
import AnimationBase from './animationBase';
import DOM from './dom';

File diff suppressed because it is too large Load Diff

123
js/controls.js vendored

@ -1,23 +1,120 @@
function Controls(node, animation) {
import Rx, { Observable } from 'rxjs';
function Controls(node, { speed, otherstuff }) {
this.node = node;
this.animation = new animation(node);
}
Controls.prototype.mount = function(customNodes) {
const countCtrl = renderCountControl();
const speedCtrl = renderSpeedControl();
const radiusCtrl = renderRadiusControl();
const rotationCtrl = renderRotationControl();
const startStopCtrl = renderStartStopControl();
this.node.appendChild(countCtrl);
this.node.appendChild(speedCtrl);
this.node.appendChild(radiusCtrl);
this.node.appendChild(rotationCtrl);
this.node.appendChild(startStopCtrl);
return Rx.Observable.merge(
Rx.Observable.fromEvent(countCtrl, 'change')
.map(evt => ({ key: 'count', value: evt.target.value * 1 })),
Rx.Observable.fromEvent(speedCtrl, 'change')
.map(evt => ({ key: 'speed', value: evt.target.value * 1 })),
Rx.Observable.fromEvent(radiusCtrl, 'change')
.map(evt => ({ key: 'radius', value: evt.target.checked })),
Rx.Observable.fromEvent(rotationCtrl, 'change')
.map(evt => ({ key: 'rotation', value: evt.target.checked })),
Rx.Observable.fromEvent(startStopCtrl, 'click')
.map(evt => ({ key: 'startstop', value: null })),
);
}
const renderCountControl = function() {
const label = document.createElement('label');
label.className = 'controls-label';
const text = document.createElement('span');
text.innerHTML = 'Number of particles';
text.className = 'controls-label-text';
const slider = document.createElement('input');
slider.type = 'range';
slider.min = 1;
slider.max = 10;
slider.className = 'controls-slider';
label.appendChild(text);
label.appendChild(slider);
return label;
}
const renderSpeedControl = function() {
const label = document.createElement('label');
label.className = 'controls-label';
const text = document.createElement('span');
text.innerHTML = 'Animation speed';
text.className = 'controls-label-text';
if (this.animation.init === undefined) {
console.error("Animation passed to Control doesn't have an init() method.");
}
const slider = document.createElement('input');
slider.type = 'range';
slider.min = 1;
slider.max = 10;
slider.className = 'controls-slider';
if (this.animation.reset === undefined) {
console.error("Animation passed to Control doesn't have a reset() method.");
}
label.appendChild(text);
label.appendChild(slider);
this.animation.init();
return label;
}
Controls.prototype.mount = function() {
// this.node.style.border = '10px solid purple'; WORKING
const renderRadiusControl = function() {
const label = document.createElement('label');
label.className = 'controls-label';
const text = document.createElement('span');
text.innerHTML = 'Random radii';
text.className = 'controls-label-text';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'controls-label-checkbox';
label.appendChild(checkbox);
label.appendChild(text);
return label;
}
const renderRotationControl = function() {
const label = document.createElement('label');
label.className = 'controls-label';
const text = document.createElement('span');
text.innerHTML = 'Random rotation';
text.className = 'controls-label-text';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'controls-label-checkbox';
label.appendChild(checkbox);
label.appendChild(text);
return label;
}
const renderStartStopControl = function() {
const button = document.createElement('button');
button.className = 'controls-button controls-bottom';
button.innerHTML = '&#9654; Start';
// button.innerHTML = '&#9724; Stop';
// right aligned panel, pass in extra custom controls array of nodes
// set of prescribed styles
return button;
}
export default Controls;

@ -5,12 +5,6 @@ import Animation1 from './animation1';
require('../css/reset.scss');
require('../css/index.scss');
require('../css/particle.scss');
require('../css/controls.scss');
(new Controls(document.getElementById('animation1'), Animation1)).mount();
// 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..."
new Animation1();

@ -21,49 +21,63 @@ function moveArc(arc, newRadius) {
}
function changeDirection(arc) {
arc.t = (arc.t + Math.PI) % RAD.t360;
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 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 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 = RAD.t270;
} else if (y === 0 && x < 0) {
alpha = RAD.t180;
} else if (x === 0 && y > 0) {
alpha = RAD.t90;
} else if (x < 0 && y < 0) {
alpha = alpha + RAD.t180;
} else if (x <= 0) {
alpha = RAD.t180 + alpha;
} else if (y < 0) {
alpha = RAD.t360 + 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 = 'particle-vision-dot';
acc.push(Object.assign(point, { div }));
return acc;
}, []);
}
function Particle(container, bounds, options = {}) {
this.container = container;
this.bounds = bounds;
this.visionGridPoints = calculateVisionGridPoints();
this.node = document.createElement('div');
this.node.className = 'particle has-vision';
@ -74,6 +88,10 @@ function Particle(container, bounds, options = {}) {
this.container.appendChild(this.node);
this.container.appendChild(this.circle);
this.visionGridPoints.forEach(point => {
this.container.appendChild(point.div);
});
this.arc = {
r: random.num(100, 200),
t: random.num(0, RAD.t360),
@ -92,13 +110,13 @@ function Particle(container, bounds, options = {}) {
this.updateOptions(options);
this.nextFrame();
};
Particle.prototype.nextFrame = function() {
this.move();
this.repaintParticle();
this.repaintCircle();
this.repaintVisionGrid();
}
Particle.prototype.updateOptions = function(options) {
@ -128,7 +146,31 @@ Particle.prototype.repaintCircle = function() {
this.circle.style.borderRadius = `${this.arc.r}px`;
}
Particle.prototype.move = function(store) {
Particle.prototype.repaintVisionGrid = function() {
const r0 = Math.min(this.arc.t, this.arc.t - RAD.t180);
const r1 = Math.max(this.arc.t, this.arc.t + RAD.t180);
const gridX = this.particle.x - this.particle.x % 5;
const gridY = this.particle.y - this.particle.y % 5;
this.visionGridPoints.forEach(({ x, y, alpha, div }, i) => {
if (alpha >= 0 && alpha <= r0) {
div.style.display = (this.particle.clockwise ? 'none' : 'block');
// div.className = (clockwise ? 'anim3-dot removed' : 'anim3-dot');
} else if (alpha >= this.arc.t && alpha <= r1) {
div.style.display = (this.particle.clockwise ? 'none' : 'block');
// div.className = (clockwise ? 'anim3-dot removed' : 'anim3-dot');
} else {
div.style.display = (this.particle.clockwise ? 'block' : 'none');
// div.className = (clockwise ? 'anim3-dot' : 'anim3-dot removed');
}
div.style.left = `${x + gridX}px`;
div.style.top = `${-y + gridY}px`;
});
}
Particle.prototype.move = function() {
// Randomly change radius and rotation direction.
this.interval -= 1;
if (this.interval <= 0) {

Loading…
Cancel
Save