2015-07-07 20:34:09 +00:00
|
|
|
/**
|
|
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
|
|
* All rights reserved.
|
|
|
|
*
|
|
|
|
* This source code is licensed under the BSD-style license found in the
|
|
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
|
|
*
|
|
|
|
* @providesModule Interpolation
|
|
|
|
* @flow
|
|
|
|
*/
|
Reimplement color processing
Summary:
**Problem:**
As I was trying to document what color formats we supported, I realized that our current implementation based on the open source project tinycolor supported some crazy things. A few examples that were all valid:
```
tinycolor('abc')
tinycolor(' #abc ')
tinycolor('##abc')
tinycolor('rgb 255 0 0')
tinycolor('RGBA(0, 1, 2)')
tinycolor('rgb (0, 1, 2)')
tinycolor('hsv(0, 1, 2)')
tinycolor({r: 10, g: 10, b: 10})
tinycolor('hsl(1%, 2, 3)')
tinycolor('rgb(1.0, 2.0, 3.0)')
tinycolor('rgb(1%, 2%, 3%)')
```
The integrations of tinycolor were also really bad. processColor added "support" for pure numbers and an array of colors!?? ColorPropTypes did some crazy trim().toString() and repeated a bad error message twice.
**Solution:**
While iteratively cleaning the file, I eventually ended up reimplementing it entierly. Major changes are:
- The API is now dead simple: returns null if it doesn't parse or returns the int32 representation of the color
- Stricter parsing of at
Closes https://github.com/facebook/react-native/pull/5529
Reviewed By: svcscm
Differential Revision: D2872015
Pulled By: nicklockwood
fb-gh-sync-id: df78244eefce6cf8e8ed2ea51f58d6b232de16f9
2016-01-29 17:11:53 +00:00
|
|
|
/* eslint no-bitwise: 0 */
|
2015-07-07 20:34:09 +00:00
|
|
|
'use strict';
|
|
|
|
|
2016-03-02 15:06:50 +00:00
|
|
|
var invariant = require('fbjs/lib/invariant');
|
Reimplement color processing
Summary:
**Problem:**
As I was trying to document what color formats we supported, I realized that our current implementation based on the open source project tinycolor supported some crazy things. A few examples that were all valid:
```
tinycolor('abc')
tinycolor(' #abc ')
tinycolor('##abc')
tinycolor('rgb 255 0 0')
tinycolor('RGBA(0, 1, 2)')
tinycolor('rgb (0, 1, 2)')
tinycolor('hsv(0, 1, 2)')
tinycolor({r: 10, g: 10, b: 10})
tinycolor('hsl(1%, 2, 3)')
tinycolor('rgb(1.0, 2.0, 3.0)')
tinycolor('rgb(1%, 2%, 3%)')
```
The integrations of tinycolor were also really bad. processColor added "support" for pure numbers and an array of colors!?? ColorPropTypes did some crazy trim().toString() and repeated a bad error message twice.
**Solution:**
While iteratively cleaning the file, I eventually ended up reimplementing it entierly. Major changes are:
- The API is now dead simple: returns null if it doesn't parse or returns the int32 representation of the color
- Stricter parsing of at
Closes https://github.com/facebook/react-native/pull/5529
Reviewed By: svcscm
Differential Revision: D2872015
Pulled By: nicklockwood
fb-gh-sync-id: df78244eefce6cf8e8ed2ea51f58d6b232de16f9
2016-01-29 17:11:53 +00:00
|
|
|
var normalizeColor = require('normalizeColor');
|
2015-10-04 21:35:48 +00:00
|
|
|
|
2015-07-07 20:34:09 +00:00
|
|
|
type ExtrapolateType = 'extend' | 'identity' | 'clamp';
|
|
|
|
|
|
|
|
export type InterpolationConfigType = {
|
|
|
|
inputRange: Array<number>;
|
|
|
|
outputRange: (Array<number> | Array<string>);
|
|
|
|
easing?: ((input: number) => number);
|
|
|
|
extrapolate?: ExtrapolateType;
|
|
|
|
extrapolateLeft?: ExtrapolateType;
|
|
|
|
extrapolateRight?: ExtrapolateType;
|
|
|
|
};
|
|
|
|
|
|
|
|
var linear = (t) => t;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Very handy helper to map input ranges to output ranges with an easing
|
|
|
|
* function and custom behavior outside of the ranges.
|
|
|
|
*/
|
|
|
|
class Interpolation {
|
|
|
|
static create(config: InterpolationConfigType): (input: number) => number | string {
|
|
|
|
|
|
|
|
if (config.outputRange && typeof config.outputRange[0] === 'string') {
|
|
|
|
return createInterpolationFromStringOutputRange(config);
|
|
|
|
}
|
|
|
|
|
|
|
|
var outputRange: Array<number> = (config.outputRange: any);
|
|
|
|
checkInfiniteRange('outputRange', outputRange);
|
|
|
|
|
|
|
|
var inputRange = config.inputRange;
|
|
|
|
checkInfiniteRange('inputRange', inputRange);
|
|
|
|
checkValidInputRange(inputRange);
|
|
|
|
|
|
|
|
invariant(
|
|
|
|
inputRange.length === outputRange.length,
|
|
|
|
'inputRange (' + inputRange.length + ') and outputRange (' +
|
|
|
|
outputRange.length + ') must have the same length'
|
|
|
|
);
|
|
|
|
|
|
|
|
var easing = config.easing || linear;
|
|
|
|
|
|
|
|
var extrapolateLeft: ExtrapolateType = 'extend';
|
|
|
|
if (config.extrapolateLeft !== undefined) {
|
|
|
|
extrapolateLeft = config.extrapolateLeft;
|
|
|
|
} else if (config.extrapolate !== undefined) {
|
|
|
|
extrapolateLeft = config.extrapolate;
|
|
|
|
}
|
|
|
|
|
|
|
|
var extrapolateRight: ExtrapolateType = 'extend';
|
|
|
|
if (config.extrapolateRight !== undefined) {
|
|
|
|
extrapolateRight = config.extrapolateRight;
|
|
|
|
} else if (config.extrapolate !== undefined) {
|
|
|
|
extrapolateRight = config.extrapolate;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (input) => {
|
|
|
|
invariant(
|
|
|
|
typeof input === 'number',
|
|
|
|
'Cannot interpolation an input which is not a number'
|
|
|
|
);
|
|
|
|
|
|
|
|
var range = findRange(input, inputRange);
|
|
|
|
return interpolate(
|
|
|
|
input,
|
|
|
|
inputRange[range],
|
|
|
|
inputRange[range + 1],
|
|
|
|
outputRange[range],
|
|
|
|
outputRange[range + 1],
|
|
|
|
easing,
|
|
|
|
extrapolateLeft,
|
|
|
|
extrapolateRight,
|
|
|
|
);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function interpolate(
|
|
|
|
input: number,
|
|
|
|
inputMin: number,
|
|
|
|
inputMax: number,
|
|
|
|
outputMin: number,
|
|
|
|
outputMax: number,
|
|
|
|
easing: ((input: number) => number),
|
|
|
|
extrapolateLeft: ExtrapolateType,
|
|
|
|
extrapolateRight: ExtrapolateType,
|
|
|
|
) {
|
|
|
|
var result = input;
|
|
|
|
|
|
|
|
// Extrapolate
|
|
|
|
if (result < inputMin) {
|
|
|
|
if (extrapolateLeft === 'identity') {
|
|
|
|
return result;
|
|
|
|
} else if (extrapolateLeft === 'clamp') {
|
|
|
|
result = inputMin;
|
|
|
|
} else if (extrapolateLeft === 'extend') {
|
|
|
|
// noop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (result > inputMax) {
|
|
|
|
if (extrapolateRight === 'identity') {
|
|
|
|
return result;
|
|
|
|
} else if (extrapolateRight === 'clamp') {
|
|
|
|
result = inputMax;
|
|
|
|
} else if (extrapolateRight === 'extend') {
|
|
|
|
// noop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (outputMin === outputMax) {
|
|
|
|
return outputMin;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inputMin === inputMax) {
|
|
|
|
if (input <= inputMin) {
|
|
|
|
return outputMin;
|
|
|
|
}
|
|
|
|
return outputMax;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Input Range
|
|
|
|
if (inputMin === -Infinity) {
|
|
|
|
result = -result;
|
|
|
|
} else if (inputMax === Infinity) {
|
|
|
|
result = result - inputMin;
|
|
|
|
} else {
|
|
|
|
result = (result - inputMin) / (inputMax - inputMin);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Easing
|
|
|
|
result = easing(result);
|
|
|
|
|
|
|
|
// Output Range
|
|
|
|
if (outputMin === -Infinity) {
|
|
|
|
result = -result;
|
|
|
|
} else if (outputMax === Infinity) {
|
|
|
|
result = result + outputMin;
|
|
|
|
} else {
|
|
|
|
result = result * (outputMax - outputMin) + outputMin;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
Reimplement color processing
Summary:
**Problem:**
As I was trying to document what color formats we supported, I realized that our current implementation based on the open source project tinycolor supported some crazy things. A few examples that were all valid:
```
tinycolor('abc')
tinycolor(' #abc ')
tinycolor('##abc')
tinycolor('rgb 255 0 0')
tinycolor('RGBA(0, 1, 2)')
tinycolor('rgb (0, 1, 2)')
tinycolor('hsv(0, 1, 2)')
tinycolor({r: 10, g: 10, b: 10})
tinycolor('hsl(1%, 2, 3)')
tinycolor('rgb(1.0, 2.0, 3.0)')
tinycolor('rgb(1%, 2%, 3%)')
```
The integrations of tinycolor were also really bad. processColor added "support" for pure numbers and an array of colors!?? ColorPropTypes did some crazy trim().toString() and repeated a bad error message twice.
**Solution:**
While iteratively cleaning the file, I eventually ended up reimplementing it entierly. Major changes are:
- The API is now dead simple: returns null if it doesn't parse or returns the int32 representation of the color
- Stricter parsing of at
Closes https://github.com/facebook/react-native/pull/5529
Reviewed By: svcscm
Differential Revision: D2872015
Pulled By: nicklockwood
fb-gh-sync-id: df78244eefce6cf8e8ed2ea51f58d6b232de16f9
2016-01-29 17:11:53 +00:00
|
|
|
function colorToRgba(input: string): string {
|
|
|
|
var int32Color = normalizeColor(input);
|
|
|
|
if (int32Color === null) {
|
2015-10-04 21:35:48 +00:00
|
|
|
return input;
|
|
|
|
}
|
Reimplement color processing
Summary:
**Problem:**
As I was trying to document what color formats we supported, I realized that our current implementation based on the open source project tinycolor supported some crazy things. A few examples that were all valid:
```
tinycolor('abc')
tinycolor(' #abc ')
tinycolor('##abc')
tinycolor('rgb 255 0 0')
tinycolor('RGBA(0, 1, 2)')
tinycolor('rgb (0, 1, 2)')
tinycolor('hsv(0, 1, 2)')
tinycolor({r: 10, g: 10, b: 10})
tinycolor('hsl(1%, 2, 3)')
tinycolor('rgb(1.0, 2.0, 3.0)')
tinycolor('rgb(1%, 2%, 3%)')
```
The integrations of tinycolor were also really bad. processColor added "support" for pure numbers and an array of colors!?? ColorPropTypes did some crazy trim().toString() and repeated a bad error message twice.
**Solution:**
While iteratively cleaning the file, I eventually ended up reimplementing it entierly. Major changes are:
- The API is now dead simple: returns null if it doesn't parse or returns the int32 representation of the color
- Stricter parsing of at
Closes https://github.com/facebook/react-native/pull/5529
Reviewed By: svcscm
Differential Revision: D2872015
Pulled By: nicklockwood
fb-gh-sync-id: df78244eefce6cf8e8ed2ea51f58d6b232de16f9
2016-01-29 17:11:53 +00:00
|
|
|
|
|
|
|
int32Color = int32Color || 0; // $FlowIssue
|
|
|
|
|
2016-02-08 12:48:29 +00:00
|
|
|
var r = (int32Color & 0xff000000) >>> 24;
|
|
|
|
var g = (int32Color & 0x00ff0000) >>> 16;
|
|
|
|
var b = (int32Color & 0x0000ff00) >>> 8;
|
|
|
|
var a = (int32Color & 0x000000ff) / 255;
|
Reimplement color processing
Summary:
**Problem:**
As I was trying to document what color formats we supported, I realized that our current implementation based on the open source project tinycolor supported some crazy things. A few examples that were all valid:
```
tinycolor('abc')
tinycolor(' #abc ')
tinycolor('##abc')
tinycolor('rgb 255 0 0')
tinycolor('RGBA(0, 1, 2)')
tinycolor('rgb (0, 1, 2)')
tinycolor('hsv(0, 1, 2)')
tinycolor({r: 10, g: 10, b: 10})
tinycolor('hsl(1%, 2, 3)')
tinycolor('rgb(1.0, 2.0, 3.0)')
tinycolor('rgb(1%, 2%, 3%)')
```
The integrations of tinycolor were also really bad. processColor added "support" for pure numbers and an array of colors!?? ColorPropTypes did some crazy trim().toString() and repeated a bad error message twice.
**Solution:**
While iteratively cleaning the file, I eventually ended up reimplementing it entierly. Major changes are:
- The API is now dead simple: returns null if it doesn't parse or returns the int32 representation of the color
- Stricter parsing of at
Closes https://github.com/facebook/react-native/pull/5529
Reviewed By: svcscm
Differential Revision: D2872015
Pulled By: nicklockwood
fb-gh-sync-id: df78244eefce6cf8e8ed2ea51f58d6b232de16f9
2016-01-29 17:11:53 +00:00
|
|
|
|
|
|
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
2015-10-04 21:35:48 +00:00
|
|
|
}
|
|
|
|
|
2015-07-07 20:34:09 +00:00
|
|
|
var stringShapeRegex = /[0-9\.-]+/g;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Supports string shapes by extracting numbers so new values can be computed,
|
|
|
|
* and recombines those values into new strings of the same shape. Supports
|
|
|
|
* things like:
|
|
|
|
*
|
|
|
|
* rgba(123, 42, 99, 0.36) // colors
|
|
|
|
* -45deg // values with units
|
|
|
|
*/
|
|
|
|
function createInterpolationFromStringOutputRange(
|
|
|
|
config: InterpolationConfigType,
|
|
|
|
): (input: number) => string {
|
|
|
|
var outputRange: Array<string> = (config.outputRange: any);
|
|
|
|
invariant(outputRange.length >= 2, 'Bad output range');
|
2015-10-04 21:35:48 +00:00
|
|
|
outputRange = outputRange.map(colorToRgba);
|
2015-07-07 20:34:09 +00:00
|
|
|
checkPattern(outputRange);
|
|
|
|
|
|
|
|
// ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)']
|
|
|
|
// ->
|
|
|
|
// [
|
|
|
|
// [0, 50],
|
|
|
|
// [100, 150],
|
|
|
|
// [200, 250],
|
|
|
|
// [0, 0.5],
|
|
|
|
// ]
|
2015-10-26 16:17:50 +00:00
|
|
|
/* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to
|
|
|
|
* guard against this possibility.
|
|
|
|
*/
|
2015-07-07 20:34:09 +00:00
|
|
|
var outputRanges = outputRange[0].match(stringShapeRegex).map(() => []);
|
|
|
|
outputRange.forEach(value => {
|
2015-10-26 16:17:50 +00:00
|
|
|
/* $FlowFixMe(>=0.18.0): `value.match()` can return `null`. Need to guard
|
|
|
|
* against this possibility.
|
|
|
|
*/
|
2015-07-07 20:34:09 +00:00
|
|
|
value.match(stringShapeRegex).forEach((number, i) => {
|
|
|
|
outputRanges[i].push(+number);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2015-10-26 16:17:50 +00:00
|
|
|
/* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to
|
|
|
|
* guard against this possibility.
|
|
|
|
*/
|
2015-07-07 20:34:09 +00:00
|
|
|
var interpolations = outputRange[0].match(stringShapeRegex).map((value, i) => {
|
|
|
|
return Interpolation.create({
|
|
|
|
...config,
|
|
|
|
outputRange: outputRanges[i],
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
return (input) => {
|
|
|
|
var i = 0;
|
|
|
|
// 'rgba(0, 100, 200, 0)'
|
|
|
|
// ->
|
|
|
|
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
|
|
|
|
return outputRange[0].replace(stringShapeRegex, () => {
|
|
|
|
return String(interpolations[i++](input));
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkPattern(arr: Array<string>) {
|
|
|
|
var pattern = arr[0].replace(stringShapeRegex, '');
|
|
|
|
for (var i = 1; i < arr.length; ++i) {
|
|
|
|
invariant(
|
|
|
|
pattern === arr[i].replace(stringShapeRegex, ''),
|
|
|
|
'invalid pattern ' + arr[0] + ' and ' + arr[i],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function findRange(input: number, inputRange: Array<number>) {
|
|
|
|
for (var i = 1; i < inputRange.length - 1; ++i) {
|
|
|
|
if (inputRange[i] >= input) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return i - 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkValidInputRange(arr: Array<number>) {
|
|
|
|
invariant(arr.length >= 2, 'inputRange must have at least 2 elements');
|
|
|
|
for (var i = 1; i < arr.length; ++i) {
|
|
|
|
invariant(
|
|
|
|
arr[i] >= arr[i - 1],
|
|
|
|
/* $FlowFixMe(>=0.13.0) - In the addition expression below this comment,
|
|
|
|
* one or both of the operands may be something that doesn't cleanly
|
|
|
|
* convert to a string, like undefined, null, and object, etc. If you really
|
|
|
|
* mean this implicit string conversion, you can do something like
|
|
|
|
* String(myThing)
|
|
|
|
*/
|
2015-07-09 21:30:14 +00:00
|
|
|
'inputRange must be monotonically increasing ' + arr
|
2015-07-07 20:34:09 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function checkInfiniteRange(name: string, arr: Array<number>) {
|
|
|
|
invariant(arr.length >= 2, name + ' must have at least 2 elements');
|
|
|
|
invariant(
|
|
|
|
arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity,
|
|
|
|
/* $FlowFixMe(>=0.13.0) - In the addition expression below this comment,
|
|
|
|
* one or both of the operands may be something that doesn't cleanly convert
|
|
|
|
* to a string, like undefined, null, and object, etc. If you really mean
|
|
|
|
* this implicit string conversion, you can do something like
|
|
|
|
* String(myThing)
|
|
|
|
*/
|
|
|
|
name + 'cannot be ]-infinity;+infinity[ ' + arr
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Interpolation;
|