From af87fea6363c2bbaae1ab56c305dd0eea533f97d Mon Sep 17 00:00:00 2001 From: William Candillon Date: Mon, 19 Oct 2020 06:57:24 +0200 Subject: [PATCH] =?UTF-8?q?feat(=F0=9F=93=89):=20Improvements=20to=20the?= =?UTF-8?q?=20Path=20API=20(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/Colors.ts | 33 +--- src/Paths.ts | 315 +++++++++++++++--------------------- src/__tests__/Paths.test.ts | 2 +- yarn.lock | 7 +- 5 files changed, 148 insertions(+), 211 deletions(-) diff --git a/package.json b/package.json index 9d0559a7..e99d9d67 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "react-native-reanimated": "2.0.0-alpha.6", "semantic-release": "^15.13.3", "semantic-release-cli": "^4.1.2", - "typescript": "^3.6.2" + "typescript": "4.0.3" }, "react-native": "lib/module/index.js", "module": "lib/module/index.js", diff --git a/src/Colors.ts b/src/Colors.ts index 40b5e731..f8eebace 100644 --- a/src/Colors.ts +++ b/src/Colors.ts @@ -1,4 +1,3 @@ -import { Platform } from "react-native"; import { interpolate, Extrapolate, @@ -7,15 +6,12 @@ import { import { clamp, mix } from "./Math"; -declare let _WORKLET: boolean; - /** * @summary TypeScript type to define an animation value as color. * @example // Color can be of string or number depending of the context in which it was executed const color: Animated.SharedValue = useDerivedValue(() => mixColor(progress.value, "blue", "red")); */ -export type Color = string | number; export enum ColorSpace { RGB, HSV, @@ -46,22 +42,9 @@ export const blue = (c: number) => { return c & 255; }; -export const color = (r: number, g: number, b: number, alpha = 1): Color => { +export const color = (r: number, g: number, b: number, alpha = 1): string => { "worklet"; - if (Platform.OS === "web" || !_WORKLET) { - return `rgba(${r}, ${g}, ${b}, ${alpha})`; - } - const a = alpha * 255; - const c = - a * (1 << 24) + - Math.round(r) * (1 << 16) + - Math.round(g) * (1 << 8) + - Math.round(b); - if (Platform.OS === "android") { - // on Android color is represented as signed 32 bit int - return c < (1 << 31) >>> 0 ? c : c - Math.pow(2, 32); - } - return c; + return `rgba(${r}, ${g}, ${b}, ${alpha})`; }; /** @@ -98,7 +81,7 @@ export const hsv2rgb = (h: number, s: number, v: number) => { /** * @summary Convert HSV to RGB */ -export const hsv2color = (h: number, s: number, v: number): Color => { +export const hsv2color = (h: number, s: number, v: number) => { "worklet"; const { r, g, b } = hsv2rgb(h, s, v); return color(r, g, b); @@ -233,9 +216,9 @@ const interpolateColorsRGB = ( export const interpolateColor = ( value: number, inputRange: number[], - rawOutputRange: Color[], + rawOutputRange: string[], colorSpace: ColorSpace = ColorSpace.RGB -): Color => { +) => { "worklet"; const outputRange = rawOutputRange.map((c) => typeof c === "number" ? c : processColor(c) @@ -256,10 +239,10 @@ export const interpolateColor = ( */ export const mixColor = ( value: number, - color1: Color, - color2: Color, + color1: string, + color2: string, colorSpace: ColorSpace = ColorSpace.RGB -): Color => { +) => { "worklet"; return interpolateColor(value, [0, 1], [color1, color2], colorSpace); }; diff --git a/src/Paths.ts b/src/Paths.ts index d290e0a4..c4df965e 100644 --- a/src/Paths.ts +++ b/src/Paths.ts @@ -10,67 +10,20 @@ type SVGCloseCommand = ["Z"]; type SVGMoveCommand = ["M", number, number]; type SVGCurveCommand = ["C", number, number, number, number, number, number]; type SVGNormalizedCommands = [ - SVGMoveCommand | SVGCurveCommand | SVGCloseCommand + SVGMoveCommand, + ...(SVGCurveCommand | SVGCloseCommand)[] ]; -export enum SVGCommand { - MOVE, - CURVE, - CLOSE, -} - -interface Move extends Vector { - type: SVGCommand.MOVE; -} - interface Curve { - type: SVGCommand.CURVE; - from: Vector; to: Vector; c1: Vector; c2: Vector; } -interface Close { - type: SVGCommand.CLOSE; -} - -export type Segment = Close | Curve | Move; -export type Path = Segment[]; - -export const exhaustiveCheck = (command: never): never => { - "worklet"; - throw new TypeError(`Unknown SVG Command: ${command}`); -}; - -const serializeMove = (c: Move) => { - "worklet"; - return `M${c.x},${c.y} `; -}; - -const serializeClose = () => { - "worklet"; - return "Z"; -}; - -const serializeCurve = (c: Curve) => { - "worklet"; - return `C${c.c1.x},${c.c1.y} ${c.c2.x},${c.c2.y} ${c.to.x},${c.to.y} `; -}; - -const isMove = (command: Segment): command is Move => { - "worklet"; - return command.type === SVGCommand.MOVE; -}; - -const isCurve = (command: Segment): command is Curve => { - "worklet"; - return command.type === SVGCommand.CURVE; -}; - -const isClose = (command: Segment): command is Close => { - "worklet"; - return command.type === SVGCommand.CLOSE; +export type Path = { + move: Vector; + curves: Curve[]; + close: boolean; }; /** @@ -78,20 +31,9 @@ const isClose = (command: Segment): command is Close => { */ export const serialize = (path: Path) => { "worklet"; - return path - .map((segment) => { - if (isMove(segment)) { - return serializeMove(segment); - } - if (isCurve(segment)) { - return serializeCurve(segment); - } - if (isClose(segment)) { - return serializeClose(); - } - return exhaustiveCheck(segment); - }) - .reduce((acc, c) => acc + c); + return `M${path.move.x},${path.move.y} ${path.curves + .map((c) => `C${c.c1.x},${c.c1.y} ${c.c2.x},${c.c2.y} ${c.to.x},${c.to.y} `) + .reduce((acc, c) => acc + c)}${path.close ? "Z" : ""}`; }; /** @@ -101,14 +43,12 @@ export const serialize = (path: Path) => { */ export const parse = (d: string): Path => { const segments: SVGNormalizedCommands = normalizeSVG(absSVG(parseSVG(d))); - return segments.map((segment, index) => { - if (segment[0] === "M") { - return moveTo(segment[1], segment[2]); - } else if (segment[0] === "Z") { - return close(); - } else { - const prev = segments[index - 1]; - const r = curve({ + const path = createPath({ x: segments[0][1], y: segments[0][2] }); + segments.forEach((segment) => { + if (segment[0] === "Z") { + close(path); + } else if (segment[0] === "C") { + addCurve(path, { c1: { x: segment[1], y: segment[2], @@ -121,14 +61,10 @@ export const parse = (d: string): Path => { x: segment[5], y: segment[6], }, - from: { - x: (prev[0] === "M" ? prev[1] : prev[5]) ?? 0, - y: (prev[0] === "M" ? prev[2] : prev[6]) ?? 0, - }, }); - return r; } }); + return path; }; /** @@ -141,94 +77,67 @@ export const interpolatePath = ( extrapolate = Animated.Extrapolate.CLAMP ) => { "worklet"; - const path = outputRange[0].map((segment, index) => { - if (isMove(segment)) { - const points = outputRange.map((p) => { - const s = p[index]; - if (isMove(s)) { - return { - x: s.x, - y: s.y, - }; - } - throw new Error("Paths to interpolate are not symetrical"); - }); - return { - type: SVGCommand.MOVE, + const path = { + move: { + x: interpolate( + value, + inputRange, + outputRange.map((p) => p.move.x), + extrapolate + ), + y: interpolate( + value, + inputRange, + outputRange.map((p) => p.move.y), + extrapolate + ), + }, + curves: outputRange[0].curves.map((_, index) => ({ + c1: { x: interpolate( value, inputRange, - points.map((p) => p.x), + outputRange.map((p) => p.curves[index].c1.x), extrapolate ), y: interpolate( value, inputRange, - points.map((p) => p.y), + outputRange.map((p) => p.curves[index].c1.y), extrapolate ), - } as Move; - } - if (isCurve(segment)) { - const curves = outputRange.map((p) => { - const s = p[index]; - if (isCurve(s)) { - return { - to: s.to, - c1: s.c1, - c2: s.c2, - }; - } - throw new Error("Paths to interpolate are not symetrical"); - }); - return { - type: SVGCommand.CURVE, - to: { - x: interpolate( - value, - inputRange, - curves.map((c) => c.to.x), - extrapolate - ), - y: interpolate( - value, - inputRange, - curves.map((c) => c.to.y), - extrapolate - ), - }, - c1: { - x: interpolate( - value, - inputRange, - curves.map((c) => c.c1.x), - extrapolate - ), - y: interpolate( - value, - inputRange, - curves.map((c) => c.c1.y), - extrapolate - ), - }, - c2: { - x: interpolate( - value, - inputRange, - curves.map((c) => c.c2.x), - extrapolate - ), - y: interpolate( - value, - inputRange, - curves.map((c) => c.c2.y), - extrapolate - ), - }, - } as Curve; - } - return segment; - }); + }, + c2: { + x: interpolate( + value, + inputRange, + outputRange.map((p) => p.curves[index].c2.x), + extrapolate + ), + y: interpolate( + value, + inputRange, + outputRange.map((p) => p.curves[index].c2.y), + extrapolate + ), + }, + to: { + x: interpolate( + value, + inputRange, + outputRange.map((p) => p.curves[index].to.x), + extrapolate + ), + y: interpolate( + value, + inputRange, + outputRange.map((p) => p.curves[index].to.y), + extrapolate + ), + }, + })), + close: outputRange[0].close, + }; return serialize(path); }; @@ -246,33 +155,77 @@ export const mixPath = ( }; /** - * @summary Returns a Bèzier curve command. + * @summary Create a new path */ -export const moveTo = (x: number, y: number) => { +export const createPath = (move: Vector): Path => { "worklet"; - return { type: SVGCommand.MOVE as const, x, y }; + return { + move, + curves: [], + close: false, + }; }; /** - * @summary Returns a Bèzier curve command + * @summary Add a Bèzier curve command to a path. */ -export const curve = (c: Omit) => { +export const addCurve = (path: Path, c: Curve) => { "worklet"; - return { - type: SVGCommand.CURVE as const, + path.curves.push({ c1: c.c1, c2: c.c2, to: c.to, - from: c.from, - }; + }); }; /** - * @summary Returns a close command. + * @summary Add a close command to a path. */ -export const close = () => { +export const close = (path: Path) => { "worklet"; - return { type: SVGCommand.CLOSE as const }; + path.close = true; +}; + +interface SelectedCurve { + from: Vector; + curve: Curve; +} + +interface NullableSelectedCurve { + from: Vector; + curve: Curve | null; +} + +const curveIsFound = (c: NullableSelectedCurve): c is SelectedCurve => { + "worklet"; + return c.curve !== null; +}; + +/** + * @summary Return the curves at x. This function assumes that only one curve is available at x + */ +export const selectCurve = (path: Path, x: number): SelectedCurve => { + "worklet"; + const result: NullableSelectedCurve = { + from: path.move, + curve: null, + }; + for (let i = 0; i < path.curves.length; i++) { + const c = path.curves[i]; + const contains = + result.from.x > c.to.x + ? x >= c.to.x && x <= result.from.x + : x >= result.from.x && x <= c.to.x; + if (contains) { + result.curve = c; + break; + } + result.from = c.to; + } + if (!curveIsFound(result)) { + throw new Error(`No curve found at ${x}`); + } + return result; }; /** @@ -288,17 +241,13 @@ export const close = () => { */ export const getYForX = (path: Path, x: number, precision = 2) => { "worklet"; - const p = path.filter((c) => { - if (isCurve(c)) { - if (c.from.x > c.to.x) { - return x >= c.to.x && x <= c.from.x; - } - return x >= c.from.x && x <= c.to.x; - } - return false; - }); - if (p.length > 0 && isCurve(p[0])) { - return cubicBezierYForX(x, p[0].from, p[0].c1, p[0].c2, p[0].to, precision); - } - return 0; + const c = selectCurve(path, x); + return cubicBezierYForX( + x, + c.from, + c.curve.c1, + c.curve.c2, + c.curve.to, + precision + ); }; diff --git a/src/__tests__/Paths.test.ts b/src/__tests__/Paths.test.ts index 191f10b3..50e4aec1 100644 --- a/src/__tests__/Paths.test.ts +++ b/src/__tests__/Paths.test.ts @@ -14,7 +14,7 @@ test("getYForX()", () => { ); expect(getYForX(p1, 200)).toBe(75); expect(getYForX(p1, 50)).toBe(151.160325); - expect(getYForX(p1, 750)).toBe(0); + expect(() => getYForX(p1, 750)).toThrow(); }); test("getYForX2()", () => { diff --git a/yarn.lock b/yarn.lock index c8e54ff0..985417c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10736,7 +10736,12 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.6.2, typescript@^3.9.7: +typescript@4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5" + integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg== + +typescript@^3.9.7: version "3.9.7" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==