Skip to content

Commit

Permalink
feat: add shortcut props to "style" for "style.transform"
Browse files Browse the repository at this point in the history
Shortcut props include:
- x/y/z
- translate/translateX/translateY/translate3d
- rotate/rotateX/rotateY/rotate3d
- scale/scaleX/scaleY/scale3d
- skew/skewX/skewY
- matrix/matrix3d

These props and the "transform" prop are combined (in order of appearance) into a single "transform" string.

This feature is for "@react-spring/web" only.

Co-Authored-By: Alec Larson <alec.stanford.larson@gmail.com>
Co-Authored-By: Paul Henschel <drcmda@gmail.com>
  • Loading branch information
3 people committed Aug 16, 2019
1 parent dc1a84b commit e27a1d4
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 0 deletions.
189 changes: 189 additions & 0 deletions targets/web/src/AnimatedStyle.ts
@@ -0,0 +1,189 @@
import { SpringValue, each, is } from 'shared'

import {
AnimatedObject,
Animated,
AnimatedArray,
AnimatedValue,
isAnimated,
to,
interpolate,
} from '@react-spring/animated'

/** The transform-functions
* (https://developer.mozilla.org/fr/docs/Web/CSS/transform-function)
* that you can pass as keys to your animated component style and that will be
* animated. Perspective has been left out as it would conflict with the
* non-transform perspective style.
*/
const domTransforms = [
'transform',
'matrix',
'translate',
'scale',
'rotate',
'skew',
]

// x, y, z and translate will get 'px' as unit default
const pxDefaults = ['x', 'y', 'z', 'translate']
// rotate and skew will get 'deg' as unit default
const dgDefaults = ['rotate', 'skew']

// adds a unit to the value when the value is unit-less (ie a number)
const mergeUnit = (value: number | string, unit: string): string | 0 =>
is.num(value) && value !== 0 ? value + unit : value

// gets the default unit for a key
const getUnit = (key: string) =>
pxDefaults.some(name => key.startsWith(name))
? 'px'
: dgDefaults.some(name => key.startsWith(name))
? 'deg'
: ''

type Value = number | string
type StyleValue = Value | Value[]
type AnimatedValueType = Animated & { to: any; interpolate: any }

/**
* Returns the right Animated object based on the value type.
*
* x: AnimatedValue --> x
* 40px --> AnimatedValue(40px)
* [40, 30] --> AnimatedValue([40,30])
* [40, y: AnimatedValue] --> AnimatedArray([AnimatedValue(40), y])
*
* @param value
*/
const ensureAnimated = (
value: AnimatedValueType | StyleValue = 0
): AnimatedValueType =>
is.arr(value) && value.some(isAnimated)
? new AnimatedArray(value.map(ensureAnimated))
: isAnimated(value)
? value
: new AnimatedValue(value)

/**
* Checks if the input value matches the identity value.
*
* isValueIdentity(0, 0) --> true
* isValueIdentity('0px', 0) --> true
* isValueIdentity([0, '0px', 0], 0) --> true
*
* @param styleValue
* @param id
*/
const isValueIdentity = (styleValue: StyleValue, id: number): boolean =>
is.arr(styleValue)
? styleValue.every(v => isValueIdentity(v, id))
: is.num(styleValue)
? styleValue === id
: parseFloat(styleValue) === id

/**
* Checks if the style value is the identity for a given key.
*
* isTransformIdentity('scale', 1) --> true
* isTransformIdentity('scale', [1,1])
* isTransformIdentity('rotate3d', [1,1,1,0]) --> true
* isTransformIdentity('x', 0) --> true
*
* @param key
* @param styleValue
*/
const isTransformIdentity = (key: string, styleValue: StyleValue): boolean =>
key === 'rotate3d'
? isValueIdentity(is.arr(styleValue) ? styleValue[3] : styleValue, 0)
: key.startsWith('scale')
? isValueIdentity(styleValue, 1)
: isValueIdentity(styleValue, 0)

type Style = object & {
transform?: any
x?: any
y?: any
z?: any
[k: string]: string
}
type Transform = (arg: any) => [string, boolean]

/**
* This AnimatedStyle will simplify animated components transforms by
* interpolating all transform function passed as keys in the style object
* including shortcuts such as x, y and z for translateX/Y/Z
*/
export class AnimatedStyle extends AnimatedObject {
constructor({ x, y, z, ...style } = {} as Style) {
const props: SpringValue[] = []

// transforms will be an array of functions applied to the props. Each function
// will return the interpolated transformed string, and a flag indicating if the
// interpolation result is an identity transform of its own
const transforms: Transform[] = []

// first we deal with x, y, z to group them into a single translate3d
if (x || y || z) {
// xyz should be an AnimatedValue or AnimatedArray
const xyz = ensureAnimated([x || 0, y || 0, z || 0])
// we add it to the array of Animated objects that will be interpolated
props.push(xyz)

// we add the interpolation function to our transform array
transforms.push(([vx, vy, vz]: Value[]) => [
`translate3d(${mergeUnit(vx, getUnit('x'))},${mergeUnit(
vy,
getUnit('y')
)},${mergeUnit(vz, getUnit('z'))})`,
isValueIdentity([vx, vy, vz], 0),
])
}

// then for each style key that matches the transform functions class
// supports, we add the input value to the props and the interpolation
// transform function
each(style, (value, key) => {
if (domTransforms.some(transform => key.startsWith(transform))) {
const unit = getUnit(key)
props.push(ensureAnimated(value))
transforms.push(
key === 'transform'
? (transform: string) => [transform, transform === '']
: key === 'rotate3d'
? ([x, y, z, deg]) => [
`rotate3d(${x},${y},${z},${mergeUnit(deg, unit)})`,
isTransformIdentity(key, deg),
]
: (arg: StyleValue) => [
is.arr(arg)
? `${key}(${arg.map(v => mergeUnit(v, unit)).join(',')})`
: `${key}(${mergeUnit(arg, unit)})`,
isTransformIdentity(key, arg),
]
)
delete style[key]
}
})

// finally, we set the transform key of the animated style to the
// interpolation of all the props, using the transform functions we defined
// above
if (props.length > 0) {
style.transform = interpolate(props, (...args) => {
let transform = ''
let identity = true
for (let i = 0; i < args.length; i++) {
const [t, id] = transforms[i](args[i])
transform += ' ' + t
identity = identity && id
}
// if the identity flag was true for all transforms, we set the transform
// to none, otherwise we return the concatenated transform string
return identity ? 'none' : transform
})
}

super(style)
}
}
2 changes: 2 additions & 0 deletions targets/web/src/index.ts
@@ -1,13 +1,15 @@
import { Globals } from '@react-spring/core'
import { applyAnimatedValues } from './applyAnimatedValues'
import { createStringInterpolator } from 'shared/stringInterpolation'
import { AnimatedStyle } from './AnimatedStyle'
import colorNames from 'shared/colors'

Globals.assign({
defaultElement: 'div',
colorNames,
applyAnimatedValues,
createStringInterpolator,
createAnimatedStyle: style => new AnimatedStyle(style),
getComponentProps: ({ scrollTop, scrollLeft, ...props }) => props,
})

Expand Down
9 changes: 9 additions & 0 deletions targets/web/src/types/__tests__/animated.tsx
Expand Up @@ -97,4 +97,13 @@ describe('animated component', () => {
scrollTop.setValue(20);
expect(wrapper.scrollTop).toBe(20);
});

it('accepts transform function and x/y/z as style keys', () => {
const AnimatedDiv = animated('div');
const { queryByTestId } = render(
<AnimatedDiv style={{ x: 10 }} data-testid="wrapper"></AnimatedDiv>
);
const wrapper: any = queryByTestId('wrapper')!;
expect(wrapper.style.transform).toBe('translate3d(10px,0,0)');
});
});

0 comments on commit e27a1d4

Please sign in to comment.