|
| 1 | +import { AnimatedObject } from "./AnimatedObject" |
| 2 | +import { addFluidObserver, callFluidObservers, FluidEvent, FluidValue, getFluidValue, hasFluidValue, removeFluidObserver } from "./fluids" |
| 3 | +import { is,each, Lookup, OneOrMore, eachProp, toArray } from "./utils" |
| 4 | + |
| 5 | +/** The transform-functions |
| 6 | + * (https://developer.mozilla.org/fr/docs/Web/CSS/transform-function) |
| 7 | + * that you can pass as keys to your animated component style and that will be |
| 8 | + * animated. Perspective has been left out as it would conflict with the |
| 9 | + * non-transform perspective style. |
| 10 | + */ |
| 11 | +const domTransforms = /^(matrix|translate|scale|rotate|skew)/ |
| 12 | + |
| 13 | +// These keys have "px" units by default |
| 14 | +const pxTransforms = /^(translate)/ |
| 15 | + |
| 16 | +// These keys have "deg" units by default |
| 17 | +const degTransforms = /^(rotate|skew)/ |
| 18 | + |
| 19 | +type Value = number | string |
| 20 | + |
| 21 | +/** Add a unit to the value when the value is unit-less (eg: a number) */ |
| 22 | +const addUnit = (value: Value, unit: string): string | 0 => |
| 23 | + is.num(value) && value !== 0 ? value + unit : value |
| 24 | + |
| 25 | +/** |
| 26 | + * Checks if the input value matches the identity value. |
| 27 | + * |
| 28 | + * isValueIdentity(0, 0) // => true |
| 29 | + * isValueIdentity('0px', 0) // => true |
| 30 | + * isValueIdentity([0, '0px', 0], 0) // => true |
| 31 | + */ |
| 32 | +const isValueIdentity = (value: OneOrMore<Value>, id: number): boolean => |
| 33 | + is.arr(value) |
| 34 | + ? value.every(v => isValueIdentity(v, id)) |
| 35 | + : is.num(value) |
| 36 | + ? value === id |
| 37 | + : parseFloat(value) === id |
| 38 | + |
| 39 | +type Inputs = ReadonlyArray<Value | FluidValue<Value>>[] |
| 40 | +type Transforms = ((value: any) => [string, boolean])[] |
| 41 | + |
| 42 | +/** |
| 43 | + * This AnimatedStyle will simplify animated components transforms by |
| 44 | + * interpolating all transform function passed as keys in the style object |
| 45 | + * including shortcuts such as x, y and z for translateX/Y/Z |
| 46 | + */ |
| 47 | +export class AnimatedStyle extends AnimatedObject { |
| 48 | + constructor({ x, y, z, ...style }: Lookup) { |
| 49 | + /** |
| 50 | + * An array of arrays that contains the values (static or fluid) |
| 51 | + * used by each transform function. |
| 52 | + */ |
| 53 | + const inputs: Inputs = [] |
| 54 | + /** |
| 55 | + * An array of functions that take a list of values (static or fluid) |
| 56 | + * and returns (1) a CSS transform string and (2) a boolean that's true |
| 57 | + * when the transform has no effect (eg: an identity transform). |
| 58 | + */ |
| 59 | + const transforms: Transforms = [] |
| 60 | + |
| 61 | + // Combine x/y/z into translate3d |
| 62 | + if (x || y || z) { |
| 63 | + inputs.push([x || 0, y || 0, z || 0]) |
| 64 | + transforms.push((xyz: Value[]) => [ |
| 65 | + `translate3d(${xyz.map(v => addUnit(v, 'px')).join(',')})`, // prettier-ignore |
| 66 | + isValueIdentity(xyz, 0), |
| 67 | + ]) |
| 68 | + } |
| 69 | + |
| 70 | + // Pluck any other transform-related props |
| 71 | + eachProp(style, (value, key) => { |
| 72 | + if (key === 'transform') { |
| 73 | + inputs.push([value || '']) |
| 74 | + transforms.push((transform: string) => [transform, transform === '']) |
| 75 | + } else if (domTransforms.test(key)) { |
| 76 | + delete style[key] |
| 77 | + if (is.und(value)) return |
| 78 | + |
| 79 | + const unit = pxTransforms.test(key) |
| 80 | + ? 'px' |
| 81 | + : degTransforms.test(key) |
| 82 | + ? 'deg' |
| 83 | + : '' |
| 84 | + |
| 85 | + inputs.push(toArray(value)) |
| 86 | + transforms.push( |
| 87 | + key === 'rotate3d' |
| 88 | + ? ([x, y, z, deg]: [number, number, number, Value]) => [ |
| 89 | + `rotate3d(${x},${y},${z},${addUnit(deg, unit)})`, |
| 90 | + isValueIdentity(deg, 0), |
| 91 | + ] |
| 92 | + : (input: Value[]) => [ |
| 93 | + `${key}(${input.map(v => addUnit(v, unit)).join(',')})`, |
| 94 | + isValueIdentity(input, key.startsWith('scale') ? 1 : 0), |
| 95 | + ] |
| 96 | + ) |
| 97 | + } |
| 98 | + }) |
| 99 | + |
| 100 | + if (inputs.length) { |
| 101 | + style.transform = new FluidTransform(inputs, transforms) |
| 102 | + } |
| 103 | + |
| 104 | + super(style) |
| 105 | + } |
| 106 | +} |
| 107 | + |
| 108 | +/** @internal */ |
| 109 | +class FluidTransform extends FluidValue<string> { |
| 110 | + protected _value: string | null = null |
| 111 | + |
| 112 | + constructor(readonly inputs: Inputs, readonly transforms: Transforms) { |
| 113 | + super() |
| 114 | + } |
| 115 | + |
| 116 | + get() { |
| 117 | + return this._value || (this._value = this._get()) |
| 118 | + } |
| 119 | + |
| 120 | + protected _get() { |
| 121 | + let transform = '' |
| 122 | + let identity = true |
| 123 | + each(this.inputs, (input, i) => { |
| 124 | + const arg1 = getFluidValue(input[0]) |
| 125 | + const [t, id] = this.transforms[i]( |
| 126 | + is.arr(arg1) ? arg1 : input.map(getFluidValue) |
| 127 | + ) |
| 128 | + transform += ' ' + t |
| 129 | + identity = identity && id |
| 130 | + }) |
| 131 | + return identity ? 'none' : transform |
| 132 | + } |
| 133 | + |
| 134 | + // Start observing our inputs once we have an observer. |
| 135 | + protected observerAdded(count: number) { |
| 136 | + if (count == 1) |
| 137 | + each(this.inputs, input => |
| 138 | + each( |
| 139 | + input, |
| 140 | + value => hasFluidValue(value) && addFluidObserver(value, this) |
| 141 | + ) |
| 142 | + ) |
| 143 | + } |
| 144 | + |
| 145 | + // Stop observing our inputs once we have no observers. |
| 146 | + protected observerRemoved(count: number) { |
| 147 | + if (count == 0) |
| 148 | + each(this.inputs, input => |
| 149 | + each( |
| 150 | + input, |
| 151 | + value => hasFluidValue(value) && removeFluidObserver(value, this) |
| 152 | + ) |
| 153 | + ) |
| 154 | + } |
| 155 | + |
| 156 | + eventObserved(event: FluidEvent) { |
| 157 | + if (event.type == 'change') { |
| 158 | + this._value = null |
| 159 | + } |
| 160 | + callFluidObservers(this, event) |
| 161 | + } |
| 162 | +} |
0 commit comments