Skip to content
This repository has been archived by the owner on Apr 14, 2020. It is now read-only.

Commit

Permalink
Merge pull request #143 from element-motion/transform-motion
Browse files Browse the repository at this point in the history
Transform motion
  • Loading branch information
Madou authored Jul 10, 2019
2 parents a68c12c + 6b97dd1 commit 558244d
Show file tree
Hide file tree
Showing 9 changed files with 362 additions and 22 deletions.
10 changes: 10 additions & 0 deletions packages/motions/.size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,14 @@ module.exports = [
path: 'dist/esm/Scale/InverseScale.js',
ignore: ['emotion'],
},
{
limit: '1.86 KB',
path: 'dist/esm/Translate/index.js',
ignore: ['emotion'],
},
{
limit: '118 B',
path: 'dist/esm/Translate/InverseTranslate.js',
ignore: ['emotion'],
},
];
43 changes: 21 additions & 22 deletions packages/motions/src/Scale/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,49 +10,47 @@ import {
standard,
dynamic,
} from '@element-motion/utils';
import { Duration } from '../types';
import { MotionProps } from '../types';

export interface ScaleProps extends CollectorChildrenProps {
timingFunction?: string;
duration?: Duration;
}
export interface ScaleProps extends CollectorChildrenProps, MotionProps {}

const buildKeyframes = (elements: MotionData, duration: number, bezierCurve: string) => {
const frameTime = 1000 / 60;
const nFrames = Math.round(duration / frameTime);
const percentIncrement = 100 / nFrames;
const originBoundingBox = elements.origin.elementBoundingBox;
const destinationBoundingBox = elements.destination.elementBoundingBox;
const timingFunction = bezierToFunc(bezierCurve, duration);

const startX = originBoundingBox.size.width / destinationBoundingBox.size.width;
const startY = originBoundingBox.size.height / destinationBoundingBox.size.height;
const timingFunction = bezierToFunc(bezierCurve, duration);
const outerAnimation: string[] = [];
const innerAnimation: string[] = [];
const endX = 1;
const endY = 1;
const animation: string[] = [];
const inverseAnimation: string[] = [];

for (let i = 0; i <= nFrames; i += 1) {
const step = Number(timingFunction(i / nFrames).toFixed(5));
const percentage = Number((i * percentIncrement).toFixed(5));
const endX = 1;
const endY = 1;
const xScale = Number((startX + (endX - startX) * step).toFixed(5));
const yScale = Number((startY + (endY - startY) * step).toFixed(5));
const invScaleX = (1 / xScale).toFixed(5);
const invScaleY = (1 / yScale).toFixed(5);

outerAnimation.push(`
animation.push(`
${percentage}% {
transform: scale(${xScale}, ${yScale});
transform: scale3d(${xScale}, ${yScale}, 1);
}`);

innerAnimation.push(`
inverseAnimation.push(`
${percentage}% {
transform: scale(${invScaleX}, ${invScaleY});
transform: scale3d(${invScaleX}, ${invScaleY}, 1);
}`);
}

return {
outer: keyframes(outerAnimation),
inner: keyframes(innerAnimation),
animation: keyframes(animation),
inverseAnimation: keyframes(inverseAnimation),
};
};

Expand All @@ -64,8 +62,8 @@ const Scale: React.FC<ScaleProps> = ({
timingFunction = standard(),
}: ScaleProps) => {
let calculatedDuration: number;
let outerKeyframes: string;
let innerKeyframes: string;
let animation: string;
let inverseAnimation: string;

return (
<Collector
Expand All @@ -79,6 +77,7 @@ const Scale: React.FC<ScaleProps> = ({
const scaleToY = originBoundingBox.size.height / destinationBoundingBox.size.height;
const inverseScaleToX = 1 / scaleToX;
const inverseScaleToY = 1 / scaleToY;

calculatedDuration =
duration === 'dynamic'
? dynamic(
Expand All @@ -87,7 +86,7 @@ const Scale: React.FC<ScaleProps> = ({
)
: duration;

({ outer: outerKeyframes, inner: innerKeyframes } = buildKeyframes(
({ animation, inverseAnimation } = buildKeyframes(
elements,
calculatedDuration,
timingFunction
Expand All @@ -105,17 +104,17 @@ const Scale: React.FC<ScaleProps> = ({
style: prevStyle => ({
...prevStyle,
...common,
transform: combine(`scale3d(${scaleToX}, ${scaleToY}, 1)`)(prevStyle.transform),
transform: combine(`scale3d(${scaleToX}, ${scaleToY}, 1)`, '')(prevStyle.transform),
animationDuration: `${calculatedDuration}ms`,
animationName: combine(outerKeyframes)(prevStyle.animationName),
animationName: combine(animation)(prevStyle.animationName),
}),
className: () =>
css({
[`.${INVERSE_SCALE_CLASS_NAME}`]: {
...common,
transform: `scale3d(${inverseScaleToX}, ${inverseScaleToY}, 1)`,
animationDuration: `${calculatedDuration}ms`,
animationName: `${innerKeyframes}`,
animationName: inverseAnimation,
},
}),
});
Expand Down
11 changes: 11 additions & 0 deletions packages/motions/src/Translate/InverseTranslate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as React from 'react';
import { INVERSE_TRANSLATE_CLASS_NAME } from './index';

interface InverseTranslateProps {
children: (opts: { className: string }) => React.ReactElement;
}

const InverseTranslate: React.FC<InverseTranslateProps> = (props: InverseTranslateProps) =>
props.children({ className: INVERSE_TRANSLATE_CLASS_NAME });

export default InverseTranslate;
72 changes: 72 additions & 0 deletions packages/motions/src/Translate/__docz__/docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
name: Translate
route: /translate
menu: Focal motions
---

import { Playground, Props } from 'docz';
import { Toggler } from '@element-motion/dev';
import { Motion } from '@element-motion/utils';
import Translate from '../index';
import InverseTranslate from '../InverseTranslate';
import { Menu } from './styled';

# Translate

Will translate an element from the `origin` to `destination` location.
Can also use `InverseTranslate` component to counteract the transform.

## Usage

```js
import Motion, { Translate, InverseTranslate } from '@element-motion/core';
```

**Try the interactive demos** 👇

### Without inverse translate

<Playground>
<Toggler>
{toggler => (
<Motion triggerSelfKey={toggler.shown}>
<Translate>
{motion => (
<Menu {...motion} right={toggler.shown} onClick={toggler.toggle}>
<div>hello, world</div>
</Menu>
)}
</Translate>
</Motion>
)}
</Toggler>
</Playground>

### With inverse translate

<Playground>
<Toggler>
{toggler => (
<Motion triggerSelfKey={toggler.shown}>
<Translate>
{motion => (
<Menu {...motion} right={toggler.shown} onClick={toggler.toggle}>
<InverseTranslate>{inverse => <div {...inverse}>hello, world</div>}</InverseTranslate>
</Menu>
)}
</Translate>
</Motion>
)}
</Toggler>
</Playground>

## Props

<Props of={Translate} />

## Gotchas

### Inverse translate

Make sure to utilise an element that is a block - either `block`, `inline-block`, `flex`, `inline-flex`.
Else the transforms will not be applied.
19 changes: 19 additions & 0 deletions packages/motions/src/Translate/__docz__/styled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import styled from 'styled-components';
import { colors } from '@element-motion/dev';

export const Menu = styled.div<any>`
display: flex;
align-items: center;
justify-content: center;
background-color: ${colors.red};
border-radius: ${props => (props.isExpanded ? 3 : 1)}px;
color: white;
height: 200px;
width: 200px;
margin-left: ${props => (props.right ? 'auto' : 0)};
cursor: pointer;
> div {
background-color: black;
}
`;
160 changes: 160 additions & 0 deletions packages/motions/src/Translate/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import * as React from 'react';
import { css, keyframes, cx } from 'emotion';
import {
Collector,
CollectorChildrenProps,
CollectorActions,
MotionData,
bezierToFunc,
combine,
standard,
dynamic,
recalculateElementBoundingBoxFromScroll,
} from '@element-motion/utils';
import { MotionProps } from '../types';

export interface TranslateProps extends CollectorChildrenProps, MotionProps {}

const buildKeyframes = (elements: MotionData, duration: number, bezierCurve: string) => {
const frameTime = 1000 / 60;
const nFrames = Math.round(duration / frameTime);
const percentIncrement = 100 / nFrames;
const originBoundingBox = recalculateElementBoundingBoxFromScroll(
elements.origin.elementBoundingBox
);
const destinationBoundingBox = elements.destination.elementBoundingBox;
const timingFunction = bezierToFunc(bezierCurve, duration);

const startX = originBoundingBox.location.left - destinationBoundingBox.location.left;
const startY = originBoundingBox.location.top - destinationBoundingBox.location.top;
const endX = 0;
const endY = 0;
const animation: string[] = [];
const inverseAnimation: string[] = [];

for (let i = 0; i <= nFrames; i += 1) {
const step = Number(timingFunction(i / nFrames).toFixed(5));
const percentage = Number((i * percentIncrement).toFixed(5));
const translateToX = Number((startX + (endX - startX) * step).toFixed(5));
const translateToY = Number((startY + (endY - startY) * step).toFixed(5));
const inverseTranslateToX = -translateToX;
const inverseTranslateToY = -translateToY;

animation.push(`
${percentage}% {
transform: translate3d(${translateToX}px, ${translateToY}px, 0);
}`);

inverseAnimation.push(`
${percentage}% {
transform: translate3d(${inverseTranslateToX}px, ${inverseTranslateToY}px, 0);
}`);
}

return {
animation: keyframes(animation),
inverseAnimation: keyframes(inverseAnimation),
};
};

export const INVERSE_TRANSLATE_CLASS_NAME = 'inVRsE-tRnsFrm';

const Translate: React.FC<TranslateProps> = ({
children,
duration = 'dynamic',
timingFunction = standard(),
}: TranslateProps) => {
let calculatedDuration: number;
let animation: string;
let inverseAnimation: string;

return (
<Collector
data={{
action: CollectorActions.motion,
payload: {
beforeAnimate: (elements, onFinish, setChildProps) => {
const originBoundingBox = recalculateElementBoundingBoxFromScroll(
elements.origin.elementBoundingBox
);
const destinationBoundingBox = elements.destination.elementBoundingBox;
const translateToX =
originBoundingBox.location.left - destinationBoundingBox.location.left;
const translateToY =
originBoundingBox.location.top - destinationBoundingBox.location.top;
const inverseTranslateToX = -translateToX;
const inverseTranslateToY = -translateToY;

calculatedDuration =
duration === 'dynamic'
? dynamic(
elements.origin.elementBoundingBox,
elements.destination.elementBoundingBox
)
: duration;

({ animation, inverseAnimation } = buildKeyframes(
elements,
calculatedDuration,
timingFunction
));

const common = {
willChange: 'transform',
transformOrigin: 'top left',
animationTimingFunction: 'step-end',
animationFillMode: 'forwards',
animationPlayState: 'paused',
};

setChildProps({
style: prevStyle => ({
...prevStyle,
...common,
transform: combine(`translate3d(${translateToX}px, ${translateToY}px, 0)`, '')(
prevStyle.transform
),
animationDuration: `${calculatedDuration}ms`,
animationName: combine(animation)(prevStyle.animationName),
}),
className: () =>
css({
[`.${INVERSE_TRANSLATE_CLASS_NAME}`]: {
...common,
transform: `translate3d(${inverseTranslateToX}px, ${inverseTranslateToY}px, 0)`,
animationDuration: `${calculatedDuration}ms`,
animationName: inverseAnimation,
},
}),
});

onFinish();
},
animate: (_, onFinish, setChildProps) => {
setChildProps({
style: prevStyle => ({
...prevStyle,
animationPlayState: 'running',
}),
className: prevClassName =>
cx(
prevClassName,
css({
[`.${INVERSE_TRANSLATE_CLASS_NAME}`]: {
animationPlayState: 'running',
},
})
),
});

setTimeout(onFinish, calculatedDuration);
},
},
}}
>
{children}
</Collector>
);
};

export default Translate;
Loading

0 comments on commit 558244d

Please sign in to comment.