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 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 = { |
const prevTheta = theta; |
||||||
init: () => { |
const delta = speed / radius; |
||||||
const container = document.querySelector('.particles'); |
theta += (clockwise ? -delta : delta); |
||||||
const { left: containerX, top: containerY } = container.getBoundingClientRect(); |
|
||||||
|
|
||||||
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) => { |
store.set({ |
||||||
const div = document.createElement('div'); |
clockwise, |
||||||
div.className = 'particle'; |
interval, |
||||||
// div.style.top = `${i * 75}px`;
|
particleX, |
||||||
// div.style.left = 0;
|
particleY, |
||||||
div.style.top = `${state.y}px`; |
radius, |
||||||
div.style.left = `${state.x}px`; |
theta |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
container.appendChild(div); |
function detectWall(store) { |
||||||
return div; |
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); |
movementCircle.style.left = `${particleX - radius - radius * Math.cos(theta)}px`; |
||||||
const strParticles = Rx.Observable.from(particleDivs); |
movementCircle.style.top = `${particleY - radius + radius * Math.sin(theta)}px`; |
||||||
const strScare = Rx.Observable.fromEvent(container, 'scare'); |
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 |
particle.style.left = `${particleX}px`; |
||||||
.fromEvent(container, 'click'); |
particle.style.top = `${particleY}px`; |
||||||
|
particle.style.transform = `rotate(${rad}rad)`; |
||||||
|
} |
||||||
|
|
||||||
strClick.subscribe(evt => { |
function reset() { |
||||||
const { pageX, pageY } = evt; |
while (DOM.container.childNodes.length) { |
||||||
const offsetX = pageX - containerX; |
DOM.container.removeChild(DOM.container.firstChild); |
||||||
const offsetY = pageY - containerY; |
} |
||||||
const diffX = Math.abs(state.x - offsetX); |
|
||||||
const diffY = Math.abs(state.y - offsetY); |
|
||||||
|
|
||||||
if (diffX < 50 && diffY < 50) { |
const store = new Store({ |
||||||
container.dispatchEvent(evtScare); |
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 Rx, { Observable } from 'rxjs'; |
||||||
import Animation1 from './animation1'; |
import Animation1 from './animation1'; |
||||||
import Animation2 from './animation2'; |
import Animation2 from './animation2'; |
||||||
|
import Animation3 from './animation3'; |
||||||
|
import Animation5 from './animation5'; |
||||||
|
|
||||||
require('../css/index.scss'); |
|
||||||
require('../css/reset.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 display file contents in page
|
||||||
// TODO adding core UI breaks bounds
|
// 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..."
|
// TODO PR: https://github.com/ReactiveX/rxjs/blob/master/doc/decision-tree-widget/tree.yml#L122 "...time past since the last..."
|
||||||
//
|
//
|
||||||
// INTERMEDIATE TOPICS
|
// INTERMEDIATE TOPICS
|
||||||
// === I have one existing Observable and
|
// === 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 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 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 share a subscription between multiple subscribers...
|
||||||
// I want to change the scheduler...
|
// I want to change the scheduler...
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
Loading…
Reference in new issue