fix animations

This commit is contained in:
Bastian Ike
2020-07-17 15:51:34 +02:00
committed by Bastian
parent ad82fe302f
commit be0f0a2cf0
5 changed files with 296 additions and 337 deletions

View File

@@ -1,134 +1,110 @@
import React from 'react';
export type Animations = {
[k: string]: Animation[]
}
export type AnimationStates = {
[k: string]: React.CSSProperties[]
}
type Animation = { type Animation = {
stateA: React.CSSProperties stateA: React.CSSProperties
stateB: React.CSSProperties stateB: React.CSSProperties
delay: number delay: number
run?(callback: (state: any) => any): any // todo fix run?(callback: (state: any) => any): any // todo fix
prepare?(callback: (state: any) => any): any // todo fix prepare?(callback: (state: any) => any): any // todo fix
} }
type AnimationController = {} export type AnimationRunner = {
getState(): AnimationStates
type AnimationRunner = { run(): any
getState(): any awaitAnimationComplete(callback: () => void): any
run(): any
awaitAnimationComplete(callback: () => void): any
}
export const createAnimationController = (animations: {[k: string]: Animation}, component: any): AnimationController => {
return {
animations,
start: () => {
Object.entries(animations).map(([name, animation]) => animation.run && animation.run((state) => {
component.setState({
...component.state,
[name]: state,
});
}));
},
prepare: () => {
Object.entries(animations).map(([name, animation]) => animation.prepare && animation.prepare((state) => {
component.setState({
...component.state,
[name]: state,
});
}));
}
};
} }
export const createAnimation = (stateA: React.CSSProperties, stateB: React.CSSProperties, delay: number): Animation => ({ export const createAnimation = (stateA: React.CSSProperties, stateB: React.CSSProperties, delay: number): Animation => ({
stateA, stateA,
stateB, stateB,
delay, delay,
}); });
const getAnimationState = (animation: Animation | Animation[], stateName: 'stateA' | 'stateB' = 'stateA'): React.CSSProperties => { const getAnimationStates = (animations: Animation[], stateName: 'stateA' | 'stateB' = 'stateA'): React.CSSProperties[] => {
if (animation instanceof Array) { return animations.map(animation => animation[stateName]);
return animation.map(a => getAnimationState(a, stateName))[0]; // todo fix
}
return animation[stateName];
}; };
const getMaxTransitionTime = (transition: string) => { const getMaxTransitionTime = (transition: string) => {
const re = /(\d+)ms/g; const re = /(\d+)ms/g;
const times: number[] = []; const times: number[] = [];
let matches; let matches;
while ((matches = re.exec(transition)) != null) { while ((matches = re.exec(transition)) != null) {
times.push(parseInt(matches[1], 10)); times.push(parseInt(matches[1], 10));
} }
return Math.max(...times); return Math.max(...times);
}; };
const getAnimationDuration = (animation: Animation | Animation[]): number => { const getAnimationDuration = (animation: Animation | Animation[]): number => {
if (animation instanceof Array) { if (animation instanceof Array) {
return animation.reduce((maxDuration, a) => { return animation.reduce((maxDuration, a) => {
const duration = getAnimationDuration(a); const duration = getAnimationDuration(a);
if (duration > maxDuration) { if (duration > maxDuration) {
return duration; return duration;
} }
return maxDuration; return maxDuration;
}, 0); }, 0);
} }
const state = animation.stateB; const state = animation.stateB;
const maxTransition = state.transition ? getMaxTransitionTime(state.transition) : 0; const maxTransition = state.transition ? getMaxTransitionTime(state.transition) : 0;
return maxTransition + animation.delay; return maxTransition + animation.delay;
}; };
const getMaxAnimationsDuration = (animations: {[k: string]: Animation} | Animation[]) => ( const getMaxAnimationsDuration = (animations: Animations) => (
getAnimationDuration(Object.values(animations)) Math.max(...Object.values(animations).map(animations => getAnimationDuration(Object.values(animations))))
); );
export const createAnimationRunner = (animations: {[k: string]: Animation} | Animation[], subscriber: () => void = () => {}):AnimationRunner => { export const createAnimationRunner = (animationsIn: { [k: string]: (Animation | Animation[]) }, subscriber: () => void = () => {
let state = Object.entries(animations).reduce((state, [name, animation]) => ({ }): AnimationRunner => {
...state, const animations = Object.entries(animationsIn).reduce((state, [name, animation]) => ({
[name]: getAnimationState(animation), ...state,
}), {}); [name]: animation instanceof Array ? animation : [animation] as Animation[],
}), {} as Animations);
const animationsDuration = getMaxAnimationsDuration(animations); let state = Object.entries(animations).reduce((state, [name, animation]) => ({
...state,
[name]: getAnimationStates(animation),
}), {} as AnimationStates);
const animate = (name: string, animation: Animation) => { const animationsDuration = getMaxAnimationsDuration(animations);
if (animation instanceof Array) {
animation.forEach((a, index) => { const animate = (name: string, animation: Animation[]) => {
window.requestAnimationFrame(() => { animation.forEach((a, index) => {
window.setTimeout(() => { window.requestAnimationFrame(() => {
state = { window.setTimeout(() => {
...state, state = {
[name]: [ ...state,
// ...(state[name]?.slice(0, index)), // todo fix [name]: [
a.stateB, ...(state[name]?.slice(0, index)),
// ...(state[name]?.slice(index + 1, state[name].length)), // todo fix a.stateB,
], ...(state[name]?.slice(index + 1, state[name].length)),
}; ],
subscriber(); };
}, a.delay); subscriber();
}, a.delay);
});
}); });
});
} else {
window.requestAnimationFrame(() => {
window.setTimeout(() => {
state = {
...state,
[name]: animation.stateB,
};
subscriber();
}, animation.delay);
});
} }
}
return { return {
getState() { getState() {
return state; return state;
}, },
run() { run() {
Object.entries(animations).forEach(([name, animation]) => { Object.entries(animations).forEach(([name, animation]) => {
animate(name, animation) animate(name, animation)
}); });
}, },
awaitAnimationComplete(callback) { awaitAnimationComplete(callback) {
window.setTimeout(callback, animationsDuration); window.setTimeout(callback, animationsDuration);
}, },
} }
} }

View File

@@ -1,243 +1,239 @@
import React, { useEffect, useState } from 'react'; import React, {useEffect, useState} from 'react';
import Badge from '../Badge/Badge'; import Badge from '../Badge/Badge';
import ItemList from '../ItemList/ItemList'; import ItemList from '../ItemList/ItemList';
import Link from '../Link/Link'; import Link from '../Link/Link';
import FooterEnd from '../FooterEnd/FooterEnd'; import FooterEnd from '../FooterEnd/FooterEnd';
import SetTitle from '../SetTitle'; import SetTitle from '../SetTitle';
import ItemRevisions from '../ItemRevisions/ItemRevisions'; import ItemRevisions from '../ItemRevisions/ItemRevisions';
import { createAnimation, createAnimationRunner } from '../../animation'; import {
AnimationStates,
createAnimation,
createAnimationRunner
} from '../../animation';
import './item-page.scss'; import './item-page.scss';
import { translate } from '../../config'; import {translate} from '../../config';
import { groupByQuadrants, Item } from '../../model'; import {groupByQuadrants, Item} from '../../model';
const getItem = (pageName: string, items: Item[]) => { const getItem = (pageName: string, items: Item[]) => {
const [quadrantName, itemName] = pageName.split('/'); const [quadrantName, itemName] = pageName.split('/');
const item = items.filter((item) => item.quadrant === quadrantName && item.name === itemName)[0]; return items.filter((item) => item.quadrant === quadrantName && item.name === itemName)[0];
return item;
}; };
const getItemsInRing = (pageName: string, items: Item[]) => { const getItemsInRing = (pageName: string, items: Item[]) => {
const item = getItem(pageName, items); const item = getItem(pageName, items);
const itemsInRing = groupByQuadrants(items)[item.quadrant][item.ring]; return groupByQuadrants(items)[item.quadrant][item.ring];
return itemsInRing;
}; };
type PageItemProps = { type PageItemProps = {
pageName: string; pageName: string;
items: Item[]; items: Item[];
leaving: boolean; leaving: boolean;
onLeave: () => void; onLeave: () => void;
}; };
export default function PageItem({ pageName, items, leaving, onLeave }: PageItemProps) { export default function PageItem({pageName, items, leaving, onLeave}: PageItemProps) {
const itemsInRing = getItemsInRing(pageName, items); const itemsInRing = getItemsInRing(pageName, items);
const animationsIn = { const animationsIn = {
background: createAnimation( background: createAnimation(
{ {
transform: 'translateX(calc((100vw - 1200px) / 2 + 800px))', 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)',
}, },
{ {
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)', transform: 'translateX(0)',
}, },
0 0
), ),
navHeader: createAnimation( navHeader: createAnimation(
{ {
transform: 'translateX(-40px)', transform: 'translateX(-40px)',
opacity: '0', opacity: '0',
}, },
{ {
transition: 'opacity 150ms ease-out, transform 300ms ease-out', transition: 'opacity 150ms ease-out, transform 300ms ease-out',
transform: 'translateX(0px)', transform: 'translateX(0px)',
opacity: '1', opacity: '1',
}, },
300 300
), ),
text: createAnimation( text: createAnimation(
{ {
transform: 'translateY(-20px)', transform: 'translateY(-20px)',
opacity: '0', opacity: '0',
}, },
{ {
transition: 'opacity 150ms ease-out, transform 300ms ease-out', transition: 'opacity 150ms ease-out, transform 300ms ease-out',
transform: 'translateY(0px)', transform: 'translateY(0px)',
opacity: '1', opacity: '1',
}, },
600 600
), ),
items: itemsInRing.map((item, i) => items: itemsInRing.map((item, i) =>
createAnimation( createAnimation(
{ {
transform: 'translateX(-40px)', transform: 'translateX(-40px)',
opacity: '0', opacity: '0',
}, },
{ {
transition: 'opacity 150ms ease-out, transform 300ms ease-out', transition: 'opacity 150ms ease-out, transform 300ms ease-out',
transform: 'translateX(0px)', transform: 'translateX(0px)',
opacity: '1', opacity: '1',
}, },
400 + 100 * i 400 + 100 * i
) )
), ),
footer: createAnimation( footer: createAnimation(
{ {
transition: 'opacity 150ms ease-out, transform 300ms ease-out', transition: 'opacity 150ms ease-out, transform 300ms ease-out',
transform: 'translateX(-40px)', transform: 'translateX(-40px)',
opacity: '0', opacity: '0',
}, },
{ {
transition: 'opacity 150ms ease-out, transform 300ms ease-out', transition: 'opacity 150ms ease-out, transform 300ms ease-out',
transform: 'translateX(0px)', transform: 'translateX(0px)',
opacity: '1', opacity: '1',
}, },
600 + itemsInRing.length * 100 600 + itemsInRing.length * 100
), ),
}; };
const animationsOut = { const animationsOut = {
background: createAnimation(animationsIn.background.stateB, animationsIn.background.stateA, 300 + itemsInRing.length * 50), background: createAnimation(animationsIn.background.stateB, animationsIn.background.stateA, 300 + itemsInRing.length * 50),
navHeader: createAnimation( navHeader: createAnimation(
animationsIn.navHeader.stateB, animationsIn.navHeader.stateB,
{ {
transition: 'opacity 150ms ease-out, transform 300ms ease-out', transition: 'opacity 150ms ease-out, transform 300ms ease-out',
transform: 'translateX(40px)', transform: 'translateX(40px)',
opacity: '0', opacity: '0',
}, },
0 0
), ),
text: createAnimation( text: createAnimation(
animationsIn.text.stateB, animationsIn.text.stateB,
{ {
transform: 'translateY(20px)', transform: 'translateY(20px)',
transition: 'opacity 150ms ease-out, transform 300ms ease-out', transition: 'opacity 150ms ease-out, transform 300ms ease-out',
opacity: '0', opacity: '0',
}, },
0 0
), ),
items: itemsInRing.map((item, i) => items: itemsInRing.map((item, i) =>
createAnimation( createAnimation(
animationsIn.items[i].stateB, animationsIn.items[i].stateB,
{ {
transition: 'opacity 150ms ease-out, transform 300ms ease-out', transition: 'opacity 150ms ease-out, transform 300ms ease-out',
transform: 'translateX(40px)', transform: 'translateX(40px)',
opacity: '0', opacity: '0',
}, },
100 + 50 * i 100 + 50 * i
) )
), ),
footer: createAnimation( footer: createAnimation(
animationsIn.text.stateB, animationsIn.text.stateB,
{ {
transition: 'opacity 150ms ease-out, transform 300ms ease-out', transition: 'opacity 150ms ease-out, transform 300ms ease-out',
transform: 'translateX(40px)', transform: 'translateX(40px)',
opacity: '0', opacity: '0',
}, },
200 + itemsInRing.length * 50 200 + itemsInRing.length * 50
), ),
}; };
const [animations, setAnimations] = useState<any>(); const [animations, setAnimations] = useState<AnimationStates>(() => {
return leaving ? createAnimationRunner(animationsIn).getState() : {}
});
useEffect(() => { const [stateLeaving, setStateLeaving] = useState(leaving);
if (leaving) {
// entering from an other page useEffect(() => {
// setAnimations(createAnimationRunner(animationsIn).getState()) if (!stateLeaving && leaving) {
} else { let animationRunner = createAnimationRunner(
// Hard refresh animationsOut,
setAnimations(null); () => setAnimations(animationRunner.getState),
)
animationRunner.run();
animationRunner.awaitAnimationComplete(onLeave);
setStateLeaving(true)
}
if (stateLeaving && !leaving) {
let animationRunner = createAnimationRunner(
animationsIn,
() => setAnimations(animationRunner.getState),
)
animationRunner.run();
setStateLeaving(false)
}
}, [stateLeaving, leaving])
const getAnimationStates = (name: string) => {
if (!animations) {
return undefined;
}
return animations[name];
} }
}, [leaving]);
const [stateLeaving, setStateLeaving] = useState(leaving); const getAnimationState = (name: string) => {
const animations = getAnimationStates(name)
if (animations === undefined || animations.length === 0) {
return undefined
}
return animations[0]
};
let [animationRunner, setAnimationRunner] = useState<any>(); const item = getItem(pageName, items);
// useEffect(() => { return (
// if (!stateLeaving && leaving) { <div>
// animationRunner = createAnimationRunner( <SetTitle title={item.title}/>
// animationsOut, <div className='item-page'>
// handleAnimationsUpdate, <div className='item-page__nav'>
// ); <div className='item-page__nav__inner'>
// setAnimationRunner(animationRunner) <div className='item-page__header' style={getAnimationState('navHeader')}>
// animationRunner.run(); <h3 className='headline'>{translate(item.quadrant)}</h3>
// animationRunner.awaitAnimationComplete(onLeave); </div>
// }
// if (stateLeaving && !leaving) {
// animationRunner = createAnimationRunner(
// animationsIn,
// handleAnimationsUpdate,
// );
// setAnimationRunner(animationRunner)
// animationRunner.run();
// }
// setStateLeaving(leaving)
// }, [leaving])
const handleAnimationsUpdate = () => { <ItemList items={itemsInRing} activeItem={item} headerStyle={getAnimationState('navHeader')}
setAnimations(animationRunner.getState()); itemStyle={getAnimationStates('items')}>
}; <div className='split'>
<div className='split__left'>
const getAnimationState = (name: string) => { <Badge big type={item.ring}>
if (!animations) { {item.ring}
return undefined; </Badge>
} </div>
return animations[name]; <div className='split__right'>
}; <Link className='icon-link' pageName={item.quadrant}>
<span className='icon icon--pie icon-link__icon'/>
const item = getItem(pageName, items); Quadrant Overview
</Link>
return ( </div>
<div> </div>
<SetTitle title={item.title} /> </ItemList>
<div className='item-page'> <div className='item-page__footer' style={getAnimationState('footer')}>
<div className='item-page__nav'> <FooterEnd modifier='in-sidebar'/>
<div className='item-page__nav__inner'> </div>
<div className='item-page__header' style={getAnimationState('navHeader')}> </div>
<h3 className='headline'>{translate(item.quadrant)}</h3>
</div>
<ItemList items={itemsInRing} activeItem={item} headerStyle={getAnimationState('navHeader')} itemStyle={getAnimationState('items')}>
<div className='split'>
<div className='split__left'>
<Badge big type={item.ring}>
{item.ring}
</Badge>
</div> </div>
<div className='split__right'> <div className='item-page__content' style={getAnimationState('background')}>
<Link className='icon-link' pageName={item.quadrant}> <div className='item-page__content__inner' style={getAnimationState('text')}>
<span className='icon icon--pie icon-link__icon' /> <div className='item-page__header'>
Quadrant Overview <div className='split'>
</Link> <div className='split__left'>
<h1 className='hero-headline hero-headline--inverse'>{item.title}</h1>
</div>
<div className='split__right'>
<Badge big type={item.ring}>
{item.ring}
</Badge>
</div>
</div>
</div>
<div className='markdown' dangerouslySetInnerHTML={{__html: item.body}}/>
{item.revisions.length > 1 && <ItemRevisions revisions={item.revisions.slice(1)}/>}
</div>
</div> </div>
</div>
</ItemList>
<div className='item-page__footer' style={getAnimationState('footer')}>
<FooterEnd modifier='in-sidebar' />
</div> </div>
</div>
</div> </div>
<div className='item-page__content' style={getAnimationState('background')}> );
<div className='item-page__content__inner' style={getAnimationState('text')}>
<div className='item-page__header'>
<div className='split'>
<div className='split__left'>
<h1 className='hero-headline hero-headline--inverse'>{item.title}</h1>
</div>
<div className='split__right'>
<Badge big type={item.ring}>
{item.ring}
</Badge>
</div>
</div>
</div>
<div className='markdown' dangerouslySetInnerHTML={{ __html: item.body }} />
{item.revisions.length > 1 && <ItemRevisions revisions={item.revisions.slice(1)} />}
</div>
</div>
</div>
</div>
);
} }

View File

@@ -4,7 +4,7 @@ import Badge from '../Badge/Badge';
import Link from '../Link/Link'; import Link from '../Link/Link';
import ItemList from '../ItemList/ItemList'; import ItemList from '../ItemList/ItemList';
import Flag from '../Flag/Flag'; import Flag from '../Flag/Flag';
import { Item, Group } from '../../model'; import { Group } from '../../model';
import './quadrant-section.scss'; import './quadrant-section.scss';
const renderList = (ringName: Ring, quadrantName: string, groups: Group, big: boolean) => { const renderList = (ringName: Ring, quadrantName: string, groups: Group, big: boolean) => {
const itemsInRing = groups[quadrantName][ringName]; const itemsInRing = groups[quadrantName][ringName];

View File

@@ -61,7 +61,6 @@ export default function Router({pageName, items, releases, search}: RouterProps)
}, [pageName, items, statePageName]); }, [pageName, items, statePageName]);
const handlePageLeave = () => { const handlePageLeave = () => {
setLeaving(true);
setStatePageName(nextPageName); setStatePageName(nextPageName);
setNextPageName(''); setNextPageName('');
@@ -76,7 +75,8 @@ export default function Router({pageName, items, releases, search}: RouterProps)
case page.index: case page.index:
return <PageIndex leaving={leaving} items={items} onLeave={handlePageLeave} releases={releases}/>; return <PageIndex leaving={leaving} items={items} onLeave={handlePageLeave} releases={releases}/>;
case page.overview: case page.overview:
return <PageOverview items={items} rings={rings} search={search} leaving={leaving} onLeave={handlePageLeave}/>; return <PageOverview items={items} rings={rings} search={search} leaving={leaving}
onLeave={handlePageLeave}/>;
case page.help: case page.help:
return <PageHelp leaving={leaving} onLeave={handlePageLeave}/>; return <PageHelp leaving={leaving} onLeave={handlePageLeave}/>;
case page.quadrant: case page.quadrant:

View File

@@ -26,17 +26,6 @@ export function assetUrl(file: string) {
// return `/techradar/assets/${file}` // return `/techradar/assets/${file}`
} }
const getPageNames = (radar: Radar) => {
return [
'index',
'overview',
'help-and-about-tech-radar',
'aoe-toolbox',
...quadrants,
...getItemPageNames(radar.items),
]
}
export const getItemPageNames = (items: Item[]) => items.map(item => `${item.quadrant}/${item.name}`); export const getItemPageNames = (items: Item[]) => items.map(item => `${item.quadrant}/${item.name}`);
const messages:{[k: string]: string} = { const messages:{[k: string]: string} = {
@@ -55,5 +44,3 @@ export function isMobileViewport() {
const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
return width < 1200; return width < 1200;
} }
// const formatRelease = (release: moment.MomentInput) => moment(release, 'YYYY-MM-DD').format('MMM YYYY');