Improve animations
This commit is contained in:
134
js/animation.js
Normal file
134
js/animation.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
export const createAnimationController = (animations, component) => {
|
||||||
|
return {
|
||||||
|
animations,
|
||||||
|
start: () => {
|
||||||
|
Object.entries(animations).map(([name, animation]) => animation.run((state) => {
|
||||||
|
component.setState({
|
||||||
|
...component.state,
|
||||||
|
[name]: state,
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
prepare: () => {
|
||||||
|
Object.entries(animations).map(([name, animation]) => animation.prepare((state) => {
|
||||||
|
component.setState({
|
||||||
|
...component.state,
|
||||||
|
[name]: state,
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createAnimation = (stateA, stateB, delay) => ({
|
||||||
|
stateA,
|
||||||
|
stateB,
|
||||||
|
delay,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAnimationState = (animation, stateName = 'stateA') => {
|
||||||
|
if (animation instanceof Array) {
|
||||||
|
return animation.map(a => getAnimationState(a, stateName));
|
||||||
|
}
|
||||||
|
|
||||||
|
return animation[stateName];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMaxTransitionTime = (transition) => {
|
||||||
|
const re = /(\d+)ms/g;
|
||||||
|
const times = [];
|
||||||
|
let matches;
|
||||||
|
while ((matches = re.exec(transition)) != null) {
|
||||||
|
times.push(parseInt(matches[1], 10));
|
||||||
|
}
|
||||||
|
return Math.max.apply(null, times);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAnimationDuration = (animation) => {
|
||||||
|
if (animation instanceof Array) {
|
||||||
|
return animation.reduce((maxDuration, a) => {
|
||||||
|
const duration = getAnimationDuration(a);
|
||||||
|
if (duration > maxDuration) {
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
return maxDuration;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = animation.stateB;
|
||||||
|
const maxTransition = state.transition ? getMaxTransitionTime(state.transition) : 0;
|
||||||
|
return maxTransition + animation.delay;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMaxAnimationsDuration = (animations) => (
|
||||||
|
getAnimationDuration(Object.values(animations))
|
||||||
|
);
|
||||||
|
|
||||||
|
export const createAnimationRunner = (animations, subscriber) => {
|
||||||
|
let state = Object.entries(animations).reduce((state, [name, animation]) => ({
|
||||||
|
...state,
|
||||||
|
[name]: getAnimationState(animation),
|
||||||
|
}), {});
|
||||||
|
|
||||||
|
const animationsDuration = getMaxAnimationsDuration(animations);
|
||||||
|
|
||||||
|
const animate = (name, animation, stateName, getDelay) => {
|
||||||
|
if (animation instanceof Array) {
|
||||||
|
animation.map((a, index) => {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
[name]: [
|
||||||
|
...(state[name].slice(0, index)),
|
||||||
|
a[stateName],
|
||||||
|
...(state[name].slice(index + 1, state[name].length)),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
subscriber();
|
||||||
|
}, getDelay(a))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
[name]: animation[stateName],
|
||||||
|
};
|
||||||
|
subscriber();
|
||||||
|
}, getDelay(animation))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getState() {
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
run() {
|
||||||
|
Object.entries(animations).forEach(([name, animation]) => {
|
||||||
|
animate(name, animation, 'stateB', a => a.delay)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
runReverse() {
|
||||||
|
Object.entries(animations).reverse().forEach(([name, animation]) => {
|
||||||
|
animate(name, animation, 'stateA', a => animationsDuration - a.delay)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
awaitAnimationComplete(callback) {
|
||||||
|
window.setTimeout(callback, animationsDuration);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare(callback) {
|
||||||
|
// callback(stateA);
|
||||||
|
// },
|
||||||
|
// run(callback) {
|
||||||
|
// window,requestAnimationFrame(() => {
|
||||||
|
// window.setTimeout(() => {
|
||||||
|
// callback(stateB);
|
||||||
|
// }, delay)
|
||||||
|
// });
|
||||||
|
// },
|
||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Link from './Link';
|
import Link from './Link';
|
||||||
|
|
||||||
export default function Item({ item, noLeadingBorder = false, active = false}) {
|
export default function Item({ item, noLeadingBorder = false, active = false, style = {}}) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className={classNames('item', {
|
className={classNames('item', {
|
||||||
@@ -10,6 +10,7 @@ export default function Item({ item, noLeadingBorder = false, active = false}) {
|
|||||||
'is-active': active,
|
'is-active': active,
|
||||||
})}
|
})}
|
||||||
pageName={`${item.quadrant}/${item.name}`}
|
pageName={`${item.quadrant}/${item.name}`}
|
||||||
|
style={style}
|
||||||
>
|
>
|
||||||
<div className="item__title">{item.title}</div>
|
<div className="item__title">{item.title}</div>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Item from './Item';
|
import Item from './Item';
|
||||||
|
|
||||||
export default function ItemList({ children, items, activeItem, noLeadingBorder }) {
|
export default function ItemList({ children, items, activeItem, noLeadingBorder, headerStyle = {}, itemStyle = [] }) {
|
||||||
return (
|
return (
|
||||||
<div className="item-list">
|
<div className="item-list">
|
||||||
<div className="item-list__header">
|
<div className="item-list__header" style={headerStyle}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<div className="item-list__list">
|
<div className="item-list__list">
|
||||||
{
|
{
|
||||||
items.map(item => (
|
items.map((item, i) => (
|
||||||
<Item
|
<Item
|
||||||
key={item.name}
|
key={item.name}
|
||||||
item={item}
|
item={item}
|
||||||
noLeadingBorder={noLeadingBorder}
|
noLeadingBorder={noLeadingBorder}
|
||||||
active={typeof activeItem === 'object' && activeItem !== null && activeItem.name === item.name}
|
active={typeof activeItem === 'object' && activeItem !== null && activeItem.name === item.name}
|
||||||
|
style={itemStyle[i]}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import React from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import actions from '../actions';
|
import actions from '../actions';
|
||||||
|
|
||||||
function Link({ pageName, children, navigate, className}) {
|
function Link({ pageName, children, navigate, className, style = {}}) {
|
||||||
const handleClick = (e) => {
|
const handleClick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate(pageName);
|
navigate(pageName);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={`/${pageName}.html`} onClick={handleClick} {...{ className }}>
|
<a href={`/${pageName}.html`} onClick={handleClick} style={style} {...{ className }}>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,23 +4,157 @@ import Badge from './Badge';
|
|||||||
import ItemList from './ItemList';
|
import ItemList from './ItemList';
|
||||||
import Link from './Link';
|
import Link from './Link';
|
||||||
import Fadeable from './Fadeable';
|
import Fadeable from './Fadeable';
|
||||||
|
import { createAnimation, createAnimationRunner } from '../animation';
|
||||||
|
|
||||||
import { groupByQuadrants } from '../../common/model';
|
import { groupByQuadrants } from '../../common/model';
|
||||||
|
|
||||||
export default function PageItems({ leaving, onLeave, pageName, items }) {
|
const items = [1, 2]; // TODO use real items
|
||||||
|
|
||||||
|
const animationsIn = {
|
||||||
|
background: createAnimation({
|
||||||
|
transform: 'translateX(calc((100vw - 1200px) / 2 + 800px))',
|
||||||
|
transition: 'transform 450ms cubic-bezier(0.24, 1.12, 0.71, 0.98)',
|
||||||
|
}, {
|
||||||
|
transition: 'transform 450ms cubic-bezier(0.24, 1.12, 0.71, 0.98)',
|
||||||
|
transform: 'translateX(0)',
|
||||||
|
},
|
||||||
|
0
|
||||||
|
),
|
||||||
|
navHeader: createAnimation({
|
||||||
|
transform: 'translateX(-40px)',
|
||||||
|
opacity: '0',
|
||||||
|
}, {
|
||||||
|
transition: 'opacity 150ms ease-out, transform 300ms ease-out',
|
||||||
|
transform: 'translateX(0px)',
|
||||||
|
opacity: '1',
|
||||||
|
},
|
||||||
|
300
|
||||||
|
),
|
||||||
|
text: createAnimation({
|
||||||
|
transform: 'translateY(-20px)',
|
||||||
|
opacity: '0',
|
||||||
|
}, {
|
||||||
|
transition: 'opacity 150ms ease-out, transform 300ms ease-out',
|
||||||
|
transform: 'translateY(0px)',
|
||||||
|
opacity: '1',
|
||||||
|
},
|
||||||
|
600
|
||||||
|
),
|
||||||
|
items: items.map((item, i) => (createAnimation({
|
||||||
|
transform: 'translateX(-40px)',
|
||||||
|
opacity: '0',
|
||||||
|
}, {
|
||||||
|
transition: 'opacity 150ms ease-out, transform 300ms ease-out',
|
||||||
|
transform: 'translateX(0px)',
|
||||||
|
opacity: '1',
|
||||||
|
},
|
||||||
|
400 + 100 * i
|
||||||
|
)))
|
||||||
|
};
|
||||||
|
|
||||||
|
const animationsOut = {
|
||||||
|
background: createAnimation(
|
||||||
|
animationsIn.background.stateB,
|
||||||
|
animationsIn.background.stateA,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
navHeader: createAnimation(
|
||||||
|
animationsIn.navHeader.stateB,
|
||||||
|
{
|
||||||
|
transition: 'opacity 150ms ease-out, transform 300ms ease-out',
|
||||||
|
transform: 'translateX(40px)',
|
||||||
|
opacity: '0',
|
||||||
|
},
|
||||||
|
0
|
||||||
|
),
|
||||||
|
text: createAnimation(
|
||||||
|
animationsIn.text.stateB,
|
||||||
|
{
|
||||||
|
transform: 'translateY(20px)',
|
||||||
|
transition: 'opacity 150ms ease-out, transform 300ms ease-out',
|
||||||
|
opacity: '0',
|
||||||
|
},
|
||||||
|
0
|
||||||
|
),
|
||||||
|
items: items.map((item, i) => (createAnimation(
|
||||||
|
animationsIn.items[i].stateB,
|
||||||
|
{
|
||||||
|
transition: 'opacity 150ms ease-out, transform 300ms ease-out',
|
||||||
|
transform: 'translateX(40px)',
|
||||||
|
opacity: '0',
|
||||||
|
},
|
||||||
|
100 + 100 * i
|
||||||
|
)))
|
||||||
|
};
|
||||||
|
|
||||||
|
const setAnimations = (state, animations) => ({
|
||||||
|
...state,
|
||||||
|
animations,
|
||||||
|
});
|
||||||
|
|
||||||
|
class PageItem extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
if (props.leaving) { // entering from an other page
|
||||||
|
this.state = setAnimations({}, createAnimationRunner(animationsIn).getState());
|
||||||
|
} else { // Hard refresh
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps({ leaving }) {
|
||||||
|
if (!this.props.leaving && leaving) { // page will be left
|
||||||
|
this.animationRunner = createAnimationRunner(animationsOut, this.handleAnimationsUpdate);
|
||||||
|
this.animationRunner.run();
|
||||||
|
this.animationRunner.awaitAnimationComplete(this.props.onLeave);
|
||||||
|
}
|
||||||
|
if (this.props.leaving && !leaving) { // page is entered
|
||||||
|
this.animationRunner = createAnimationRunner(animationsIn, this.handleAnimationsUpdate);
|
||||||
|
this.animationRunner.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTransitionEnd = () => {
|
||||||
|
if (this.state.faded) {
|
||||||
|
this.props.onLeave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleAnimationsUpdate = () => {
|
||||||
|
this.setState(setAnimations(this.state, this.animationRunner.getState()));
|
||||||
|
};
|
||||||
|
|
||||||
|
getAnimationState = (name) => {
|
||||||
|
if (!this.state.animations) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.state.animations[name];
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { leaving, onLeave, pageName, items } = this.props;
|
||||||
const [quadrantName, itemName] = pageName.split('/');
|
const [quadrantName, itemName] = pageName.split('/');
|
||||||
const item = items.filter(item => item.quadrant === quadrantName && item.name === itemName)[0];
|
const item = items.filter(item => item.quadrant === quadrantName && item.name === itemName)[0];
|
||||||
const itemsInRing = groupByQuadrants(items)[item.quadrant][item.ring];
|
const itemsInRing = groupByQuadrants(items)[item.quadrant][item.ring];
|
||||||
|
|
||||||
|
// console.log(this.getAnimationState('items'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fadeable leaving={leaving} onLeave={onLeave}>
|
<div>
|
||||||
<div className="item-page">
|
<div className="item-page">
|
||||||
<div className="item-page__nav">
|
<div className="item-page__nav">
|
||||||
<div className="item-page__nav__inner">
|
<div className="item-page__nav__inner">
|
||||||
<div className="item-page__header">
|
<div className="item-page__header" style={this.getAnimationState('navHeader')}>
|
||||||
<h3 className="headline">Languages & Frameworks</h3>
|
<h3 className="headline">Languages & Frameworks</h3>
|
||||||
</div>
|
</div>
|
||||||
<ItemList items={itemsInRing} activeItem={item}>
|
|
||||||
|
<ItemList
|
||||||
|
items={itemsInRing}
|
||||||
|
activeItem={item}
|
||||||
|
headerStyle={this.getAnimationState('navHeader')}
|
||||||
|
itemStyle={this.getAnimationState('items')}
|
||||||
|
>
|
||||||
<div className="split">
|
<div className="split">
|
||||||
<div className="split__left">
|
<div className="split__left">
|
||||||
<Badge big type={item.ring}>{item.ring}</Badge>
|
<Badge big type={item.ring}>{item.ring}</Badge>
|
||||||
@@ -34,8 +168,8 @@ export default function PageItems({ leaving, onLeave, pageName, items }) {
|
|||||||
</ItemList>
|
</ItemList>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="item-page__content">
|
<div className="item-page__content" style={this.getAnimationState('background')}>
|
||||||
<div className="item-page__content__inner">
|
<div className="item-page__content__inner" style={this.getAnimationState('text')}>
|
||||||
<div className="item-page__header">
|
<div className="item-page__header">
|
||||||
<div className="split">
|
<div className="split">
|
||||||
<div className="split__left">
|
<div className="split__left">
|
||||||
@@ -50,6 +184,9 @@ export default function PageItems({ leaving, onLeave, pageName, items }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fadeable>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default PageItem;
|
||||||
|
|||||||
Reference in New Issue
Block a user