Skip to content

Commit

Permalink
feat(🎨): New function for SVG Path Morphing
Browse files Browse the repository at this point in the history
Breaking Change: interpolation functions have been updated to be more symmetic.
  • Loading branch information
wcandillon authored Jun 9, 2019
1 parent 89622b0 commit c157587
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 41 deletions.
37 changes: 27 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Svg style={styles.svg} viewBox="0 0 100 300">
<AnimatedPath fill="black" {...{ d }} />
</Svg>
);
```

### `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 (
<>
<Animated.Code>
Expand All @@ -78,7 +93,7 @@ return (
</Animated.Code>
<Svg style={styles.container} viewBox="0 0 409 280">
<AnimatedPath
d={interpolatePath(rhinoPath, elephantPath, progress)}
d={bInterpolatePath(progress, rhino, elephant)}
fill="#7d8f9b"
/>
</Svg>
Expand Down Expand Up @@ -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:
Expand All @@ -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)`

Expand Down
32 changes: 26 additions & 6 deletions src/Colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,33 @@ const interpolateColorsRGB = (
return color(r, g, b);
};

export const interpolateColors = (
animationValue: Animated.Adaptable<number>,
inputRange: number[],
colors: RGBColor[],
interface ColorInterpolationConfig {
inputRange: number[];
outputRange: RGBColor[];
}

export const interpolateColor = (
value: Animated.Adaptable<number>,
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<number>,
color1: RGBColor,
color2: RGBColor,
colorSpace: "hsv" | "rgb" = "hsv"
) =>
interpolateColor(
value,
{
inputRange: [0, 1],
outputRange: [color1, color2]
},
colorSpace
);
104 changes: 79 additions & 25 deletions src/SVG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -144,32 +144,86 @@ export const getPointAtLength = (
};
};

const animatedString = (
strings: ReadonlyArray<Animated.Adaptable<string>>,
...values: Animated.Adaptable<string | number>[]
): Animated.Node<string> => {
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<Animated.Adaptable<number>>;
outputRange: ReadonlyArray<ReanimatedPath | string>;
}

export const interpolatePath = (
path1: ReanimatedPath,
path2: ReanimatedPath,
progress: Animated.Value<number>
value: Animated.Adaptable<number>,
config: PathInterpolationConfig
): Animated.Node<string> => {
const commands = path1.segments.map((_, index) => {
const command: Animated.Node<string>[] = [];
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<number>,
path1: ReanimatedPath,
path2: ReanimatedPath
): Animated.Node<string> =>
interpolatePath(value, {
inputRange: [0, 1],
outputRange: [path1, path2]
});

0 comments on commit c157587

Please sign in to comment.