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 { .outerContainer {
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;
height: 400px; height: 400px;
margin: 10px auto; margin: 10px auto;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
width: 90%; 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 { // .palette {
// background: url('../res/palette.svg'); // background: url('../res/palette.svg');
// background-size: 167px 100px; // background-size: 167px 100px;

@ -48,33 +48,33 @@
width: 4px; width: 4px;
} }
} }
//
// .anim3-vision-grid { .particle-vision-grid {
// height: 100px; height: 100px;
// // margin-top: 50px; // margin-top: 50px;
// // margin-left: 50px; // margin-left: 50px;
// position: absolute; position: absolute;
// transform-origin: left top; transform-origin: left top;
// width: 100px; width: 100px;
// z-index: 99; z-index: 99;
// } }
//
// .anim3-dot { .particle-vision-dot {
// $s: 2px; $s: 2px;
//
// background: red; background: red;
// // border-radius: 2px; // border-radius: 2px;
// height: $s; height: $s;
// position: absolute; position: absolute;
// width: $s; width: $s;
// z-index: 99; z-index: 99;
//
// &.removed { &.removed {
// // background: purple; // background: purple;
// background: green; background: green;
// } }
//
// &.touching { &.touching {
// outline: 2px solid yellow; outline: 2px solid yellow;
// } }
// } }

@ -5,13 +5,32 @@
body { body {
font-family: sans-serif; } font-family: sans-serif; }
.particles { .outerContainer {
background: rgba(102, 51, 153, 0.1);
height: 400px; height: 400px;
margin: 10px auto; margin: 10px auto;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
width: 90%; } 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 { .particle {
background: url(../res/seahorse.svg) no-repeat center center #aaa; background: url(../res/seahorse.svg) no-repeat center center #aaa;
background-size: 20px 20px; background-size: 20px 20px;
@ -54,3 +73,46 @@ body {
position: absolute; position: absolute;
top: 50%; top: 50%;
width: 4px; } 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> </head>
<body> <body>
<h1>Dust</h1> <h1>Dust</h1>
<h2>AI Swarm Movement with RxJs</h2> <h2>AI Swarms with RxJs</h2>
<hr> <hr>
<h3>Project Goal</h3> <h3>Project Goal</h3>
@ -24,13 +24,16 @@
<hr> <hr>
<p> <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 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 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> </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 Rx, { Observable } from 'rxjs';
import Particle from './particle'; import Particle from './particle';
import Store from './store'; import Store from './store';
import Controls from './controls';
function Animation1(node) { function Animation1() {
this.container = node; this.options = {
this.bounds = node.getBoundingClientRect(); count: 1
} };
// const grid = {}; this.container = document.getElementById('animation1');
// for (let x = 0; x <= 600; x += 5) { this.bounds = this.container.getBoundingClientRect();
// grid[x] = {}; this.controls = new Controls(document.getElementById('controls1'), this.options);
// 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() { const eventStack = this.controls.mount();
this.particles.forEach(p => p.nextFrame());
}
Animation1.prototype.reset = () => { eventStack.subscribe(this.subscriber);
// 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'); this.particles = [];
// Change animation speed this.updateCount();
// 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."); // this.particles = Array(5).fill().map(_ => new Particle(this.container, this.bounds));
const fps$ = Rx.Observable.interval(1000 / 32) //
.takeUntil(stop$) // const stop$ = Rx.Observable.fromEvent(this.container, 'stop');
.finally(() => { console.error("Stopped."); }) //
// // 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'); Animation1.prototype.subscriber = function({ key, value }) {
click$.subscribe(() => { switch(key) {
this.container.dispatchEvent(new CustomEvent('stop')); 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; export default Animation1;

@ -6,6 +6,18 @@
// find if going to hit a wall // find if going to hit a wall
// find if palette nearby // 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 Rx, { Observable } from 'rxjs';
import AnimationBase from './animationBase'; import AnimationBase from './animationBase';
import DOM from './dom'; 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.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) { const slider = document.createElement('input');
console.error("Animation passed to Control doesn't have an init() method."); slider.type = 'range';
} slider.min = 1;
slider.max = 10;
slider.className = 'controls-slider';
if (this.animation.reset === undefined) { label.appendChild(text);
console.error("Animation passed to Control doesn't have a reset() method."); label.appendChild(slider);
}
this.animation.init(); return label;
} }
Controls.prototype.mount = function() { const renderRadiusControl = function() {
// this.node.style.border = '10px solid purple'; WORKING 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 return button;
// set of prescribed styles
} }
export default Controls; export default Controls;

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

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

Loading…
Cancel
Save