Movement circle and vision grid moved into particle container.

master
Ben Burlingham 8 years ago
parent aae9a8fe90
commit 5edcdea48f
  1. 59
      css/particle.scss
  2. 28
      css/style.css
  3. 35
      index.html
  4. 49
      js/animation1a.js
  5. 75
      js/animation1b.js
  6. 89
      js/animation2.js
  7. 93
      js/animation2a.js
  8. 20737
      js/bundle.js
  9. 47
      js/controls.js
  10. 3
      js/enums.js
  11. 10
      js/index.js
  12. 196
      js/particle.js
  13. 2
      package.json

@ -1,64 +1,45 @@
.particle { $particleS: 20px;
$side: 20px; $visionS: 100px;
// background: url('../res/seahorse.svg') no-repeat center center #aaa; .particle-container {
background-color: #555;
background-size: $side $side;
border-radius: $side / 2;
color: #fff;
font-size: 11px;
height: $side;
line-height: $side;
margin: #{-$side / 2} 0 0 #{-$side / 2};
position: absolute; position: absolute;
text-align: center;
width: $side;
z-index: 1; z-index: 1;
}
// &.has-vision::after { .particle-body {
// border: 50px solid; background-color: #555;
// border-color: lightgreen transparent transparent turquoise; background-size: $particleS $particleS;
// border-radius: 50px; border-radius: 0 ($particleS / 2) ($particleS / 2) ($particleS / 2);
// content: ' '; height: $particleS;
// height: 0; margin: #{-$particleS / 2} 0 0 #{-$particleS / 2};
// left: -50px; position: relative;
// margin: #{$side / 2} 0 0 #{$side / 2}; width: $particleS;
// opacity: 0.5; z-index: 2;
// position: absolute;
// transform: rotate(45deg);
// top: -50px;
// width: 0;
// z-index: 0;
// }
} }
.particle-movement-circle { .particle-movement-circle {
border: 3px dotted darkturquoise; border: 3px dotted darkturquoise;
margin-left: 10px;
margin-top: 10px;
position: absolute; position: absolute;
z-index: 0; z-index: 0;
} }
.particle-vision-grid { .particle-vision {
height: 100px; // background: #ddd;
// margin-top: 50px; height: $visionS;
// margin-left: 50px; left: -$visionS / 2;
position: absolute; position: absolute;
transform-origin: left top; top: -$visionS / 2;
width: 100px; width: $visionS;
z-index: 99; z-index: 1;
} }
.particle-vision-dot { .particle-vision-dot {
$s: 2px; $s: 2px;
background: red; background: red;
// border-radius: 2px;
height: $s; height: $s;
position: absolute; position: absolute;
width: $s; width: $s;
z-index: 99;
&.removed { &.removed {
// background: purple; // background: purple;

@ -30,40 +30,38 @@ body {
position: relative; position: relative;
width: 100%; width: 100%;
z-index: 0; } z-index: 0; }
.particle { .particle-container {
position: absolute;
z-index: 1; }
.particle-body {
background-color: #555; background-color: #555;
background-size: 20px 20px; background-size: 20px 20px;
border-radius: 10px; border-radius: 0 10px 10px 10px;
color: #fff;
font-size: 11px;
height: 20px; height: 20px;
line-height: 20px;
margin: -10px 0 0 -10px; margin: -10px 0 0 -10px;
position: absolute; position: relative;
text-align: center;
width: 20px; width: 20px;
z-index: 1; } z-index: 2; }
.particle-movement-circle { .particle-movement-circle {
border: 3px dotted darkturquoise; border: 3px dotted darkturquoise;
margin-left: 10px;
margin-top: 10px;
position: absolute; position: absolute;
z-index: 0; } z-index: 0; }
.particle-vision-grid { .particle-vision {
height: 100px; height: 100px;
left: -50px;
position: absolute; position: absolute;
transform-origin: left top; top: -50px;
width: 100px; width: 100px;
z-index: 99; } z-index: 1; }
.particle-vision-dot { .particle-vision-dot {
background: red; background: red;
height: 2px; height: 2px;
position: absolute; position: absolute;
width: 2px; width: 2px; }
z-index: 99; }
.particle-vision-dot.removed { .particle-vision-dot.removed {
background: green; } background: green; }
.particle-vision-dot.touching { .particle-vision-dot.touching {

@ -35,8 +35,8 @@
</p> </p>
<div class='outerContainer'> <div class='outerContainer'>
<div id="animation1" class='animationContainer'></div> <div id="animation1a" class='animationContainer'></div>
<div id="controls1" class='controlsContainer'></div> <div id="controls1a" class='controlsContainer'></div>
</div> </div>
<p> <p>
@ -44,34 +44,23 @@
</p> </p>
<div class='outerContainer'> <div class='outerContainer'>
<div id="animation2" class='animationContainer'></div> <div id="animation1b" class='animationContainer'></div>
<div id="controls2" class='controlsContainer'></div> <div id="controls1b" class='controlsContainer'></div>
</div> </div>
<p> <p>
<h4>Exploration 2: Grid-based vision</h4> <h4>Exploration 2: Grid-based vision</h4>
</p> </p>
<p>
The next goal was to explore a grid-based vision system. Each particle owns a vision grid, which is compared
against the global grid to detect collisions. Here's a small scale system, with the vision grid visualized:
</p>
<!-- <div class='outerContainer'>
<div id="animation2a" class='animationContainer'></div>
<div id="controls2a" class='controlsContainer'></div>
// Another tricky part was the wall avoidance and vision grid. </div>
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 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 -->
<script src='js/bundle.js'></script> <script src='js/bundle.js'></script>
</body> </body>

@ -4,19 +4,16 @@ import Store from './store';
import Controls from './controls'; import Controls from './controls';
import { CONTROLS } from './enums'; import { CONTROLS } from './enums';
function Animation1() { function Animation1a() {
this.options = { this.options = {
animating: false,
count: 1, count: 1,
maxCount: 25, maxCount: 25,
randomizeRadius: true, randomize: true,
randomizeRotation: true, showMovementCircle: true,
showMovementCircle: false,
// showVisionGrid: false,
speed: 4 speed: 4
}; };
this.container = document.getElementById('animation1'); this.container = document.getElementById('animation1a');
this.bounds = this.container.getBoundingClientRect(); this.bounds = this.container.getBoundingClientRect();
this.movementCircleCtrl = createMovementCircleControl(); this.movementCircleCtrl = createMovementCircleControl();
@ -24,7 +21,7 @@ function Animation1() {
this.particles = []; this.particles = [];
const controls = new Controls( const controls = new Controls(
document.getElementById('controls1'), document.getElementById('controls1a'),
this.options, this.options,
[this.movementCircleCtrl] [this.movementCircleCtrl]
); );
@ -36,7 +33,6 @@ function Animation1() {
eventStack$.subscribe(this.subscriber.bind(this)); eventStack$.subscribe(this.subscriber.bind(this));
this.updateAnimating(this.options.animating);
this.updateCount(this.options.count); this.updateCount(this.options.count);
this.updateMovementCircle(this.options.showMovementCircle); this.updateMovementCircle(this.options.showMovementCircle);
}; };
@ -59,22 +55,21 @@ function createMovementCircleControl() {
return label; return label;
} }
Animation1.prototype.subscriber = function({ key, value }) { Animation1a.prototype.subscriber = function({ key, value }) {
switch(key) { switch(key) {
case CONTROLS.ANIMATING: this.updateAnimating(value); break; case CONTROLS.ANIMATING: this.updateAnimating(value); break;
case CONTROLS.COUNT: this.updateCount(value); break; case CONTROLS.COUNT: this.updateCount(value); break;
case CONTROLS.MOVEMENT_CIRCLE: this.updateMovementCircle(value); break; case CONTROLS.MOVEMENT_CIRCLE: this.updateMovementCircle(value); break;
case CONTROLS.RADIUS: this.updateRadius(value); break; case CONTROLS.RANDOMIZE: this.updateRandomize(value); break;
case CONTROLS.ROTATION: this.updateRotation(value); break;
case CONTROLS.SPEED: this.updateSpeed(value); break; case CONTROLS.SPEED: this.updateSpeed(value); break;
} }
} }
Animation1.prototype.nextFrame = function() { Animation1a.prototype.nextFrame = function() {
this.particles.forEach(p => p.nextFrame()); this.particles.forEach(p => p.nextFrame());
} }
Animation1.prototype.updateAnimating = function(isAnimating) { Animation1a.prototype.updateAnimating = function(isAnimating) {
this.options.animating = isAnimating; this.options.animating = isAnimating;
if (isAnimating) { if (isAnimating) {
@ -85,10 +80,9 @@ Animation1.prototype.updateAnimating = function(isAnimating) {
} }
} }
Animation1.prototype.updateCount = function(count) { Animation1a.prototype.updateCount = function(count) {
while (this.particles.length >= count) { while (this.particles.length >= count) {
const p = this.particles.pop(); delete this.particles.pop().remove();
this.container.removeChild(p.node);
} }
while (this.particles.length < count) { while (this.particles.length < count) {
@ -97,23 +91,20 @@ Animation1.prototype.updateCount = function(count) {
} }
} }
Animation1.prototype.updateMovementCircle = function(value) { Animation1a.prototype.updateMovementCircle = function(value) {
this.options.showMovementCircle = value;
this.movementCircleCtrl.querySelector('[type=checkbox]').checked = value; this.movementCircleCtrl.querySelector('[type=checkbox]').checked = value;
this.particles.forEach(p => p.updateOptions({ showMovementCircle: value })); this.particles.forEach(p => p.updateOptions({ showMovementCircle: value }));
} }
Animation1.prototype.updateRadius = function(value) { Animation1a.prototype.updateRandomize = function(value) {
this.randomRadiusCtrl.querySelector('[type=checkbox]').checked = value; this.options.randomize = value;
this.particles.forEach(p => p.updateOptions({ randomizeRadius: value })); this.particles.forEach(p => p.updateOptions({ randomize: value }));
} }
Animation1.prototype.updateRotation = function(value) { Animation1a.prototype.updateSpeed = function(value) {
this.randomRotationCtrl.querySelector('[type=checkbox]').checked = value; this.options.speed = value;
this.particles.forEach(p => p.updateOptions({ randomizeRotation: value })); this.particles.forEach(p => p.updateOptions({ speed: value }));
} }
Animation1.prototype.updateSpeed = function(speed) { export default Animation1a;
this.particles.forEach(p => p.updateOptions({ speed }));
}
export default Animation1;

@ -0,0 +1,75 @@
import Rx, { Observable } from 'rxjs';
import Particle from './particle';
import Store from './store';
import Controls from './controls';
import { CONTROLS } from './enums';
function Animation1b() {
this.options = {
count: 1,
maxCount: 2000,
randomize: false,
speed: 8
};
this.container = document.getElementById('animation1b');
this.bounds = this.container.getBoundingClientRect();
this.particles = [];
const controls = new Controls(
document.getElementById('controls1b'),
this.options
);
controls.mount().subscribe(this.subscriber.bind(this));
this.updateCount(this.options.count);
};
Animation1b.prototype.subscriber = function({ key, value }) {
switch(key) {
case CONTROLS.ANIMATING: this.updateAnimating(value); break;
case CONTROLS.COUNT: this.updateCount(value); break;
case CONTROLS.RANDOMIZE: this.updateRandomize(value); break;
case CONTROLS.SPEED: this.updateSpeed(value); break;
}
}
Animation1b.prototype.nextFrame = function() {
this.particles.forEach(p => p.nextFrame());
}
Animation1b.prototype.updateAnimating = function(isAnimating) {
this.options.animating = isAnimating;
if (isAnimating) {
const fps$ = Rx.Observable.interval(1000 / 32)
.takeWhile(_ => this.options.animating);
fps$.subscribe(this.nextFrame.bind(this));
}
}
Animation1b.prototype.updateCount = function(count) {
while (this.particles.length >= count) {
delete this.particles.pop().remove();
}
while (this.particles.length < count) {
const p = new Particle(this.container, this.bounds, this.options);
this.particles.push(p);
}
}
Animation1b.prototype.updateRandomize = function(value) {
this.options.randomize = value;
this.particles.forEach(p => p.updateOptions({ randomize: value }));
}
Animation1b.prototype.updateSpeed = function(value) {
this.options.speed = value;
this.particles.forEach(p => p.updateOptions({ speed: value }));
}
export default Animation1b;

@ -1,89 +0,0 @@
import Rx, { Observable } from 'rxjs';
import Particle from './particle';
import Store from './store';
import Controls from './controls';
import { CONTROLS } from './enums';
function Animation2() {
this.options = {
animating: false,
count: 1,
maxCount: 2000,
speed: 4
};
this.container = document.getElementById('animation2');
this.bounds = this.container.getBoundingClientRect();
this.particles = [];
const controls = new Controls(
document.getElementById('controls2'),
this.options
);
controls.mount().subscribe(this.subscriber.bind(this));
this.updateAnimating(this.options.animating);
this.updateCount(this.options.count);
// TODO X dimension modified by core UI
// TODO ANIM3 Show vision grid (including touches!)
// TODO ANIM3 regen vision grid is option updated
// TODO ANIM3 cleanup vision grid
// TODO ANIM3 Pallet avoidance
// TODO ANIM4 Flocking
};
Animation2.prototype.subscriber = function({ key, value }) {
switch(key) {
case CONTROLS.ANIMATING: this.updateAnimating(value); break;
case CONTROLS.COUNT: this.updateCount(value); break;
case CONTROLS.RADIUS: this.updateRadius(value); break;
case CONTROLS.ROTATION: this.updateRotation(value); break;
case CONTROLS.SPEED: this.updateSpeed(value); break;
}
}
Animation2.prototype.nextFrame = function() {
this.particles.forEach(p => p.nextFrame());
}
Animation2.prototype.updateAnimating = function(isAnimating) {
this.options.animating = isAnimating;
if (isAnimating) {
const fps$ = Rx.Observable.interval(1000 / 32)
.takeWhile(_ => this.options.animating);
fps$.subscribe(this.nextFrame.bind(this));
}
}
Animation2.prototype.updateCount = function(count) {
while (this.particles.length >= count) {
const p = this.particles.pop();
this.container.removeChild(p.node);
}
while (this.particles.length < count) {
const p = new Particle(this.container, this.bounds, this.options);
this.particles.push(p);
}
}
Animation2.prototype.updateRadius = function(value) {
this.particles.forEach(p => p.updateOptions({ randomizeRadius: value }));
}
Animation2.prototype.updateRotation = function(value) {
this.particles.forEach(p => p.updateOptions({ randomizeRotation: value }));
}
Animation2.prototype.updateSpeed = function(speed) {
this.particles.forEach(p => p.updateOptions({ speed }));
}
export default Animation2;

@ -0,0 +1,93 @@
import Rx, { Observable } from 'rxjs';
import Particle from './particle';
import Store from './store';
import Controls from './controls';
import { CONTROLS } from './enums';
function Animation2a() {
this.options = {
count: 1,
maxCount: 10,
randomize: true,
showVisionGrid: true,
showMovementCircle: false,
speed: 4
};
this.container = document.getElementById('animation2a');
this.bounds = this.container.getBoundingClientRect();
this.particles = [];
const controls = new Controls(
document.getElementById('controls2a'),
this.options
);
controls.mount().subscribe(this.subscriber.bind(this));
this.updateAnimating(this.options.animating);
this.updateCount(this.options.count);
// TODO X dimension modified by core UI
// TODO remove bottom padding from Disqus
// TODO perf - cache trig or perform operations
// TODO move animating into controls
// TODO particle evade
// TODO ANIM2 put pallettes in animation
// TODO ANIM2a Show vision grid (including touches!)
// TODO ANIM2b Scale vision grid to 1000? particles
// TODO ANIM4 Pallet avoidance
// TODO ANIM4 Lots of particles
// TODO ANIM5 Flocking
};
Animation2a.prototype.subscriber = function({ key, value }) {
switch(key) {
case CONTROLS.ANIMATING: this.updateAnimating(value); break;
case CONTROLS.COUNT: this.updateCount(value); break;
case CONTROLS.RANDOMIZE: this.updateRandomize(value); break;
case CONTROLS.SPEED: this.updateSpeed(value); break;
}
}
Animation2a.prototype.nextFrame = function() {
this.particles.forEach(p => p.nextFrame());
}
Animation2a.prototype.updateAnimating = function(isAnimating) {
this.options.animating = isAnimating;
if (isAnimating) {
const fps$ = Rx.Observable.interval(1000 / 32)
.takeWhile(_ => this.options.animating);
fps$.subscribe(this.nextFrame.bind(this));
}
}
Animation2a.prototype.updateCount = function(count) {
while (this.particles.length >= count) {
delete this.particles.pop().remove();
}
while (this.particles.length < count) {
const p = new Particle(this.container, this.bounds, this.options);
this.particles.push(p);
}
}
Animation2a.prototype.updateRandomize = function(value) {
this.options.randomize = value;
this.particles.forEach(p => p.updateOptions({ randomize: value }));
}
Animation2a.prototype.updateSpeed = function(value) {
this.options.speed = value;
this.particles.forEach(p => p.updateOptions({ speed: value }));
}
export default Animation2a;

File diff suppressed because one or more lines are too long

47
js/controls.js vendored

@ -1,19 +1,17 @@
import Rx, { Observable } from 'rxjs'; import Rx, { Observable } from 'rxjs';
import { CONTROLS } from './enums'; import { CONTROLS } from './enums';
function Controls(container, { animating, count, maxCount, randomizeRadius, randomizeRotation, speed }, customNodes) { function Controls(container, { animating, count, maxCount, randomize, speed }, customNodes) {
this.nodes = { this.nodes = {
animating: createAnimatingControl(animating), animating: createAnimatingControl(animating),
count: createCountControl(count, maxCount), count: createCountControl(count, maxCount),
radius: createRandomRadiusControl(randomizeRadius), randomize: createRandomizeControl(randomize),
rotation: createRandomRotationControl(randomizeRotation),
speed: createSpeedControl(speed), speed: createSpeedControl(speed),
} }
container.appendChild(this.nodes.count); container.appendChild(this.nodes.count);
container.appendChild(this.nodes.speed); container.appendChild(this.nodes.speed);
container.appendChild(this.nodes.radius); container.appendChild(this.nodes.randomize);
container.appendChild(this.nodes.rotation);
if (customNodes !== undefined) { if (customNodes !== undefined) {
customNodes.forEach(node => { container.appendChild(node); }); customNodes.forEach(node => { container.appendChild(node); });
@ -23,8 +21,7 @@ function Controls(container, { animating, count, maxCount, randomizeRadius, rand
this.updateOptions({ key: CONTROLS.ANIMATING, value: animating }); this.updateOptions({ key: CONTROLS.ANIMATING, value: animating });
this.updateOptions({ key: CONTROLS.COUNT, value: count }); this.updateOptions({ key: CONTROLS.COUNT, value: count });
this.updateOptions({ key: CONTROLS.RADIUS, value: randomizeRadius }); this.updateOptions({ key: CONTROLS.RANDOMIZE, value: randomize });
this.updateOptions({ key: CONTROLS.ROTATION, value: randomizeRotation });
this.updateOptions({ key: CONTROLS.SPEED, value: speed }); this.updateOptions({ key: CONTROLS.SPEED, value: speed });
} }
@ -52,11 +49,8 @@ Controls.prototype.mount = function(customNodes) {
const count$ = Rx.Observable.fromEvent(this.nodes.count, 'input') const count$ = Rx.Observable.fromEvent(this.nodes.count, 'input')
.map(evt => ({ key: CONTROLS.COUNT, value: evt.target.value * 1 })); .map(evt => ({ key: CONTROLS.COUNT, value: evt.target.value * 1 }));
const radius$ = Rx.Observable.fromEvent(this.nodes.radius, 'change') const randomize$ = Rx.Observable.fromEvent(this.nodes.randomize, 'change')
.map(evt => ({ key: CONTROLS.RADIUS, value: evt.target.checked })); .map(evt => ({ key: CONTROLS.RANDOMIZE, value: evt.target.checked }));
const rotation$ = Rx.Observable.fromEvent(this.nodes.rotation, 'change')
.map(evt => ({ key: CONTROLS.ROTATION, value: evt.target.checked }));
const speed$ = Rx.Observable.fromEvent(this.nodes.speed, 'input') const speed$ = Rx.Observable.fromEvent(this.nodes.speed, 'input')
.map(evt => ({ key: CONTROLS.SPEED, value: evt.target.value * 1 })); .map(evt => ({ key: CONTROLS.SPEED, value: evt.target.value * 1 }));
@ -64,8 +58,7 @@ Controls.prototype.mount = function(customNodes) {
const eventStack$ = Rx.Observable.merge( const eventStack$ = Rx.Observable.merge(
animating$, animating$,
count$, count$,
radius$, randomize$,
rotation$,
speed$ speed$
); );
@ -113,12 +106,12 @@ function createCountControl(value, max) {
return label; return label;
} }
function createRandomRadiusControl(value) { function createRandomizeControl(value) {
const label = document.createElement('label'); const label = document.createElement('label');
label.className = 'controls-checkbox'; label.className = 'controls-checkbox';
const text = document.createElement('span'); const text = document.createElement('span');
text.innerHTML = 'Random radii'; text.innerHTML = 'Randomize movement';
text.className = 'controls-checkbox-text'; text.className = 'controls-checkbox-text';
const checkbox = document.createElement('input'); const checkbox = document.createElement('input');
@ -132,27 +125,7 @@ function createRandomRadiusControl(value) {
return label; return label;
} }
function createRandomRotationControl(value) { function createSpeedControl(value) {
const label = document.createElement('label');
label.className = 'controls-checkbox';
const text = document.createElement('span');
text.innerHTML = 'Random rotation';
text.className = 'controls-checkbox-text';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'controls-checkbox-input';
checkbox.checked = value;
label.appendChild(checkbox);
label.appendChild(text);
return label;
}
const createSpeedControl = function(value) {
const label = document.createElement('label'); const label = document.createElement('label');
label.className = 'controls-range'; label.className = 'controls-range';

@ -10,8 +10,7 @@ const CONTROLS = {
ANIMATING: 'animating', ANIMATING: 'animating',
COUNT: 'count', COUNT: 'count',
MOVEMENT_CIRCLE: 'movementCircle', MOVEMENT_CIRCLE: 'movementCircle',
RADIUS: 'radius', RANDOMIZE: 'randomize',
ROTATION: 'rotation',
SPEED: 'speed' SPEED: 'speed'
}; };

@ -1,12 +1,14 @@
import Rx, { Observable } from 'rxjs'; import Rx, { Observable } from 'rxjs';
import Controls from './controls'; import Controls from './controls';
import Animation1 from './animation1'; import Animation1a from './animation1a';
import Animation2 from './animation2'; import Animation1b from './animation1b';
import Animation2a from './animation2a';
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'); require('../css/controls.scss');
new Animation1(); new Animation1a();
new Animation2(); new Animation1b();
new Animation2a();

@ -1,5 +1,4 @@
import Rx, { Observable } from 'rxjs'; import Rx, { Observable } from 'rxjs';
// import DOM from './dom';
import { RAD } from './enums'; import { RAD } from './enums';
import Store from './store'; import Store from './store';
@ -11,32 +10,14 @@ const random = {
/* ===== Constructor ===== */ /* ===== Constructor ===== */
function Particle(container, bounds, options) { function Particle(parent, bounds, options) {
const color = random.color();
this.container = container;
this.bounds = bounds;
// this.visionGridPoints = calculateVisionGridPoints();
this.node = document.createElement('div');
this.node.className = 'particle has-vision';
this.node.style.backgroundColor = color;
this.container.appendChild(this.node);
// this.visionGridPoints.forEach(point => {
// this.container.appendChild(point.div);
// });
this.options = Object.assign({ this.options = Object.assign({
randomizeRadius: false, randomize: false,
randomizeRotation: false,
showMovementCircle: false, showMovementCircle: false,
showVIsionGrid: false, showVisionGrid: false,
speed: 4 speed: 4
}, options); }, options);
this.arc = { this.arc = {
r: random.num(100, 200), r: random.num(100, 200),
t: random.num(RAD.t90, RAD.t360), t: random.num(RAD.t90, RAD.t360),
@ -46,26 +27,43 @@ function Particle(container, bounds, options) {
this.particle = { this.particle = {
clockwise: random.bool(), clockwise: random.bool(),
color, color: random.color(),
x: 0, x: 0,
y: 0 y: 0
} }
this.visionGridPoints = [];
this.interval = random.num(RAD.t90, RAD.t360); this.interval = random.num(RAD.t90, RAD.t360);
this.bounds = bounds;
this.parent = parent;
this.container = document.createElement('div');
this.container.className = 'particle-container';
this.body = document.createElement('div');
this.body.className = 'particle-body';
this.body.style.backgroundColor = this.particle.color;
this.container.appendChild(this.body);
this.parent.appendChild(this.container);
this.updateOptions(this.options); this.updateOptions(this.options);
this.nextFrame(); this.nextFrame();
}; };
Particle.prototype.remove = function() {
this.parent.removeChild(this.container);
return this;
}
Particle.prototype.nextFrame = function() { Particle.prototype.nextFrame = function() {
this.move(); this.move();
repaintParticle(this.arc, this.node, this.particle); repaintContainer(this.container, this.particle);
repaintBody(this.arc, this.body, this.particle);
repaintCircle(this.arc, this.circle, this.particle); repaintCircle(this.arc, this.circle, this.particle);
repaintVision(this.arc, this.vision, this.particle);
// if (this.options.showVisionGrid === true) { }
// repaintVisionGrid(this.arc, this.particle, this.visionGridPoints);
// }
};
Particle.prototype.updateBounds = function(bounds) { Particle.prototype.updateBounds = function(bounds) {
this.bounds = bounds; this.bounds = bounds;
@ -78,13 +76,35 @@ Particle.prototype.updateOptions = function(options) {
this.circle = document.createElement('div'); this.circle = document.createElement('div');
this.circle.className = 'particle-movement-circle'; this.circle.className = 'particle-movement-circle';
this.circle.style.borderColor = this.particle.color; this.circle.style.borderColor = this.particle.color;
this.node.appendChild(this.circle); this.container.appendChild(this.circle);
} }
if (options.showMovementCircle === false && this.circle !== undefined) { if (options.showMovementCircle === false && this.circle !== undefined) {
this.circle.parentNode.removeChild(this.circle); this.container.removeChild(this.circle);
delete this.circle; delete this.circle;
} }
if (options.showVisionGrid === true && this.vision === undefined) {
this.vision = document.createElement('div');
this.vision.className = 'particle-vision';
this.container.appendChild(this.vision);
this.visionGridPoints = calculateVisionGridPoints(this.particle);
this.visionGridPoints.forEach(point => {
this.vision.appendChild(point.div);
});
}
if (options.showVisionGrid === false && this.vision !== undefined) {
// this.container.removeChild(this.vision);
// delete this.vision;
//
// this.visionGridPoints.forEach(point => {
// this.vision.removeChild(point.div);
// });
// this.visionGridPoints = [];
}
} }
Particle.prototype.move = function() { Particle.prototype.move = function() {
@ -92,10 +112,10 @@ Particle.prototype.move = function() {
if (this.interval <= 0) { if (this.interval <= 0) {
this.interval = random.num(RAD.t90, RAD.t360); this.interval = random.num(RAD.t90, RAD.t360);
if (this.options.randomizeRadius === true) { if (this.options.randomize === true) {
this.arc = moveArc(this.arc, random.num(100, 200)); this.arc = moveArc(this.arc, random.num(100, 200));
if (random.bool(0.8) && this.options.randomizeRotation === true) { if (random.bool(0.8)) {
this.particle.clockwise = !this.particle.clockwise; this.particle.clockwise = !this.particle.clockwise;
this.arc = changeDirection(this.arc); this.arc = changeDirection(this.arc);
} }
@ -111,7 +131,6 @@ Particle.prototype.move = function() {
this.particle.x = this.arc.x + this.arc.r * Math.cos(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); this.particle.y = this.arc.y - this.arc.r * Math.sin(this.arc.t);
this.node.setAttribute('data-arc', JSON.stringify(this.arc))
// Overflow. // Overflow.
if (this.particle.x < 0) { if (this.particle.x < 0) {
@ -151,98 +170,77 @@ function changeDirection(arc) {
return arc; return arc;
} }
function calculateVisionGridPoints() { function calculateVisionGridPoints(particle) {
const gridSize = 5; const gridSize = 5;
const visionRadius = 50; const visionRadius = 50;
const squareGrid = []; const r0 = Math.pow(visionRadius, 2);
const r1 = Math.pow(visionRadius - gridSize, 2);
const points = [];
for (let x = -visionRadius; x <= visionRadius; x += gridSize) { for (let x = -visionRadius; x <= visionRadius; x += gridSize) {
for (let y = -visionRadius; y <= visionRadius; y += gridSize) { for (let y = -visionRadius; y <= visionRadius; y += gridSize) {
let alpha = Math.atan(y / x); // Half of triangle
if (x > -y) {
if (x === 0 && y === 0) { continue;
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 }); // Vision band
} const p = Math.pow(x, 2) + Math.pow(y, 2);
} if (p > r0 || p < r1) {
continue;
}
const r0 = Math.pow(visionRadius, 2); const div = document.createElement('div');
const r1 = Math.pow(visionRadius - gridSize, 2); div.className = 'particle-vision-dot';
div.style.backgroundColor = particle.color;
div.style.left = `${x + visionRadius}px`;
div.style.top = `${y + visionRadius}px`;
return squareGrid.reduce((acc, point) => { points.push({ x, y, div });
const p = Math.pow(point.x, 2) + Math.pow(point.y, 2);
if (p > r0 || p < r1) {
return acc;
} }
}
const div = document.createElement('div'); return points;
div.className = 'particle-vision-dot'; }
acc.push(Object.assign(point, { div })); function repaintContainer(containerNode, particle) {
return acc; containerNode.style.left = `${particle.x}px`;
}, []); containerNode.style.top = `${particle.y}px`;
} }
function repaintParticle(arc, node, particle) { function repaintBody(arc, bodyNode, particle) {
const rad = particle.clockwise const rad = particle.clockwise
? RAD.t180 - arc.t ? RAD.t180 - arc.t
: RAD.t360 - arc.t; : RAD.t360 - arc.t;
node.style.left = `${particle.x}px`; bodyNode.style.transform = `rotate(${rad + RAD.t45}rad)`;
node.style.top = `${particle.y}px`;
} }
function repaintCircle(arc, node, particle) { function repaintVision(arc, visionNode, particle) {
if (node === undefined) { if (visionNode === undefined) {
return; return;
} }
node.style.width = `${2 * arc.r}px`; const rad = particle.clockwise
node.style.height = `${2 * arc.r}px`; ? RAD.t180 - arc.t
: RAD.t360 - arc.t;
node.style.left = `${-particle.x - arc.r + arc.x}px`;
node.style.top = `${-particle.y - arc.r + arc.y}px`;
node.style.borderRadius = `${arc.r}px`; visionNode.style.transform = `rotate(${rad + RAD.t45}rad)`;
} }
function repaintVisionGrid(arc, particle, points) { function repaintCircle(arc, circleNode) {
const r0 = Math.min(arc.t, arc.t - RAD.t180); if (circleNode === undefined) {
const r1 = Math.max(arc.t, arc.t + RAD.t180); return;
}
const gridX = particle.x - particle.x % 5;
const gridY = particle.y - particle.y % 5;
points.forEach(({ x, y, alpha, div }, i) => {
if (alpha >= 0 && alpha <= r0) {
div.style.display = (particle.clockwise ? 'none' : 'block');
// div.className = (clockwise ? 'anim3-dot removed' : 'anim3-dot');
} else if (alpha >= arc.t && alpha <= r1) {
div.style.display = (particle.clockwise ? 'none' : 'block');
// div.className = (clockwise ? 'anim3-dot removed' : 'anim3-dot');
} else {
div.style.display = (particle.clockwise ? 'block' : 'none');
// div.className = (clockwise ? 'anim3-dot' : 'anim3-dot removed');
}
div.style.left = `${x + gridX}px`; circleNode.style.width = `${2 * arc.r}px`;
div.style.top = `${-y + gridY}px`; circleNode.style.height = `${2 * arc.r}px`;
});
}
circleNode.style.left = `-${arc.r + arc.r * Math.cos(arc.t)}px`;
circleNode.style.top = `-${arc.r - arc.r * Math.sin(arc.t)}px`;
circleNode.style.borderRadius = `${arc.r}px`;
}
export default Particle; export default Particle;

@ -4,7 +4,7 @@
"description": "Particle management using streams.", "description": "Particle management using streams.",
"main": "webpack.config.js", "main": "webpack.config.js",
"scripts": { "scripts": {
"dev": "webpack --watch" "dev": "webpack --watch -d"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

Loading…
Cancel
Save