react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
154 lines (131 loc) • 4.78 kB
text/typescript
import { logger, ReanimatedError } from '../../common';
import { PERCENTAGE_REGEX } from '../constants';
import type { Point } from '../types';
import type {
ControlPoint,
NormalizedLinearEasing,
ParametrizedTimingFunction,
} from './types';
export const ERROR_MESSAGES = {
invalidPointsCount: () =>
`Invalid linear easing points count. There should be at least two points`,
invalidInputProgressValue: (inputProgress: string | number) =>
`Invalid input progress ${inputProgress} value, it should be a percentage between 0% and 100%`,
};
export const WARN_MESSAGES = {
inputProgressLessThanPrecedingPoint: (x: number, precedingX: number) =>
`Linear easing point x value ${x} is less than value of the preceding control point ${precedingX}. Value will be overridden by ${precedingX}`,
};
const parsePercentage = (percentage: string | number): number => {
let result: number | undefined;
if (typeof percentage === 'number') {
result = percentage;
} else if (PERCENTAGE_REGEX.test(percentage)) {
result = parseFloat(percentage) / 100;
}
if (result === undefined || result < 0 || result > 1) {
throw new ReanimatedError(
ERROR_MESSAGES.invalidInputProgressValue(percentage)
);
}
return result;
};
const extrapolate = (x: number, point1: Point, point2: Point) => {
const slope = (point2.y - point1.y) / (point2.x - point1.x);
return point1.y + slope * (x - point1.x);
};
export class LinearEasing implements ParametrizedTimingFunction {
static readonly easingName = 'linear';
readonly points: ControlPoint[];
constructor(points: ControlPoint[]) {
if (points.length < 2) {
throw new ReanimatedError(ERROR_MESSAGES.invalidPointsCount());
}
this.points = points.map((p) =>
Array.isArray(p) && p.length === 1 ? p[0] : p
);
}
toString(): string {
return `${LinearEasing.easingName}(${this.points
.map((point) =>
Array.isArray(point)
? `[${point.map((p) => (typeof p === 'string' ? `"${p}"` : p)).join(', ')}]`
: point
)
.join(', ')})`;
}
normalize(): NormalizedLinearEasing {
const points = this.canonicalize();
// Extrapolate points if the input progress of the first one is greater than 0
// or the input progress of the last one is less than 1
if (points[0].x > 0) {
points.unshift({ x: 0, y: extrapolate(0, points[0], points[1]) });
}
if (points[points.length - 1].x < 1) {
points.push({
x: 1,
y: extrapolate(1, points[points.length - 2], points[points.length - 1]),
});
}
return { name: LinearEasing.easingName, points };
}
private canonicalize() {
const result = this.points.flatMap<{ x?: number; y: number }>((point) =>
Array.isArray(point)
? point.slice(1).map((x) => ({ x: parsePercentage(x), y: point[0] }))
: [{ y: point }]
);
// 1. If the first control point lacks an input progress value,
// set its input progress value to 0.
if (result[0].x === undefined) {
result[0].x = 0;
}
// 2.If the last control point lacks an input progress value,
// set its input progress value to 1.
if (result[result.length - 1].x === undefined) {
result[result.length - 1].x = 1;
}
// 3. If any control point has an input progress value that is less
// than the input progress value of any preceding control point, set
// its input progress value to the largest input progress value of
// any preceding control point.
let maxPrecedingX = 0;
for (let i = 1; i < result.length - 1; i++) {
const x = result[i].x;
if (x !== undefined) {
if (x < maxPrecedingX) {
logger.warn(
WARN_MESSAGES.inputProgressLessThanPrecedingPoint(x, maxPrecedingX)
);
result[i].x = maxPrecedingX;
} else {
maxPrecedingX = x;
}
}
}
// 4. If any control point still lacks an input progress value, then
// for each contiguous run of such control points, set their input
// progress values so that they are evenly spaced between the preceding
// and following control points with input progress values.
let precedingX = result[0].x;
let missingCount = 0;
for (let i = 1; i < result.length; i++) {
const x = result[i].x;
if (x === undefined) {
missingCount++;
continue;
}
if (missingCount > 0) {
const range = x - precedingX;
for (let j = 0; j < missingCount; j++) {
result[i - missingCount + j].x =
precedingX + (range * (j + 1)) / (missingCount + 1);
}
}
precedingX = x;
missingCount = 0;
}
return result as Point[];
}
}
;