UNPKG

react-native-redash

Version:

Utility library for React Native Reanimated

236 lines (216 loc) 6.25 kB
import Animated from "react-native-reanimated"; import parseSVG from "parse-svg-path"; import absSVG from "abs-svg-path"; import normalizeSVG from "normalize-svg-path"; import { get } from "./Array"; import { string } from "./String"; import { cubicBezier } from "./Math"; import cubicBezierLength from "./bezier/CubicBezierLength"; import cubicBezierSolve from "./bezier/CubicBezierSolve"; const { Value, lessOrEq, greaterOrEq, and, cond, interpolate, multiply, lessThan, add } = Animated; // const COMMAND = 0; const MX = 1; const MY = 2; const CX1 = 1; const CY1 = 2; const CX2 = 3; const CY2 = 4; const CX = 5; const CY = 6; type SVGMoveCommand = ["M", number, number]; type SVGCurveCommand = ["C", number, number, number, number, number, number]; type SVGNormalizedCommands = [SVGMoveCommand, ...SVGCurveCommand[]]; type BezierPoint = | "p0x" | "p0y" | "p1x" | "p1y" | "p2x" | "p2y" | "p3x" | "p3y"; interface Point { x: number; y: number; } interface BezierCubicCurve { length: number; p0: Point; p1: Point; p2: Point; p3: Point; } export interface PathInterpolationConfig { inputRange: readonly Animated.Adaptable<number>[]; outputRange: readonly (ReanimatedPath | string)[]; extrapolate?: Animated.Extrapolate; extrapolateLeft?: Animated.Extrapolate; extrapolateRight?: Animated.Extrapolate; } export interface ReanimatedPath { totalLength: number; segments: { start: number; end: number; p0x: number; p3x: number }[]; length: number[]; start: number[]; end: number[]; p0x: number[]; p0y: number[]; p1x: number[]; p1y: number[]; p2x: number[]; p2y: number[]; p3x: number[]; p3y: number[]; } export const parsePath = (d: string): ReanimatedPath => { const [move, ...curves]: SVGNormalizedCommands = normalizeSVG( absSVG(parseSVG(d)) ); const parts: BezierCubicCurve[] = curves.map((curve, index) => { const prevCurve = curves[index - 1]; const p0 = index === 0 ? { x: move[MX], y: move[MY] } : { x: prevCurve[CX], y: prevCurve[CY] }; const p1 = { x: curve[CX1], y: curve[CY1] }; const p2 = { x: curve[CX2], y: curve[CY2] }; const p3 = { x: curve[CX], y: curve[CY] }; const length = cubicBezierLength(p0, p1, p2, p3); return { p0, p1, p2, p3, length }; }); const segments = parts.map((part, index) => { const start = parts.slice(0, index).reduce((acc, p) => acc + p.length, 0); const end = start + part.length; return { start, end, p0x: part.p0.x, p3x: part.p3.x }; }); return { segments, totalLength: parts.reduce((acc, part) => acc + part.length, 0), length: parts.map(part => part.length), start: segments.map(segment => segment.start), end: segments.map(segment => segment.end), p0x: parts.map(part => part.p0.x), p0y: parts.map(part => part.p0.y), p1x: parts.map(part => part.p1.x), p1y: parts.map(part => part.p1.y), p2x: parts.map(part => part.p2.x), p2y: parts.map(part => part.p2.y), p3x: parts.map(part => part.p3.x), p3y: parts.map(part => part.p3.y) }; }; export const getPointAtLength = ( path: ReanimatedPath, length: Animated.Adaptable<number> ): { x: Animated.Node<number>; y: Animated.Node<number> } => { const notFound: Animated.Node<number> = new Value(-1); const index = path.segments.reduce( (acc, p, i) => cond(and(greaterOrEq(length, p.start), lessOrEq(length, p.end)), i, acc), notFound ); const start = get(path.start, index); const end = get(path.end, index); const p0x = get(path.p0x, index); const p1x = get(path.p1x, index); const p2x = get(path.p2x, index); const p3x = get(path.p3x, index); const p0y = get(path.p0y, index); const p1y = get(path.p1y, index); const p2y = get(path.p2y, index); const p3y = get(path.p3y, index); const t = interpolate(length, { inputRange: [start, end], outputRange: [0, 1] }); return { x: cubicBezier(t, p0x, p1x, p2x, p3x), y: cubicBezier(t, p0y, p1y, p2y, p3y) }; }; export const interpolatePath = ( value: Animated.Adaptable<number>, { inputRange, outputRange, ...config }: PathInterpolationConfig ): Animated.Node<string> => { const paths = outputRange.map(path => typeof path === "string" ? parsePath(path) : path ); const path = paths[0]; const commands = path.segments.map((_, index) => { const interpolatePoint = (point: BezierPoint) => interpolate(value, { inputRange, outputRange: paths.map(p => p[point][index]), ...config }); const mx = interpolatePoint("p0x"); const my = interpolatePoint("p0y"); const p1x = interpolatePoint("p1x"); const p1y = interpolatePoint("p1y"); const p2x = interpolatePoint("p2x"); const p2y = interpolatePoint("p2y"); const p3x = interpolatePoint("p3x"); const p3y = interpolatePoint("p3y"); return string`${ index === 0 ? string`M${mx},${my} ` : "" }C${p1x},${p1y} ${p2x},${p2y} ${p3x},${p3y}`; }); return string`${commands}`; }; export const bInterpolatePath = ( value: Animated.Value<number>, path1: ReanimatedPath | string, path2: ReanimatedPath | string ): Animated.Node<string> => interpolatePath(value, { inputRange: [0, 1], outputRange: [path1, path2] }); // https://pomax.github.io/bezierinfo/#yforx export const getLengthAtX = ( path: ReanimatedPath, x: Animated.Adaptable<number> ): Animated.Node<number> => { const notFound: Animated.Node<number> = new Value(-1); const index = path.segments.reduce( (acc, p, i) => cond(and(greaterOrEq(x, p.p0x), lessOrEq(x, p.p3x)), i, acc), notFound ); const p0 = get(path.p0x, index); const p1 = get(path.p1x, index); const p2 = get(path.p2x, index); const p3 = get(path.p3x, index); const t = cubicBezierSolve(p0, p1, p2, p3); const length = get(path.length, index); /* eslint-disable @typescript-eslint/no-explicit-any */ const start = add( ...(path.length.map((l, i) => cond(lessThan(i, index), l, 0)) as [ any, any, ...any[] ]) ); /* eslint-enable @typescript-eslint/no-explicit-any */ return add(start, multiply(t, length)); };