From 6d4f1d0c09b6a58ff30020264e24e3e2c0219dc2 Mon Sep 17 00:00:00 2001 From: Tom Raithel Date: Wed, 1 Mar 2017 20:34:08 +0100 Subject: [PATCH] Improve animations --- js/animation.js | 134 ++++++++++++++++++++++++ js/components/Item.js | 3 +- js/components/ItemList.js | 7 +- js/components/Link.js | 4 +- js/components/PageItem.js | 209 +++++++++++++++++++++++++++++++------- 5 files changed, 315 insertions(+), 42 deletions(-) create mode 100644 js/animation.js diff --git a/js/animation.js b/js/animation.js new file mode 100644 index 0000000..3af22c9 --- /dev/null +++ b/js/animation.js @@ -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) +// }); +// }, diff --git a/js/components/Item.js b/js/components/Item.js index 262cf54..f88f720 100644 --- a/js/components/Item.js +++ b/js/components/Item.js @@ -2,7 +2,7 @@ import React from 'react'; import classNames from 'classnames'; import Link from './Link'; -export default function Item({ item, noLeadingBorder = false, active = false}) { +export default function Item({ item, noLeadingBorder = false, active = false, style = {}}) { return (
{item.title}
{ diff --git a/js/components/ItemList.js b/js/components/ItemList.js index f433288..d3d7f09 100644 --- a/js/components/ItemList.js +++ b/js/components/ItemList.js @@ -1,20 +1,21 @@ import React from 'react'; import Item from './Item'; -export default function ItemList({ children, items, activeItem, noLeadingBorder }) { +export default function ItemList({ children, items, activeItem, noLeadingBorder, headerStyle = {}, itemStyle = [] }) { return (
-
+
{children}
{ - items.map(item => ( + items.map((item, i) => ( )) } diff --git a/js/components/Link.js b/js/components/Link.js index 165729c..c2c54d0 100644 --- a/js/components/Link.js +++ b/js/components/Link.js @@ -2,14 +2,14 @@ import React from 'react'; import { connect } from 'react-redux'; import actions from '../actions'; -function Link({ pageName, children, navigate, className}) { +function Link({ pageName, children, navigate, className, style = {}}) { const handleClick = (e) => { e.preventDefault(); navigate(pageName); }; return ( - + {children} ); diff --git a/js/components/PageItem.js b/js/components/PageItem.js index 03e67a3..eab733a 100644 --- a/js/components/PageItem.js +++ b/js/components/PageItem.js @@ -4,52 +4,189 @@ import Badge from './Badge'; import ItemList from './ItemList'; import Link from './Link'; import Fadeable from './Fadeable'; +import { createAnimation, createAnimationRunner } from '../animation'; import { groupByQuadrants } from '../../common/model'; -export default function PageItems({ leaving, onLeave, pageName, items }) { - const [quadrantName, itemName] = pageName.split('/'); - const item = items.filter(item => item.quadrant === quadrantName && item.name === itemName)[0]; - const itemsInRing = groupByQuadrants(items)[item.quadrant][item.ring]; +const items = [1, 2]; // TODO use real items - return ( - -
-
-
-
-

Languages & Frameworks

-
- -
-
- {item.ring} -
-
- - Quadrant Overview - -
+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 item = items.filter(item => item.quadrant === quadrantName && item.name === itemName)[0]; + const itemsInRing = groupByQuadrants(items)[item.quadrant][item.ring]; + + // console.log(this.getAnimationState('items')); + + return ( +
+
+
+
+
+

Languages & Frameworks

- + + +
+
+ {item.ring} +
+
+ + Quadrant Overview + +
+
+
+
-
-
-
-
-
-
-

{item.title}

-
-
- {item.ring} +
+
+
+
+
+

{item.title}

+
+
+ {item.ring} +
+
-
- - ); + ); + } } + +export default PageItem;