Skip to content

Commit

Permalink
Fix path.toCmd() & interpolate() (#608)
Browse files Browse the repository at this point in the history
  • Loading branch information
wcandillon authored Jun 21, 2022
1 parent 303ae55 commit dbf8846
Show file tree
Hide file tree
Showing 10 changed files with 356 additions and 76 deletions.
15 changes: 9 additions & 6 deletions example/src/Examples/Wallet/Math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -138,6 +140,7 @@ export const selectCurve = (cmds: PathCommand[], x: number): Cubic | null => {
to,
};
}
from = to;
}
}
return null;
Expand All @@ -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);
};
Expand Down
7 changes: 4 additions & 3 deletions example/src/Examples/Wallet/Wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions package/cpp/api/JsiSkPath.h
Original file line number Diff line number Diff line change
Expand Up @@ -480,8 +480,8 @@ class JsiSkPath : public JsiSkWrappingSharedPtrHostObject<SkPath> {
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;
Expand All @@ -491,8 +491,8 @@ class JsiSkPath : public JsiSkWrappingSharedPtrHostObject<SkPath> {
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<double>(points[i].fX)));
cmd.setValueAtIndex(runtime, j++, jsi::Value(static_cast<double>(points[i].fY)));
cmd.setValueAtIndex(runtime, j++, jsi::Value(static_cast<double>(points[1 + i].fX)));
cmd.setValueAtIndex(runtime, j++, jsi::Value(static_cast<double>(points[1 + i].fY)));
}
if (SkPath::kConic_Verb == verb) {
cmd.setValueAtIndex(runtime, j, jsi::Value(static_cast<double>(it.conicWeight())));
Expand Down
14 changes: 11 additions & 3 deletions package/patches/canvaskit-wasm+0.34.0.patch
Original file line number Diff line number Diff line change
@@ -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 @@
Expand All @@ -9,7 +9,15 @@ index 3abbcc3..88fcb2b 100644

export interface CanvasKitInitOptions {
/**
@@ -1983,7 +1983,7 @@ export interface Paint extends EmbindObject<Paint> {
@@ -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<Paint> {
* Sets the current color filter, replacing the existing one if there was one.
* @param filter
*/
Expand All @@ -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<Paint> {
@@ -1997,25 +1998,25 @@ export interface Paint extends EmbindObject<Paint> {
* Sets the current image filter, replacing the existing one if there was one.
* @param filter
*/
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
209 changes: 197 additions & 12 deletions package/src/skia/__tests__/Path.spec.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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", () => {
Expand All @@ -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]);
Expand Down Expand Up @@ -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");
});
});
Loading

0 comments on commit dbf8846

Please sign in to comment.