diff --git a/example/src/Examples/Wallet/Math.ts b/example/src/Examples/Wallet/Math.ts index 38a25ef8f8..8d905f610a 100644 --- a/example/src/Examples/Wallet/Math.ts +++ b/example/src/Examples/Wallet/Math.ts @@ -123,13 +123,15 @@ interface Cubic { } export const selectCurve = (cmds: PathCommand[], x: number): Cubic | null => { + let from: Vector = vec(0, 0); for (let i = 0; i < cmds.length; i++) { const cmd = cmds[i]; - if (cmd[0] === PathVerb.Cubic) { - const from = vec(cmd[1], cmd[2]); - const to = vec(cmd[7], cmd[8]); - const c1 = vec(cmd[3], cmd[4]); - const c2 = vec(cmd[5], cmd[6]); + if (cmd[0] === PathVerb.Move) { + from = vec(cmd[1], cmd[2]); + } else if (cmd[0] === PathVerb.Cubic) { + const c1 = vec(cmd[1], cmd[2]); + const c2 = vec(cmd[3], cmd[4]); + const to = vec(cmd[5], cmd[6]); if (x >= from.x && x <= to.x) { return { from, @@ -138,6 +140,7 @@ export const selectCurve = (cmds: PathCommand[], x: number): Cubic | null => { to, }; } + from = to; } } return null; @@ -146,7 +149,7 @@ export const selectCurve = (cmds: PathCommand[], x: number): Cubic | null => { export const getYForX = (cmds: PathCommand[], x: number, precision = 2) => { const c = selectCurve(cmds, x); if (c === null) { - return null; + return cmds[1][6]; } return cubicBezierYForX(x, c.from, c.c1, c.c2, c.to, precision); }; diff --git a/example/src/Examples/Wallet/Wallet.tsx b/example/src/Examples/Wallet/Wallet.tsx index ff06814690..9f60c9a340 100644 --- a/example/src/Examples/Wallet/Wallet.tsx +++ b/example/src/Examples/Wallet/Wallet.tsx @@ -27,8 +27,9 @@ const styles = StyleSheet.create({ }); export const Wallet = () => { - const { width } = useWindowDimensions(); - const height = width / 2; + const window = useWindowDimensions(); + const { width } = window; + const height = Math.min(window.width, window.height) / 2; const translateY = height + PADDING; const graphs = useMemo(() => getGraph(width, height), [width, height]); // animation value to transition from one graph to the next @@ -48,7 +49,7 @@ export const Wallet = () => { // x and y values of the cursor const x = useValue(0); const y = useDerivedValue( - () => getYForX(path.current.toCmds(), x.current)!, + () => getYForX(path.current.toCmds(), x.current), [x, path] ); const onTouch = useGraphTouchHandler(x, y, width, height); diff --git a/package/cpp/api/JsiSkPath.h b/package/cpp/api/JsiSkPath.h index 8436722528..92b02f5d65 100644 --- a/package/cpp/api/JsiSkPath.h +++ b/package/cpp/api/JsiSkPath.h @@ -480,8 +480,8 @@ class JsiSkPath : public JsiSkWrappingSharedPtrHostObject { auto cmds = jsi::Array(runtime, path.countVerbs()); auto it = SkPath::Iter(path, false); // { "Move", "Line", "Quad", "Conic", "Cubic", "Close", "Done" }; - const int pointCount[] = { 1 , 2 , 3 , 3 , 4 , 1 , 0 }; - const int cmdCount[] = { 3 , 5 , 7 , 8 , 9 , 3 , 0 }; + const int pointCount[] = { 1 , 1 , 2 , 2 , 3 , 0 , 0 }; + const int cmdCount[] = { 3 , 3 , 5 , 6 , 7 , 1 , 0 }; SkPoint points[4]; SkPath::Verb verb; auto k = 0; @@ -491,8 +491,8 @@ class JsiSkPath : public JsiSkWrappingSharedPtrHostObject { auto j = 0; cmd.setValueAtIndex(runtime, j++, jsi::Value(verbVal)); for (int i = 0; i < pointCount[verbVal]; ++i) { - cmd.setValueAtIndex(runtime, j++, jsi::Value(static_cast(points[i].fX))); - cmd.setValueAtIndex(runtime, j++, jsi::Value(static_cast(points[i].fY))); + cmd.setValueAtIndex(runtime, j++, jsi::Value(static_cast(points[1 + i].fX))); + cmd.setValueAtIndex(runtime, j++, jsi::Value(static_cast(points[1 + i].fY))); } if (SkPath::kConic_Verb == verb) { cmd.setValueAtIndex(runtime, j, jsi::Value(static_cast(it.conicWeight()))); diff --git a/package/patches/canvaskit-wasm+0.34.0.patch b/package/patches/canvaskit-wasm+0.34.0.patch index 52df510367..bd16dc56e6 100644 --- a/package/patches/canvaskit-wasm+0.34.0.patch +++ b/package/patches/canvaskit-wasm+0.34.0.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/canvaskit-wasm/types/index.d.ts b/node_modules/canvaskit-wasm/types/index.d.ts -index 3abbcc3..88fcb2b 100644 +index 3abbcc3..07a9cab 100644 --- a/node_modules/canvaskit-wasm/types/index.d.ts +++ b/node_modules/canvaskit-wasm/types/index.d.ts @@ -1,5 +1,5 @@ @@ -9,7 +9,15 @@ index 3abbcc3..88fcb2b 100644 export interface CanvasKitInitOptions { /** -@@ -1983,7 +1983,7 @@ export interface Paint extends EmbindObject { +@@ -1018,6 +1018,7 @@ export interface SkSLUniform { + rows: number; + /** The index into the uniforms array that this uniform begins. */ + slot: number; ++ isInteger: boolean; + } + + /** +@@ -1983,7 +1984,7 @@ export interface Paint extends EmbindObject { * Sets the current color filter, replacing the existing one if there was one. * @param filter */ @@ -18,7 +26,7 @@ index 3abbcc3..88fcb2b 100644 /** * Sets the color used when stroking and filling. The color values are interpreted as being in -@@ -1997,25 +1997,25 @@ export interface Paint extends EmbindObject { +@@ -1997,25 +1998,25 @@ export interface Paint extends EmbindObject { * Sets the current image filter, replacing the existing one if there was one. * @param filter */ diff --git a/package/src/__tests__/snapshots/path/interpolate.png b/package/src/__tests__/snapshots/path/interpolate.png new file mode 100644 index 0000000000..4414f7042f Binary files /dev/null and b/package/src/__tests__/snapshots/path/interpolate.png differ diff --git a/package/src/skia/__tests__/Path.spec.ts b/package/src/skia/__tests__/Path.spec.ts index b4e7264953..b6d01dca28 100644 --- a/package/src/skia/__tests__/Path.spec.ts +++ b/package/src/skia/__tests__/Path.spec.ts @@ -1,9 +1,9 @@ /* eslint-disable max-len */ import { interpolatePaths } from "../../animation/functions/interpolatePaths"; +import type { Skia, SkPath } from "../types"; +import { FillType, PathOp, PathVerb } from "../types"; import { processResult } from "../../__tests__/setup"; -import { FillType } from "../types"; -import type { SkPath } from "../types/Path/Path"; -import type { Skia } from "../types/Skia"; +import { PaintStyle } from "../types/Paint/Paint"; import { setupSkia } from "./setup"; @@ -34,6 +34,87 @@ describe("Path", () => { processResult(surface, "snapshots/path/logo.png"); }); + it("Should test that path are interpolatable as specificed in fiddle", () => { + // https://fiddle.skia.org/c/@Path_isInterpolatable + const { Skia } = setupSkia(); + const path = Skia.Path.Make(); + const path2 = Skia.Path.Make(); + path.moveTo(20, 20); + path.lineTo(40, 40); + path.lineTo(20, 20); + path.lineTo(40, 40); + path.close(); + path2.addRect(Skia.XYWHRect(20, 20, 20, 20)); + expect(path.isInterpolatable(path2)).toBe(true); + }); + + it("Should test that path interpolation works as specified in the Skia test suite", () => { + // https://github.com/google/skia/blob/1f193df9b393d50da39570dab77a0bb5d28ec8ef/tests/PathTest.cpp + const { Skia } = setupSkia(); + const p1 = Skia.Path.Make(); + const p2 = Skia.Path.Make(); + let p3: SkPath; + expect(p1.isInterpolatable(p2)).toBe(true); + p3 = p1.interpolate(p2, 0)!; + expect(p3).toBeTruthy(); + expect(p1.toCmds()).toEqual(p3.toCmds()); + p3 = p1.interpolate(p2, 1)!; + expect(p3).toBeTruthy(); + expect(p2.toCmds()).toEqual(p3.toCmds()); + p1.moveTo(0, 2); + p1.lineTo(0, 4); + expect(p1.isInterpolatable(p2)).toBe(false); + expect(p1.interpolate(p2, 1)).toBeNull(); + p2.moveTo(6, 0); + p2.lineTo(8, 0); + expect(p1.isInterpolatable(p2)).toBe(true); + p3 = p1.interpolate(p2, 0)!; + expect(p3).toBeTruthy(); + expect(p2.toCmds()).toEqual(p3.toCmds()); + p3 = p1.interpolate(p2, 1)!; + expect(p3).toBeTruthy(); + expect(p1.toCmds()).toEqual(p3.toCmds()); + p3 = p1.interpolate(p2, 0.5)!; + expect(p3).toBeTruthy(); + let bounds = p3.getBounds(); + let rect = Skia.XYWHRect(3, 1, 1, 1); + expect([bounds.x, bounds.y, bounds.width, bounds.height]).toEqual([ + rect.x, + rect.y, + rect.width, + rect.height, + ]); + p1.reset(); + p1.moveTo(4, 4); + p1.conicTo(5, 4, 5, 5, 0.2); + p2.reset(); + p2.moveTo(4, 2); + p2.conicTo(7, 2, 7, 5, 0.2); + expect(p1.isInterpolatable(p2)).toBe(true); + p3 = p1.interpolate(p2, 0.5)!; + expect(p3).toBeTruthy(); + expect(p3.toCmds()).toEqual([ + [PathVerb.Move, 4, 3], + [PathVerb.Conic, 6, 3, 6, 5, Math.fround(0.2)], + ]); + bounds = p3.getBounds(); + rect = Skia.XYWHRect(4, 3, 2, 2); + expect([bounds.x, bounds.y, bounds.width, bounds.height]).toEqual([ + rect.x, + rect.y, + rect.width, + rect.height, + ]); + p2.reset(); + p2.moveTo(4, 2); + p2.conicTo(6, 3, 6, 5, 1); + expect(p1.isInterpolatable(p2)).toBe(false); + p2.reset(); + p2.moveTo(4, 4); + p2.conicTo(5, 4, 5, 5, 0.5); + expect(p1.isInterpolatable(p2)).toBe(false); + }); + it("Should test that paths can be interpolated", () => { const { Skia } = setupSkia(); const path1 = Skia.Path.Make(); @@ -47,7 +128,7 @@ describe("Path", () => { expect(path1.isInterpolatable(path2)).toBe(false); }); - it("Should interpolate Path", () => { + it("Should interpolate one Path", () => { const { Skia } = setupSkia(); const path1 = Skia.Path.Make(); path1.moveTo(0, 0); @@ -58,13 +139,18 @@ describe("Path", () => { const path3 = Skia.Path.Make(); path3.moveTo(50, 50); path3.lineTo(50, 50); - const p3Cmds = path3.toCmds().flat(); - expect(path1.interpolate(path2, 0)!.toCmds().flat()).toEqual( - path2.toCmds().flat() - ); - expect(path1.interpolate(path2, 0.5)!.toCmds().flat()).toEqual(p3Cmds); - path2.lineTo(0, 100); - expect(path1.interpolate(path2, 1)).toBe(null); + expect(path1.isInterpolatable(path2)).toBe(true); + const interpolate0 = path1.interpolate(path2, 0)!; + const interpolate05 = path1.interpolate(path2, 0.5)!; + const interpolate1 = path1.interpolate(path2, 1)!; + + expect(interpolate0).not.toBeNull(); + expect(interpolate05).not.toBeNull(); + expect(interpolate1).not.toBeNull(); + + expect(interpolate0.toCmds().flat()).toEqual(path2.toCmds().flat()); + expect(interpolate05.toCmds().flat()).toEqual(path3.toCmds().flat()); + expect(interpolate1.toCmds().flat()).toEqual(path1.toCmds().flat()); }); it("Should interpolate more than one Path", () => { @@ -78,6 +164,8 @@ describe("Path", () => { const path3 = Skia.Path.Make(); path3.moveTo(0, 0); path3.lineTo(200, 200); + expect(path1.isInterpolatable(path2)).toBe(true); + expect(path2.isInterpolatable(path3)).toBe(true); let path = interpolatePaths(0, [0, 0.5, 1], [path1, path2, path3]); expect(path.toCmds().flat()).toEqual(path1.toCmds().flat()); path = interpolatePaths(0.5, [0, 0.5, 1], [path1, path2, path3]); @@ -113,6 +201,103 @@ describe("Path", () => { path3.moveTo(0, 0); path3.lineTo(200, 200); const path = interpolatePaths(2, [0, 0.5, 1], [path1, path2, path3]); - expect(path.toCmds().flat()).toEqual(path3.toCmds().flat()); + expect(path.toCmds()).toEqual(path3.toCmds()); + }); + + it("Should match construct a path from commands", () => { + const { Skia } = setupSkia(); + const cmds = [ + [PathVerb.Move, 205, 5], + [PathVerb.Line, 795, 5], + [PathVerb.Line, 595, 295], + [PathVerb.Line, 5, 295], + [PathVerb.Line, 205, 5], + [PathVerb.Close], + ]; + const path = Skia.Path.MakeFromCmds(cmds)!; + expect(path).toBeTruthy(); + const svgStr = path.toSVGString(); + // We output it in terse form, which is different than Wikipedia's version + expect(svgStr).toEqual("M205 5L795 5L595 295L5 295L205 5Z"); + }); + + it("Should serialize a path to commands", () => { + const { Skia } = setupSkia(); + const path = Skia.Path.MakeFromSVGString( + "M 205,5 L 795,5 L 595,295 L 5,295 L 205,5 z" + )!; + const cmds = path.toCmds(); + expect(cmds).toBeTruthy(); + // 1 move, 4 lines, 1 close + // each element in cmds is an array, with index 0 being the verb, and the rest being args + expect(cmds.length).toBe(6); + expect(cmds).toEqual([ + [PathVerb.Move, 205, 5], + [PathVerb.Line, 795, 5], + [PathVerb.Line, 595, 295], + [PathVerb.Line, 5, 295], + [PathVerb.Line, 205, 5], + [PathVerb.Close], + ]); + }); + + it("should create a path by combining two other paths", () => { + const { Skia } = setupSkia(); + // Get the intersection of two overlapping squares and verify that it is the smaller square. + const pathOne = Skia.Path.Make(); + pathOne.addRect(Skia.XYWHRect(0, 0, 100, 100)); + + const pathTwo = Skia.Path.Make(); + pathTwo.addRect(Skia.XYWHRect(50, 50, 50, 50)); + + const path = Skia.Path.MakeFromOp(pathOne, pathTwo, PathOp.Intersect)!; + expect(path).not.toBeNull(); + const cmds = path.toCmds(); + expect(cmds).toBeTruthy(); + expect(cmds).toEqual([ + [PathVerb.Move, 50, 50], + [PathVerb.Line, 100, 50], + [PathVerb.Line, 100, 100], + [PathVerb.Line, 50, 100], + [PathVerb.Close], + ]); + }); + + it("should create an SVG string from a path", () => { + const { Skia } = setupSkia(); + const cmds = [ + [PathVerb.Move, 205, 5], + [PathVerb.Line, 795, 5], + [PathVerb.Line, 595, 295], + [PathVerb.Line, 5, 295], + [PathVerb.Line, 205, 5], + [PathVerb.Close], + ]; + const path = Skia.Path.MakeFromCmds(cmds)!; + expect(path).toBeTruthy(); + // We output it in terse form, which is different than Wikipedia's version + expect(path.toSVGString()).toEqual("M205 5L795 5L595 295L5 295L205 5Z"); + }); + + it("should draw different interpolation states", () => { + const { Skia, surface, canvas } = setupSkia(); + const paint = Skia.Paint(); + paint.setAntiAlias(true); + paint.setStyle(PaintStyle.Stroke); + const path = Skia.Path.Make(); + const path2 = Skia.Path.Make(); + path.moveTo(20, 20); + path.lineTo(40, 40); + path.lineTo(20, 40); + path.lineTo(40, 20); + path.close(); + path2.addRect(Skia.XYWHRect(20, 20, 20, 20)); + for (let i = 0; i <= 1; i += 1 / 6) { + const interp = path.interpolate(path2, i)!; + expect(interp).toBeTruthy(); + canvas.drawPath(interp, paint); + canvas.translate(30, 0); + } + processResult(surface, "snapshots/path/interpolate.png"); }); }); diff --git a/package/src/skia/__tests__/RuntimeEffect.spec.ts b/package/src/skia/__tests__/RuntimeEffect.spec.ts new file mode 100644 index 0000000000..437f7c5da6 --- /dev/null +++ b/package/src/skia/__tests__/RuntimeEffect.spec.ts @@ -0,0 +1,66 @@ +import { processResult } from "../../__tests__/setup"; + +import { setupSkia } from "./setup"; + +const spiralSkSL = ` +uniform float rad_scale; +uniform int2 in_center; +uniform float4 in_colors0; +uniform float4 in_colors1; +half4 main(float2 p) { + float2 pp = p - float2(in_center); + float radius = sqrt(dot(pp, pp)); + radius = sqrt(radius); + float angle = atan(pp.y / pp.x); + float t = (angle + 3.1415926/2) / (3.1415926); + t += radius * rad_scale; + t = fract(t); + return half4(mix(in_colors0, in_colors1, t)); +}`; + +describe("RuntimeEffect", () => { + it("Should draw a spiral", () => { + const { surface, Skia, width, height } = setupSkia(); + const spiral = Skia.RuntimeEffect.Make(spiralSkSL)!; + expect(spiral).toBeTruthy(); + + expect(spiral.getUniformCount()).toEqual(4); + expect(spiral.getUniformFloatCount()).toEqual(11); + const center = spiral.getUniform(1); + expect(center).toBeTruthy(); + + expect(center.slot).toEqual(1); + expect(center.columns).toEqual(2); + expect(center.rows).toEqual(1); + expect(center.isInteger).toEqual(true); + const color0 = spiral.getUniform(2); + expect(color0).toBeTruthy(); + expect(color0.slot).toEqual(3); + expect(color0.columns).toEqual(4); + expect(color0.rows).toEqual(1); + expect(color0.isInteger).toEqual(false); + expect(spiral.getUniformName(2)).toEqual("in_colors0"); + + const canvas = surface.getCanvas(); + const paint = Skia.Paint(); + canvas.clear(Skia.Color("black")); // black should not be visible + const shader = spiral.makeShader( + [ + 0.3, + width / 2, + height / 2, + 1, + 0, + 0, + 1, // solid red + 0, + 1, + 0, + 1, + ] // solid green + ); + paint.setShader(shader); + canvas.drawRect(Skia.XYWHRect(0, 0, width, height), paint); + processResult(surface, "snapshots/runtime-effects/spiral.png"); + }); +}); diff --git a/package/src/skia/types/Path/Path.ts b/package/src/skia/types/Path/Path.ts index deb6b8b3bf..970d0045ad 100644 --- a/package/src/skia/types/Path/Path.ts +++ b/package/src/skia/types/Path/Path.ts @@ -44,7 +44,6 @@ export enum PathVerb { Conic, Cubic, Close, - Done, } export type PathCommand = number[]; diff --git a/package/src/skia/types/RuntimeEffect/RuntimeEffect.ts b/package/src/skia/types/RuntimeEffect/RuntimeEffect.ts index 4a28c42c89..3f2d650b39 100644 --- a/package/src/skia/types/RuntimeEffect/RuntimeEffect.ts +++ b/package/src/skia/types/RuntimeEffect/RuntimeEffect.ts @@ -7,6 +7,7 @@ export interface SkSLUniform { rows: number; /** The index into the uniforms array that this uniform begins. */ slot: number; + isInteger: boolean; } export interface SkRuntimeShaderBuilder diff --git a/package/src/skia/web/JsiSkPath.ts b/package/src/skia/web/JsiSkPath.ts index 9116603e2d..d2c5d249a2 100644 --- a/package/src/skia/web/JsiSkPath.ts +++ b/package/src/skia/web/JsiSkPath.ts @@ -17,6 +17,29 @@ import { ckEnum, HostObject, optEnum, toValue } from "./Host"; import { JsiSkPoint } from "./JsiSkPoint"; import { JsiSkRect } from "./JsiSkRect"; +const CommandCount = { + [PathVerb.Move]: 3, + [PathVerb.Line]: 3, + [PathVerb.Quad]: 5, + [PathVerb.Conic]: 6, + [PathVerb.Cubic]: 7, + [PathVerb.Close]: 1, +}; + +const areCmdsInterpolatable = (cmd1: PathCommand[], cmd2: PathCommand[]) => { + if (cmd1.length !== cmd2.length) { + return false; + } + for (let i = 0; i < cmd1.length; i++) { + if (cmd1[i][0] !== cmd2[i][0]) { + return false; + } else if (cmd1[i][0] === PathVerb.Conic && cmd1[i][5] !== cmd2[i][5]) { + return false; + } + } + return true; +}; + export class JsiSkPath extends HostObject implements SkPath { constructor(CanvasKit: CanvasKit, ref: Path) { super(CanvasKit, ref, "Path"); @@ -300,20 +323,25 @@ export class JsiSkPath extends HostObject implements SkPath { // throw new NotImplementedOnRNWeb(); const cmd1 = this.toCmds(); const cmd2 = end.toCmds(); - if (cmd1.length !== cmd2.length) { + if (!areCmdsInterpolatable(cmd1, cmd2)) { return null; } const interpolated: PathCommand[] = []; - for (let i = 0; i < cmd1.length; i++) { - if (cmd1[i][0] !== cmd2[i][0]) { - return null; - } - const cmd: PathCommand = [cmd1[i][0]]; - for (let j = 1; j < cmd1[i].length; j++) { - cmd.push(cmd2[i][j] + (cmd1[i][j] - cmd2[i][j]) * t); - } - interpolated.push(cmd); - } + cmd1.forEach((cmd, i) => { + const interpolatedCmd = [cmd[0]]; + interpolated.push(interpolatedCmd); + cmd.forEach((c, j) => { + if (j === 0) { + return; + } + if (interpolatedCmd[0] === PathVerb.Conic && j === 5) { + interpolatedCmd.push(c); + } else { + const c2 = cmd2[i][j]; + interpolatedCmd.push(c2 + (c - c2) * t); + } + }); + }); const path = this.CanvasKit.Path.MakeFromCmds(interpolated.flat()); if (path === null) { return null; @@ -326,44 +354,33 @@ export class JsiSkPath extends HostObject implements SkPath { // throw new NotImplementedOnRNWeb(); const cmd1 = this.toCmds(); const cmd2 = path2.toCmds(); - if (cmd1.length !== cmd2.length) { - return false; - } - for (let i = 0; i < cmd1.length; i++) { - if (cmd1[i][0] !== cmd2[i][0]) { - return false; - } - } - return true; - } - - toCmds(): PathCommand[] { - const cmds: PathCommand[] = []; - let cmd = []; - const flatCmds = this.ref.toCmds(); - const CmdCount = { - [PathVerb.Move]: 3, - [PathVerb.Line]: 3, - [PathVerb.Quad]: 5, - [PathVerb.Conic]: 6, - [PathVerb.Cubic]: 7, - [PathVerb.Close]: 0, - [PathVerb.Done]: 0, - }; - for (let i = 0; i < flatCmds.length; i++) { - if (cmd.length === 0 && flatCmds[i] === PathVerb.Done) { - break; + return areCmdsInterpolatable(cmd1, cmd2); + } + + toCmds() { + const cmds = this.ref.toCmds(); + const result = cmds.reduce((acc, cmd, i) => { + if (i === 0) { + acc.push([]); } - const c = flatCmds[i]; - cmd.push(c); - if (cmd.length > 1) { - const length = CmdCount[cmd[0] as PathVerb]; - if (cmd.length === length) { - cmds.push(cmd); - cmd = []; + const current = acc[acc.length - 1]; + if (current.length === 0) { + current.push(cmd); + const length = CommandCount[current[0] as PathVerb]; + if (current.length === length && i !== cmds.length - 1) { + acc.push([]); + } + } else { + const length = CommandCount[current[0] as PathVerb]; + if (current.length < length) { + current.push(cmd); + } + if (current.length === length && i !== cmds.length - 1) { + acc.push([]); } } - } - return cmds.concat(cmd); + return acc; + }, []); + return result; } }