From 628f1a3e24fca56deb554b37834906811524d7c8 Mon Sep 17 00:00:00 2001 From: Ben Burlingham Date: Sat, 8 Apr 2017 21:56:55 -0700 Subject: [PATCH] Animation 2 complete. --- css/index.scss | 43 +++++++++++++++---- css/style.css | 34 +++++++++++---- index.html | 18 ++++++++ js/animation2.js | 78 ++++++++++++++++++++++++++++------ js/bundle.js | 107 +++++++++++++++++++++++++++++++++++++++++------ js/dom.js | 21 ++++++++-- js/index.js | 8 ++-- res/seigaiha.svg | 27 ++++++++++++ 8 files changed, 291 insertions(+), 45 deletions(-) create mode 100644 res/seigaiha.svg diff --git a/css/index.scss b/css/index.scss index 8f165ce..3d97d1a 100644 --- a/css/index.scss +++ b/css/index.scss @@ -1,19 +1,48 @@ .particles { background: #fafafa; - border: 5px solid #fafafa; - border-radius: 3px; - height: 400px; + // background: url('../res/seigaiha.svg'); + background-size: 100px 50px; + border-radius: 50px; + height: 600px; margin: 10px auto; position: relative; - width: 90%; + width: 600px; } .particle { - $side: 20px; + $side: 100px; - background: url('../res/seahorse.svg'); - background-size: $side $side; + 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; } + +.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; + } +} diff --git a/css/style.css b/css/style.css index dba6fc6..889bbcb 100644 --- a/css/style.css +++ b/css/style.css @@ -1,18 +1,38 @@ .particles { background: #fafafa; - border: 5px solid #fafafa; - border-radius: 3px; - height: 400px; + background-size: 100px 50px; + border-radius: 50px; + height: 600px; margin: 10px auto; position: relative; - width: 90%; } + width: 600px; } .particle { - background: url(../res/seahorse.svg); + background: url(../res/seahorse.svg) no-repeat center center; background-size: 20px 20px; - height: 20px; + border-color: salmon; + border-style: dashed; + border-radius: 50px; + border-width: 1px; + height: 100px; position: absolute; - width: 20px; } + width: 100px; } + +.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; } } * { box-sizing: border-box; margin: 0; diff --git a/index.html b/index.html index 777f270..1879ef1 100644 --- a/index.html +++ b/index.html @@ -3,12 +3,30 @@ +

Dust

+

Swarm behavior experiments with RxJs

+
+

Animation 1

+ Goal: Animate several seahorses over an interval. + Key points: + - range() used to create N divs + - interval() used to create FPS + +

Animation 2

+ Goal: Scare a seahorse with a click. Move her to a safe distance away. + Key points: + - 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 + diff --git a/js/animation2.js b/js/animation2.js index 6a749ce..46505e0 100644 --- a/js/animation2.js +++ b/js/animation2.js @@ -15,38 +15,90 @@ function checkScare([evt, store]) { const diffX = Math.abs(state.x - evtX); const diffY = Math.abs(state.y - evtY); - if (diffX < 50 && diffY < 50) { + if (evt.target === particleDiv) { DOM.container.dispatchEvent(evtScare(evtX, evtY)); } }; +function move(acc, i) { + let { x, y, dx, dy } = acc; + + const east = DOM.containerBounds.width - particleDiv.offsetWidth; + const south = DOM.containerBounds.height - particleDiv.offsetHeight; + + x += dx; + y += dy; + + if (x < 0) { + x = Math.abs(x); + dx = -dx; + } + + if (x > east) { + x = Math.round(2 * east - x); + dx = -dx; + } + + if (y < 0) { + y = Math.abs(y); + dy = -dy; + } + + if (y > south) { + y = Math.round(2 * south - y); + dy = -dy; + } + + return { x, y, dx, dy }; +}; + function flee([evt, store]) { const initialState = store.get(); + const fleeRadius = 200; const { scareX, scareY } = evt.detail; const fps$ = Rx.Observable.interval(1000 / 32); - fps$ - .scan((acc, i) => { - return store.set({ x: acc.x + acc.dx, y: acc.y + acc.dy }) - }, initialState) + const frames$ = fps$ + .scan(move, initialState) .takeWhile(state => { - const xDanger = Math.abs(initialState.x - state.x) < 150; - const yDanger = Math.abs(initialState.y - state.y) < 150; + const xDanger = Math.abs(initialState.x - state.x) < fleeRadius; + const yDanger = Math.abs(initialState.y - state.y) < fleeRadius; return xDanger && yDanger; }) - .subscribe(state => { - particleDiv.style.left = `${state.x}px`; - particleDiv.style.top = `${state.y}px`; - }) + + frames$.last().subscribe(finalState => { + store.set(finalState); + store.set(randomMoveVector()); + }); + + frames$.subscribe(state => { + particleDiv.style.left = `${state.x}px`; + particleDiv.style.top = `${state.y}px`; + }); }; +function randomMoveVector() { + const speed = 5; + let dx = Math.round(Math.random() * speed); + let dy = Math.pow(Math.pow(speed, 2) - Math.pow(dx, 2), 0.5); + + const negX = Math.random() < 0.5 ? -1 : 1; + const negY = Math.random() < 0.5 ? -1 : 1; + + dx *= negX; + dy *= negY; + + return { dx, dy }; +} + function reset() { if (particleDiv.parentNode) { DOM.container.removeChild(particleDiv); } - const store = new Store({ x: 10, y: 10, dx: 5, dy: 5 }); + const { dx, dy } = randomMoveVector(); + const store = new Store({ x: 0, y: 0, dx, dy }); const state = store.get(); particleDiv.style.top = `${state.y}px`; @@ -62,6 +114,8 @@ function init() { const click$ = Rx.Observable .fromEvent(DOM.container, 'click') + // .do(DOM.calcBounds) + .do(DOM.highlight) .map(evt => [evt, store]) .subscribe(checkScare); diff --git a/js/bundle.js b/js/bundle.js index f44625e..434fb0c 100644 --- a/js/bundle.js +++ b/js/bundle.js @@ -6113,42 +6113,102 @@ function checkScare(_ref) { var diffX = Math.abs(state.x - evtX); var diffY = Math.abs(state.y - evtY); - if (diffX < 50 && diffY < 50) { + if (evt.target === particleDiv) { _dom2.default.container.dispatchEvent(evtScare(evtX, evtY)); } }; +function move(acc, i) { + var x = acc.x, + y = acc.y, + dx = acc.dx, + dy = acc.dy; + + + var east = _dom2.default.containerBounds.width - particleDiv.offsetWidth; + var south = _dom2.default.containerBounds.height - particleDiv.offsetHeight; + + x += dx; + y += dy; + + if (x < 0) { + x = Math.abs(x); + dx = -dx; + } + + if (x > east) { + x = Math.round(2 * east - x); + dx = -dx; + } + + if (y < 0) { + y = Math.abs(y); + dy = -dy; + } + + if (y > south) { + y = Math.round(2 * south - y); + dy = -dy; + } + + return { x: x, y: y, dx: dx, dy: dy }; +}; + function flee(_ref3) { var _ref4 = _slicedToArray(_ref3, 2), evt = _ref4[0], store = _ref4[1]; var initialState = store.get(); + var fleeRadius = 200; var _evt$detail = evt.detail, scareX = _evt$detail.scareX, scareY = _evt$detail.scareY; var fps$ = _rxjs2.default.Observable.interval(1000 / 32); - fps$.scan(function (acc, i) { - return store.set({ x: acc.x + acc.dx, y: acc.y + acc.dy }); - }, initialState).takeWhile(function (state) { - var xDanger = Math.abs(initialState.x - state.x) < 150; - var yDanger = Math.abs(initialState.y - state.y) < 150; + var frames$ = fps$.scan(move, initialState).takeWhile(function (state) { + var xDanger = Math.abs(initialState.x - state.x) < fleeRadius; + var yDanger = Math.abs(initialState.y - state.y) < fleeRadius; return xDanger && yDanger; - }).subscribe(function (state) { + }); + + frames$.last().subscribe(function (finalState) { + store.set(finalState); + store.set(randomMoveVector()); + }); + + frames$.subscribe(function (state) { particleDiv.style.left = state.x + 'px'; particleDiv.style.top = state.y + 'px'; }); }; +function randomMoveVector() { + var speed = 5; + var dx = Math.round(Math.random() * speed); + var dy = Math.pow(Math.pow(speed, 2) - Math.pow(dx, 2), 0.5); + + var negX = Math.random() < 0.5 ? -1 : 1; + var negY = Math.random() < 0.5 ? -1 : 1; + + dx *= negX; + dy *= negY; + + return { dx: dx, dy: dy }; +} + function reset() { if (particleDiv.parentNode) { _dom2.default.container.removeChild(particleDiv); } - var store = new _store2.default({ x: 10, y: 10, dx: 5, dy: 5 }); + var _randomMoveVector = randomMoveVector(), + dx = _randomMoveVector.dx, + dy = _randomMoveVector.dy; + + var store = new _store2.default({ x: 0, y: 0, dx: dx, dy: dy }); var state = store.get(); particleDiv.style.top = state.y + 'px'; @@ -6162,7 +6222,9 @@ function reset() { function init() { var store = reset(); - var click$ = _rxjs2.default.Observable.fromEvent(_dom2.default.container, 'click').map(function (evt) { + var click$ = _rxjs2.default.Observable.fromEvent(_dom2.default.container, 'click') + // .do(DOM.calcBounds) + .do(_dom2.default.highlight).map(function (evt) { return [evt, store]; }).subscribe(checkScare); @@ -6213,8 +6275,11 @@ __webpack_require__(72); _animation4.default.init(); -// TODO -// PR: https://github.com/ReactiveX/rxjs/blob/master/doc/decision-tree-widget/tree.yml#L122 "...time past since the last..." +// TODO ANIM2 clicking several times on seahorse creates jumpiness +// TODO display file contents in page +// TODO adding core UI breaks bounds +// +// 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 @@ -20110,11 +20175,10 @@ Object.defineProperty(exports, "__esModule", { value: true }); var container = document.querySelector('.particles'); -var containerBounds = document.querySelector('.particles').getBoundingClientRect(); +var containerBounds = container.getBoundingClientRect(); var DOM = { container: container, - containerBounds: containerBounds, getEventOffsetCoords: function getEventOffsetCoords(evt, containerCoords) { @@ -20126,7 +20190,24 @@ var DOM = { evtX: pageX - containerCoords.left, evtY: pageY - containerCoords.top }; + }, + + highlight: function highlight(evt) { + var _DOM$getEventOffsetCo = DOM.getEventOffsetCoords(evt, DOM.containerBounds), + evtX = _DOM$getEventOffsetCo.evtX, + evtY = _DOM$getEventOffsetCo.evtY; + + var highlightDiv = document.createElement('div'); + highlightDiv.className = 'highlight'; + highlightDiv.style.left = evtX + 'px'; + highlightDiv.style.top = evtY + 'px'; + + DOM.container.appendChild(highlightDiv); + setTimeout(function () { + DOM.container.removeChild(highlightDiv); + }, 500); } + }; exports.default = DOM; diff --git a/js/dom.js b/js/dom.js index cc34b9b..752a825 100644 --- a/js/dom.js +++ b/js/dom.js @@ -1,9 +1,8 @@ const container = document.querySelector('.particles'); -const containerBounds = document.querySelector('.particles').getBoundingClientRect(); +const containerBounds = container.getBoundingClientRect(); const DOM = { container, - containerBounds, getEventOffsetCoords: (evt, containerCoords) => { @@ -13,7 +12,23 @@ const DOM = { evtX: (pageX - containerCoords.left), evtY: (pageY - containerCoords.top) }; - } + }, + + 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); + }, + + // calcBounds: () => { + // DOM.containerBounds = container.getBoundingClientRect(); + // }, }; export default DOM; diff --git a/js/index.js b/js/index.js index 8fde8fb..28ab183 100644 --- a/js/index.js +++ b/js/index.js @@ -8,9 +8,11 @@ require('../css/reset.scss'); Animation2.init(); - -// TODO -// PR: https://github.com/ReactiveX/rxjs/blob/master/doc/decision-tree-widget/tree.yml#L122 "...time past since the last..." +// TODO ANIM2 clicking several times on seahorse creates jumpiness +// TODO display file contents in page +// TODO adding core UI breaks bounds +// +// 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 diff --git a/res/seigaiha.svg b/res/seigaiha.svg new file mode 100644 index 0000000..5a15ebd --- /dev/null +++ b/res/seigaiha.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file