improvement for usePathInterpolation #2279
Replies: 2 comments 1 reply
-
I've updated the implementation to allow passing many const quadToCubic = (prev: PathCommand, curr: PathCommand): PathCommand => {
const [prevX, prevY] = prev.slice(-2);
const [x1, y1, x, y] = curr.slice(-4);
const cx1 = prevX + (2 / 3) * (x1 - prevX);
const cy1 = prevY + (2 / 3) * (y1 - prevY);
const cx2 = x + (2 / 3) * (x1 - x);
const cy2 = y + (2 / 3) * (y1 - y);
return [PathVerb.Cubic, cx1, cy1, cx2, cy2, x, y];
};
const convertToCubic = (prev: PathCommand, curr: PathCommand): PathCommand => {
const [verb] = curr;
const actual = [PathVerb.Move, PathVerb.Cubic, PathVerb.Close];
if (actual.includes(verb)) {
return curr;
}
if (verb === PathVerb.Line) {
const [prevX, prevY] = prev.slice(-2);
const [x, y] = curr.slice(-2);
return [PathVerb.Cubic, prevX, prevY, x, y, x, y];
}
return quadToCubic(prev, curr);
};
const splitCubicByPercent = (
prev: PathCommand,
curr: PathCommand,
/** From 0 to 1 */
percent: number,
): {first: PathCommand; last: PathCommand} => {
if (percent < 0 || percent > 1) {
console.warn(`Incorrect percent value. Should be >=0 and <=1, actual is ${percent}`);
return {first: curr, last: curr};
}
if (prev.length < 2 || curr.length < 6) {
console.warn(
`Incorrect previous or current command. Previous should be >=2, actual is ${prev.length}. Current should be >=6, actual is ${curr.length}`,
);
return {first: curr, last: curr};
}
const [prevX, prevY] = prev.slice(-2);
const [x1, y1, x2, y2, x, y] = curr.slice(-6);
const helper = [x1 + percent * (x2 - x1), y1 + percent * (y2 - y1)];
const firstP1 = [prevX + percent * (x1 - prevX), prevY + percent * (y1 - prevY)];
const firstP2 = [firstP1[0] + percent * (helper[0] - firstP1[0]), firstP1[1] + percent * (helper[1] - firstP1[1])];
const secondP2 = [x2 + percent * (x - x2), y2 + percent * (y - y2)];
const secondP1 = [helper[0] + percent * (secondP2[0] - helper[0]), helper[1] + percent * (secondP2[1] - helper[1])];
const firstP3 = [
firstP2[0] + percent * (secondP1[0] - firstP2[0]),
firstP2[1] + percent * (secondP1[1] - firstP2[1]),
];
return {
first: [PathVerb.Cubic, firstP1[0], firstP1[1], firstP2[0], firstP2[1], firstP3[0], firstP3[1]],
last: [PathVerb.Cubic, secondP1[0], secondP1[1], secondP2[0], secondP2[1], x, y],
};
};
const splitCubicByCount = (
prev: PathCommand,
curr: PathCommand,
/** an integer greater than 1 */
count: number,
): PathCommand[] => {
if (!Number.isInteger(count) || count < 1) {
console.warn(`Incorrect count, should be integer and >=1, actual is ${count}`);
return [curr];
}
if (prev.length < 2 || curr.length < 6) {
console.warn(
`Incorrect previous or current command. Previous should be >=2, actual is ${prev.length}. Current should be >=6, actual is ${curr.length}`,
);
return [curr];
}
const result: PathCommand[] = [];
let tempPrev = prev;
let tempCurr = curr;
for (let i = 0; i < count - 1; i++) {
const delta = 1 / (count - i);
const segment = splitCubicByPercent(tempPrev, tempCurr, delta);
result.push(segment.first);
tempPrev = segment.first;
tempCurr = segment.last;
}
result.push(tempCurr);
return result;
};
const equalizeArrays = (arrayOfCmds: PathCommand[][]): PathCommand[][] => {
const arrayOfArrays = arrayOfCmds.map(item =>
item.filter(element => element[0] !== PathVerb.Move && element[0] !== PathVerb.Close),
);
const longestLength = arrayOfArrays.reduce((max, item) => Math.max(max, item.length), 0);
let tempParts = 0;
let tempUsedCount = 0;
const resultArray = arrayOfArrays.map((itemArray, index) => {
if (itemArray.length === longestLength) {
return itemArray;
}
const updatedArray = itemArray.flatMap((curve, curveIndex, curveArray) => {
tempParts = Math.ceil((longestLength - tempUsedCount) / (curveArray.length - curveIndex));
tempUsedCount += tempParts;
const prev = curveIndex ? curveArray[curveIndex - 1] : arrayOfCmds[index][0];
return splitCubicByCount(prev, curve, tempParts);
});
tempParts = 0;
tempUsedCount = 0;
return updatedArray;
});
return resultArray.map((item, index) => [arrayOfCmds[index][0], ...item, [PathVerb.Close]]);
};
export const unifyPaths = (arrayOfCmds: SkPath[]) => {
const res = arrayOfCmds.map(item => {
const tempSkia = Skia.Path.MakeFromSVGString(item.toSVGString())!.close();
return tempSkia.toCmds().map((element, index, array) => convertToCubic(array[index - 1], element));
});
const result = equalizeArrays(res);
return result.map(item => Skia.Path.MakeFromCmds(item)!);
}; import React, {useEffect} from 'react';
import {View} from 'react-native';
import {useSharedValue, withRepeat, withTiming} from 'react-native-reanimated';
import {unifyPaths} from './helpers';
import {Canvas, Path, Skia, usePathInterpolation} from '@shopify/react-native-skia';
const path1SVG = 'M 0 0 C 0 0 120 0 120 0 T 120 120 L 0 120 L 0 0';
const path2SVG =
'M40 40L30 10L60 0L90 10L80 40L110 30L120 60L110 90L80 80L90 110L60 120L30 110L40 80L10 90L0 60L10 30L40 40Z';
const path3SVG = 'M 0 0 L 120 0 C 120 40 100 60 60 60 Q 0 60 0 0';
const outputPaths = unifyPaths([
Skia.Path.MakeFromSVGString(path1SVG)!,
Skia.Path.MakeFromSVGString(path2SVG)!,
Skia.Path.MakeFromSVGString(path3SVG)!,
]);
export const Main = () => {
const sv = useSharedValue(0);
useEffect(() => {
sv.value = withRepeat(withTiming(1, {duration: 4800}), -1, true);
}, [sv]);
const resPath = usePathInterpolation(sv, [0, 0.5, 1], outputPaths);
return (
<View>
<Canvas style={{flex: 1, margin: 60}}>
<Path path={resPath} />
</Canvas>
</View>
);
}; Unified.mp4 |
Beta Was this translation helpful? Give feedback.
-
I think you might be highly interesting: https://github.com/wcandillon/canvaskit-js/tree/main/package/src/c2d/Path/PathComponents |
Beta Was this translation helpful? Give feedback.
-
usePathInterpolation
is a rather useful hook, but you need to use two paths with the same combination and order of commands. Could you implement something like unification of paths?i was playing with this maybe can be useful
unification.mp4
Beta Was this translation helpful? Give feedback.
All reactions