parent
e4bd773eb3
commit
7b13230220
16 changed files with 2024 additions and 1140 deletions
@ -0,0 +1,13 @@ |
||||
.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; |
||||
} |
@ -0,0 +1,47 @@ |
||||
.anim3-particle { |
||||
$side: 20px; |
||||
|
||||
background: url('../res/seahorse.svg') no-repeat center center #aaa; |
||||
background-size: $side $side; |
||||
border-radius: $side / 2; |
||||
color: #fff; |
||||
height: $side; |
||||
line-height: $side; |
||||
margin: #{-$side / 2} 0 0 #{-$side / 2}; |
||||
position: absolute; |
||||
text-align: center; |
||||
width: $side; |
||||
z-index: 1; |
||||
|
||||
&::after { |
||||
border: 50px solid transparent; |
||||
border-right-color: lightgreen; |
||||
content: ' '; |
||||
height: 0; |
||||
left: -30px; |
||||
opacity: 0.5; |
||||
position: absolute; |
||||
top: -40px; |
||||
width: 0; |
||||
} |
||||
} |
||||
|
||||
.anim3-movement-circle { |
||||
border: 2px dotted darkturquoise; |
||||
position: absolute; |
||||
transition: all 0.2s; |
||||
z-index: 0; |
||||
|
||||
&:after { |
||||
background: darkturquoise; |
||||
border-radius: 2px; |
||||
content:' '; |
||||
height: 4px; |
||||
left: 50%; |
||||
margin-left: -2px; |
||||
margin-top: -2px; |
||||
position: absolute; |
||||
top: 50%; |
||||
width: 4px; |
||||
} |
||||
} |
@ -0,0 +1,20 @@ |
||||
.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; |
||||
} |
||||
} |
@ -1,47 +1,159 @@ |
||||
// Single particle movement.
|
||||
// Goal: per-frame decisions
|
||||
// 20 x 20 grid
|
||||
|
||||
// The trickiest portion of this iteration was animating the curved paths. Calculating arc length (for scalar speed) along an elliptical
|
||||
// geometry is quite difficult, so the current solution uses circular paths, which change radius and sometimes rotation after a random
|
||||
// number of frames have emitted. A smoothstep cubic curve was considered, but maintaining consistent entry and exit angles will affect
|
||||
// performance for large particle counts.
|
||||
|
||||
import Rx, { Observable } from 'rxjs'; |
||||
import AnimationBase from './animation0'; |
||||
import DOM from './dom'; |
||||
import Store from './store'; |
||||
|
||||
const speed = 2; |
||||
const visionRadius = 50; |
||||
const grid = {}; |
||||
|
||||
const movementCircle = document.createElement('div'); |
||||
movementCircle.className = 'anim3-movement-circle'; |
||||
|
||||
const visionCircle = document.createElement('div'); |
||||
// visionCircle.className = 'anim3-vision-circle';
|
||||
|
||||
const particle = document.createElement('div'); |
||||
particle.className = 'anim3-particle'; |
||||
|
||||
function move(store) { |
||||
let { |
||||
clockwise, |
||||
interval, |
||||
particleX, |
||||
particleY, |
||||
radius, |
||||
theta |
||||
} = store.get(); |
||||
|
||||
|
||||
if (interval <= 0) { |
||||
interval = Math.round(Math.random() * 10) + 50; |
||||
radius = Math.round(Math.random() * 200) + 50 |
||||
|
||||
// IF SEEING A WALL: tight 180
|
||||
// if ( < visionRadius
|
||||
// if (Math.random() < 0.3) {
|
||||
// console.warn("turn!")
|
||||
// state.radius = 20;
|
||||
// state.interval = Math.PI * 20 / speed;
|
||||
// }
|
||||
|
||||
detectWall(store); |
||||
|
||||
if (Math.random() < 0.5) { |
||||
clockwise = !clockwise; |
||||
theta = (theta > Math.PI ? theta - Math.PI : theta + Math.PI); |
||||
} |
||||
} |
||||
|
||||
interval -= 1; |
||||
|
||||
const Animation2 = { |
||||
init: () => { |
||||
const container = document.querySelector('.particles'); |
||||
const { left: containerX, top: containerY } = container.getBoundingClientRect(); |
||||
const prevTheta = theta; |
||||
const delta = speed / radius; |
||||
theta += (clockwise ? -delta : delta); |
||||
|
||||
const state = { x: 0, y: 75 }; |
||||
particleX -= radius * Math.cos(theta) - radius * Math.cos(prevTheta); |
||||
particleY -= radius * Math.sin(theta) - radius * Math.sin(prevTheta); |
||||
|
||||
const particleDivs = Array.apply(null, { length: 1 }).map((v, i) => { |
||||
const div = document.createElement('div'); |
||||
div.className = 'particle'; |
||||
// div.style.top = `${i * 75}px`;
|
||||
// div.style.left = 0;
|
||||
div.style.top = `${state.y}px`; |
||||
div.style.left = `${state.x}px`; |
||||
store.set({ |
||||
clockwise, |
||||
interval, |
||||
particleX, |
||||
particleY, |
||||
radius, |
||||
theta |
||||
}); |
||||
} |
||||
|
||||
container.appendChild(div); |
||||
return div; |
||||
}); |
||||
function detectWall(store) { |
||||
const { particleX, particleY, radius, theta } = store.get(); |
||||
} |
||||
|
||||
const evtScare = new CustomEvent("scare"); |
||||
function paintMovementCircle(store) { |
||||
// TODO UPDATE ONLY IF THETA CHANGED - MUST HAVE PREVSTATE
|
||||
const { particleX, particleY, radius, theta } = store.get(); |
||||
|
||||
const strFps = Rx.Observable.interval(1000 / 32); |
||||
const strParticles = Rx.Observable.from(particleDivs); |
||||
const strScare = Rx.Observable.fromEvent(container, 'scare'); |
||||
movementCircle.style.left = `${particleX - radius - radius * Math.cos(theta)}px`; |
||||
movementCircle.style.top = `${particleY - radius + radius * Math.sin(theta)}px`; |
||||
movementCircle.style.height = `${2 * radius}px`; |
||||
movementCircle.style.width = `${2 * radius}px`; |
||||
movementCircle.style.borderRadius = `${radius}px`; |
||||
} |
||||
|
||||
strScare.subscribe(console.log) |
||||
function paintParticle(store) { |
||||
const { clockwise, particleX, particleY, theta } = store.get(); |
||||
const rad = (clockwise ? theta + Math.PI / 2 : theta - Math.PI / 2); |
||||
|
||||
const strClick = Rx.Observable |
||||
.fromEvent(container, 'click'); |
||||
particle.style.left = `${particleX}px`; |
||||
particle.style.top = `${particleY}px`; |
||||
particle.style.transform = `rotate(${rad}rad)`; |
||||
} |
||||
|
||||
strClick.subscribe(evt => { |
||||
const { pageX, pageY } = evt; |
||||
const offsetX = pageX - containerX; |
||||
const offsetY = pageY - containerY; |
||||
const diffX = Math.abs(state.x - offsetX); |
||||
const diffY = Math.abs(state.y - offsetY); |
||||
function reset() { |
||||
while (DOM.container.childNodes.length) { |
||||
DOM.container.removeChild(DOM.container.firstChild); |
||||
} |
||||
|
||||
if (diffX < 50 && diffY < 50) { |
||||
container.dispatchEvent(evtScare); |
||||
const store = new Store({ |
||||
clockwise: false, |
||||
interval: 10, |
||||
particleX: 300, |
||||
particleY: 150, |
||||
radius: 150, |
||||
theta: Math.PI / 2 |
||||
}); |
||||
|
||||
move(store); |
||||
|
||||
DOM.container.appendChild(particle); |
||||
DOM.container.appendChild(movementCircle); |
||||
|
||||
return store; |
||||
}; |
||||
|
||||
function init() { |
||||
const store = reset(); |
||||
|
||||
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' }; |
||||
} |
||||
}); |
||||
}, |
||||
} |
||||
} |
||||
|
||||
const a = Math.round(Math.random() * 180) + 20; |
||||
const b = Math.round(Math.random() * 180) + 20; |
||||
|
||||
const stop$ = Rx.Observable.fromEvent(DOM.container, 'stop'); |
||||
const fps$ = Rx.Observable.interval(1000 / 32) |
||||
.map(_ => store) |
||||
// .take(100)
|
||||
.takeUntil(stop$); console.error("CLICK TO STOP"); |
||||
|
||||
const click$ = Rx.Observable.fromEvent(DOM.container, 'click'); |
||||
click$.subscribe(() => { |
||||
DOM.container.dispatchEvent(new CustomEvent('stop')); |
||||
}); |
||||
|
||||
fps$.subscribe(move); |
||||
fps$.subscribe(paintParticle); |
||||
// fps$.subscribe(paintMovementCircle);
|
||||
}; |
||||
|
||||
export default Animation2; |
||||
const Animation3 = Object.assign({}, AnimationBase, { init, reset }); |
||||
|
||||
export default Animation3; |
||||
|
@ -0,0 +1,98 @@ |
||||
// Join mechanic, multiple particles
|
||||
// ????? Chase, maybe?
|
||||
// ???? Occupied?
|
||||
|
||||
// called each frame update
|
||||
// find if going to hit a wall
|
||||
// find if palette nearby
|
||||
|
||||
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 [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 * 25; |
||||
|
||||
div.style.left = 0 |
||||
div.style.top = `${y}px`; |
||||
|
||||
acc[0].push(div); |
||||
acc[1].push({ x, y }); |
||||
|
||||
return acc; |
||||
}, [[], []]); |
||||
|
||||
const palettes = (new Array(1)).fill(null).reduce((acc, v, i) => { |
||||
const div = document.createElement('div'); |
||||
div.className = 'palette'; |
||||
div.style.left = '200px'; |
||||
div.style.top = '200px'; |
||||
|
||||
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 < 100 && diffY < 100) { |
||||
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 reset() { |
||||
while (DOM.container.childNodes.length) { |
||||
DOM.container.removeChild(DOM.container.firstChild); |
||||
} |
||||
|
||||
particles.forEach((div) => { |
||||
div.innerHTML = ''; |
||||
DOM.container.appendChild(div) |
||||
}); |
||||
|
||||
console.warn(palettes) |
||||
|
||||
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); |
||||
|
||||
scare$.subscribe(evt => { |
||||
particles[evt.detail.i].innerHTML = 'S' |
||||
DOM.addClass(particles[evt.detail.i], 'scared'); |
||||
const p = particles[evt.detail.i]; |
||||
setTimeout(() => { |
||||
p.innerHTML = ''; |
||||
DOM.removeClass(p, 'scared'); |
||||
}, 1000); |
||||
}); |
||||
|
||||
click$.subscribe(scare); |
||||
}; |
||||
|
||||
const Animation3 = Object.assign({}, AnimationBase, { init, reset }); |
||||
|
||||
export default Animation3; |
@ -0,0 +1,117 @@ |
||||
// 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
@ -1,23 +1,30 @@ |
||||
import Rx, { Observable } from 'rxjs'; |
||||
import Animation1 from './animation1'; |
||||
import Animation2 from './animation2'; |
||||
import Animation3 from './animation3'; |
||||
import Animation5 from './animation5'; |
||||
|
||||
require('../css/index.scss'); |
||||
require('../css/reset.scss'); |
||||
require('../css/index.scss'); |
||||
require('../css/animation2.scss'); |
||||
require('../css/animation3.scss'); |
||||
require('../css/animation5.scss'); |
||||
|
||||
Animation2.init(); |
||||
Animation3.init(); |
||||
|
||||
|
||||
// TODO ANIM2 clicking several times on seahorse creates jumpiness
|
||||
// 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 wrap its messages with metadata...
|
||||
// I want to share a subscription between multiple subscribers...
|
||||
// I want to change the scheduler...
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
Loading…
Reference in new issue