From c1575879f3d9a164f7e6e10eece222f07a17cffc Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 9 Jun 2019 15:16:28 +0200 Subject: [PATCH] =?UTF-8?q?feat(=F0=9F=8E=A8):=20New=20function=20for=20SV?= =?UTF-8?q?G=20Path=20Morphing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking Change: interpolation functions have been updated to be more symmetic. --- README.md | 37 +++++++++++++----- src/Colors.ts | 32 +++++++++++++--- src/SVG.ts | 104 ++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 132 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 069126a0..1071bcc4 100644 --- a/README.md +++ b/README.md @@ -53,15 +53,30 @@ const path = parsePath(d); const { y, x } = getPointAtLength(path, length); ``` -### `interpolatePath(path1, path2, progress): path` +### `interpolatePath(value: Node, { inputRange, outputRange }): path` + +Interpolate from one SVG point to the other, this function assumes that each path has the same number of points. + +```tsx + const phone1 = "M 18 149C 18 149 25 149 25 149 25 14..."; + const d = interpolatePath(slider, { + inputRange: [0, width, width * 2], + outputRange: [phone1, phone2, phone3] + }); + return ( + + + + ); +``` + +### `bInterpolatePath(path1, path2, progress): path` Interpolate from one SVG point to the other, this function assumes that each path has the same number of points. ```tsx const rhino = "M 217.765 29.683 C 225.855 29.683 "; -const rhinoPath = parsePath(rhino); const elephant = "M 223.174 43.413 ..."; -const elephantPath = parsePath(elephant); return ( <> @@ -78,7 +93,7 @@ return ( @@ -208,12 +223,12 @@ runDecay(clock: Clock, value: Node, velocity: Node, rerunDecaying: Node): Node Interpolate the node from 0 to 1 without clamping. -### `interpolateColors(node, inputRange, colors, [colorSpace = "hsv"])` +### `interpolateColor(node, { inputRange, outputRange }, [colorSpace = "hsv"])` Interpolate colors based on an animation value and its value range. ```js -interpolateColors(value: Node, inputRange: number[], colors: Colors, colorSpace?: "hsv" | "rgb") +interpolateColor(value: Node, { inputRange: number[], outputRange: Colors }, colorSpace?: "hsv" | "rgb") ``` Example Usage: @@ -231,14 +246,16 @@ const to = { }; // Interpolate in default color space (HSV) -interpolateColors(x, [0, 1], [from, to]); +interpolateColor(x, [0, 1], [from, to]); // Interpolate in RGB color space -interpolateColors(x, [0, 1], [from, to], "rgb"); +interpolateColor(x, [0, 1], [from, to], "rgb"); ``` -![](https://user-images.githubusercontent.com/616906/58366137-3d667b80-7ece-11e9-9b20-ea5e84494afc.png) -_Interpolating between red and blue, with in-between colors shown. Image source: [this tool](https://www.alanzucconi.com/2016/01/06/colour-interpolation/4/)._ + +### `bInterpolateColor(node, color1, color2, [colorSpace = "hsv"])` + +Interpolate the node from 0 to 1 without clamping. ### `snapPoint(point, velocity, points)` diff --git a/src/Colors.ts b/src/Colors.ts index 1ee07129..df58bfbd 100644 --- a/src/Colors.ts +++ b/src/Colors.ts @@ -162,13 +162,33 @@ const interpolateColorsRGB = ( return color(r, g, b); }; -export const interpolateColors = ( - animationValue: Animated.Adaptable, - inputRange: number[], - colors: RGBColor[], +interface ColorInterpolationConfig { + inputRange: number[]; + outputRange: RGBColor[]; +} + +export const interpolateColor = ( + value: Animated.Adaptable, + config: ColorInterpolationConfig, colorSpace: "hsv" | "rgb" = "hsv" ) => { + const { inputRange, outputRange } = config; if (colorSpace === "hsv") - return interpolateColorsHSV(animationValue, inputRange, colors); - return interpolateColorsRGB(animationValue, inputRange, colors); + return interpolateColorsHSV(value, inputRange, outputRange); + return interpolateColorsRGB(value, inputRange, outputRange); }; + +export const bInterpolateColor = ( + value: Animated.Adaptable, + color1: RGBColor, + color2: RGBColor, + colorSpace: "hsv" | "rgb" = "hsv" +) => + interpolateColor( + value, + { + inputRange: [0, 1], + outputRange: [color1, color2] + }, + colorSpace + ); diff --git a/src/SVG.ts b/src/SVG.ts index 8e156c80..be3bcfcf 100644 --- a/src/SVG.ts +++ b/src/SVG.ts @@ -5,7 +5,6 @@ import normalizeSVG from "normalize-svg-path"; import { find } from "./Arrays"; import { cubicBezier } from "./Math"; -import { bInterpolate } from "./Animations"; import cubicBezierLength from "./CubicBezierLength"; const { @@ -65,6 +64,7 @@ export interface ReanimatedPath { p3x: number[]; p3y: number[]; } + export const parsePath = (d: string): ReanimatedPath => { const [move, ...curves]: SVGNormalizedCommands = normalizeSVG( absSVG(parseSVG(d)) @@ -144,32 +144,86 @@ export const getPointAtLength = ( }; }; +const animatedString = ( + strings: ReadonlyArray>, + ...values: Animated.Adaptable[] +): Animated.Node => { + const arr = []; + const n = values.length; + for (let i = 0; i < n; i += 1) { + arr.push(strings[i], values[i]); + } + const end = strings[n]; + if (end) { + arr.push(end); + } + return concat(...(arr as any)); +}; + +interface PathInterpolationConfig { + inputRange: ReadonlyArray>; + outputRange: ReadonlyArray; +} + export const interpolatePath = ( - path1: ReanimatedPath, - path2: ReanimatedPath, - progress: Animated.Value + value: Animated.Adaptable, + config: PathInterpolationConfig ): Animated.Node => { - const commands = path1.segments.map((_, index) => { - const command: Animated.Node[] = []; - if (index === 0) { - const mx = bInterpolate(progress, path1.p0x[index], path2.p0x[index]); - const my = bInterpolate(progress, path1.p0y[index], path2.p0y[index]); - command.push(concat("M", mx, ",", my, " ")); - } - - const p1x = bInterpolate(progress, path1.p1x[index], path2.p1x[index]); - const p1y = bInterpolate(progress, path1.p1y[index], path2.p1y[index]); - - const p2x = bInterpolate(progress, path1.p2x[index], path2.p2x[index]); - const p2y = bInterpolate(progress, path1.p2y[index], path2.p2y[index]); - - const p3x = bInterpolate(progress, path1.p3x[index], path2.p3x[index]); - const p3y = bInterpolate(progress, path1.p3y[index], path2.p3y[index]); - - command.push( - concat("C", p1x, ",", p1y, " ", p2x, ",", p2y, " ", p3x, ",", p3y, " ") - ); - return concat(...command); + const { inputRange } = config; + const paths = config.outputRange.map(path => + typeof path === "string" ? parsePath(path) : path + ); + const path = paths[0]; + const commands = path.segments.map((_, index) => { + const mx = interpolate(value, { + inputRange, + outputRange: paths.map(p => p.p0x[index]) + }); + const my = interpolate(value, { + inputRange, + outputRange: paths.map(p => p.p0y[index]) + }); + + const p1x = interpolate(value, { + inputRange, + outputRange: paths.map(p => p.p1x[index]) + }); + const p1y = interpolate(value, { + inputRange, + outputRange: paths.map(p => p.p1y[index]) + }); + + const p2x = interpolate(value, { + inputRange, + outputRange: paths.map(p => p.p2x[index]) + }); + const p2y = interpolate(value, { + inputRange, + outputRange: paths.map(p => p.p2y[index]) + }); + + const p3x = interpolate(value, { + inputRange, + outputRange: paths.map(p => p.p3x[index]) + }); + const p3y = interpolate(value, { + inputRange, + outputRange: paths.map(p => p.p3y[index]) + }); + + return animatedString`${ + index === 0 ? animatedString`M${mx},${my} ` : "" + }C${p1x},${p1y} ${p2x},${p2y} ${p3x},${p3y}`; }); return concat(...commands); }; + +export const bInterpolatePath = ( + value: Animated.Value, + path1: ReanimatedPath, + path2: ReanimatedPath +): Animated.Node => + interpolatePath(value, { + inputRange: [0, 1], + outputRange: [path1, path2] + });