@shopify/react-native-skia
Version:
High-performance React Native Graphics using Skia
697 lines (635 loc) • 17.1 kB
text/typescript
import type { CanvasKit, PathBuilder as CKPathBuilder } from "canvaskit-wasm";
import { PathVerb } from "../types";
import type {
FillType,
PathCommand,
PathOp,
SkMatrix,
SkPath,
SkPoint,
SkRect,
InputRRect,
StrokeOpts,
InputMatrix,
} from "../types";
import { getEnum, HostObject, optEnum } from "./Host";
import { JsiSkPoint } from "./JsiSkPoint";
import { JsiSkRect } from "./JsiSkRect";
import { JsiSkRRect } from "./JsiSkRRect";
import { JsiSkMatrix } from "./JsiSkMatrix";
const CommandCount = {
[PathVerb.Move]: 3,
[PathVerb.Line]: 3,
[PathVerb.Quad]: 5,
[PathVerb.Conic]: 6,
[PathVerb.Cubic]: 7,
[PathVerb.Close]: 1,
};
// Track which deprecation warnings have been shown to avoid spam
const shownDeprecationWarnings = new Set<string>();
const warnDeprecatedPathMethod = (
methodName: string,
suggestion: string
): void => {
if (shownDeprecationWarnings.has(methodName)) {
return;
}
shownDeprecationWarnings.add(methodName);
console.warn(
`[react-native-skia] SkPath.${methodName}() is deprecated and will be removed in a future release. ${suggestion} See migration guide: https://shopify.github.io/react-native-skia/docs/shapes/path-migration`
);
};
export const toMatrix3x3 = (m: InputMatrix): number[] => {
let matrix =
m instanceof JsiSkMatrix
? Array.from(JsiSkMatrix.fromValue<Float32Array>(m))
: (m as Exclude<InputMatrix, SkMatrix>);
if (matrix.length === 16) {
matrix = [
matrix[0],
matrix[1],
matrix[3],
matrix[4],
matrix[5],
matrix[7],
matrix[12],
matrix[13],
matrix[15],
];
} else if (matrix.length !== 9) {
throw new Error(`Invalid matrix length: ${matrix.length}`);
}
return matrix as number[];
};
/**
* SkPath wraps a CK PathBuilder internally, providing both mutable building
* methods and immutable query methods. Use snapshot() internally to get
* an immutable CK Path for read-only operations.
*/
export class JsiSkPath
extends HostObject<CKPathBuilder, "Path">
implements SkPath
{
constructor(CanvasKit: CanvasKit, ref: CKPathBuilder) {
super(CanvasKit, ref, "Path");
}
/** Returns an immutable CK Path snapshot for read-only operations. */
private asPath() {
return this.ref.snapshot();
}
/** Extract an immutable CK Path from a JsiSkPath value (for CK interop). */
static pathFromValue(value: SkPath) {
return JsiSkPath.fromValue<CKPathBuilder>(value).snapshot();
}
// ---- Mutable building methods (deprecated) ----
addPath(src: SkPath, matrix?: SkMatrix, extend = false) {
warnDeprecatedPathMethod(
"addPath",
"Use Skia.PathBuilder.Make().addPath() instead."
);
const srcBuilder = JsiSkPath.fromValue<CKPathBuilder>(src);
const srcPath = srcBuilder.snapshot();
const args = [
srcPath,
...(matrix ? JsiSkMatrix.fromValue<Float32Array>(matrix) : []),
extend,
];
this.ref.addPath(...args);
srcPath.delete();
return this;
}
addArc(
oval: SkRect,
startAngleInDegrees: number,
sweepAngleInDegrees: number
) {
warnDeprecatedPathMethod(
"addArc",
"Use Skia.PathBuilder.Make().addArc() instead."
);
this.ref.addArc(
JsiSkRect.fromValue(this.CanvasKit, oval),
startAngleInDegrees,
sweepAngleInDegrees
);
return this;
}
addOval(oval: SkRect, isCCW?: boolean, startIndex?: number) {
warnDeprecatedPathMethod(
"addOval",
"Use Skia.Path.Oval() or Skia.PathBuilder.Make().addOval() instead."
);
this.ref.addOval(
JsiSkRect.fromValue(this.CanvasKit, oval),
isCCW,
startIndex
);
return this;
}
addPoly(points: SkPoint[], close: boolean) {
warnDeprecatedPathMethod(
"addPoly",
"Use Skia.Path.Polygon() or Skia.PathBuilder.Make().addPoly() instead."
);
this.ref.addPolygon(
points.map((p) => Array.from(JsiSkPoint.fromValue(p))).flat(),
close
);
return this;
}
addRect(rect: SkRect, isCCW?: boolean) {
warnDeprecatedPathMethod(
"addRect",
"Use Skia.Path.Rect() or Skia.PathBuilder.Make().addRect() instead."
);
this.ref.addRect(JsiSkRect.fromValue(this.CanvasKit, rect), isCCW);
return this;
}
addRRect(rrect: InputRRect, isCCW?: boolean) {
warnDeprecatedPathMethod(
"addRRect",
"Use Skia.Path.RRect() or Skia.PathBuilder.Make().addRRect() instead."
);
this.ref.addRRect(JsiSkRRect.fromValue(this.CanvasKit, rrect), isCCW);
return this;
}
addCircle(x: number, y: number, r: number) {
warnDeprecatedPathMethod(
"addCircle",
"Use Skia.Path.Circle() or Skia.PathBuilder.Make().addCircle() instead."
);
this.ref.addCircle(x, y, r);
return this;
}
moveTo(x: number, y: number) {
warnDeprecatedPathMethod(
"moveTo",
"Use Skia.PathBuilder.Make().moveTo() instead."
);
this.ref.moveTo(x, y);
return this;
}
rMoveTo(x: number, y: number) {
warnDeprecatedPathMethod(
"rMoveTo",
"Use Skia.PathBuilder.Make().rMoveTo() instead."
);
this.ref.rMoveTo(x, y);
return this;
}
lineTo(x: number, y: number) {
warnDeprecatedPathMethod(
"lineTo",
"Use Skia.PathBuilder.Make().lineTo() instead."
);
this.ref.lineTo(x, y);
return this;
}
rLineTo(x: number, y: number) {
warnDeprecatedPathMethod(
"rLineTo",
"Use Skia.PathBuilder.Make().rLineTo() instead."
);
this.ref.rLineTo(x, y);
return this;
}
quadTo(x1: number, y1: number, x2: number, y2: number) {
warnDeprecatedPathMethod(
"quadTo",
"Use Skia.PathBuilder.Make().quadTo() instead."
);
this.ref.quadTo(x1, y1, x2, y2);
return this;
}
rQuadTo(x1: number, y1: number, x2: number, y2: number) {
warnDeprecatedPathMethod(
"rQuadTo",
"Use Skia.PathBuilder.Make().rQuadTo() instead."
);
this.ref.rQuadTo(x1, y1, x2, y2);
return this;
}
conicTo(x1: number, y1: number, x2: number, y2: number, w: number) {
warnDeprecatedPathMethod(
"conicTo",
"Use Skia.PathBuilder.Make().conicTo() instead."
);
this.ref.conicTo(x1, y1, x2, y2, w);
return this;
}
rConicTo(x1: number, y1: number, x2: number, y2: number, w: number) {
warnDeprecatedPathMethod(
"rConicTo",
"Use Skia.PathBuilder.Make().rConicTo() instead."
);
this.ref.rConicTo(x1, y1, x2, y2, w);
return this;
}
cubicTo(
cpx1: number,
cpy1: number,
cpx2: number,
cpy2: number,
x: number,
y: number
) {
warnDeprecatedPathMethod(
"cubicTo",
"Use Skia.PathBuilder.Make().cubicTo() instead."
);
this.ref.cubicTo(cpx1, cpy1, cpx2, cpy2, x, y);
return this;
}
rCubicTo(
cpx1: number,
cpy1: number,
cpx2: number,
cpy2: number,
x: number,
y: number
) {
warnDeprecatedPathMethod(
"rCubicTo",
"Use Skia.PathBuilder.Make().rCubicTo() instead."
);
this.ref.rCubicTo(cpx1, cpy1, cpx2, cpy2, x, y);
return this;
}
close() {
warnDeprecatedPathMethod(
"close",
"Use Skia.PathBuilder.Make().close() instead."
);
this.ref.close();
return this;
}
reset() {
warnDeprecatedPathMethod(
"reset",
"Use Skia.PathBuilder.Make().reset() instead."
);
// CK PathBuilder has no reset — recreate
const newBuilder = new this.CanvasKit.PathBuilder();
if (
this.ref !== null &&
typeof this.ref === "object" &&
"delete" in this.ref &&
typeof this.ref.delete === "function"
) {
this.ref.delete();
}
this.ref = newBuilder;
return this;
}
rewind() {
warnDeprecatedPathMethod(
"rewind",
"Use Skia.PathBuilder.Make().reset() instead."
);
return this.reset();
}
arcToOval(
oval: SkRect,
startAngleInDegrees: number,
sweepAngleInDegrees: number,
forceMoveTo: boolean
) {
warnDeprecatedPathMethod(
"arcToOval",
"Use Skia.PathBuilder.Make().arcToOval() instead."
);
this.ref.arcToOval(
JsiSkRect.fromValue(this.CanvasKit, oval),
startAngleInDegrees,
sweepAngleInDegrees,
forceMoveTo
);
return this;
}
arcToRotated(
rx: number,
ry: number,
xAxisRotateInDegrees: number,
useSmallArc: boolean,
isCCW: boolean,
x: number,
y: number
) {
warnDeprecatedPathMethod(
"arcToRotated",
"Use Skia.PathBuilder.Make().arcToRotated() instead."
);
this.ref.arcToRotated(
rx,
ry,
xAxisRotateInDegrees,
useSmallArc,
isCCW,
x,
y
);
return this;
}
rArcTo(
rx: number,
ry: number,
xAxisRotateInDegrees: number,
useSmallArc: boolean,
isCCW: boolean,
dx: number,
dy: number
) {
warnDeprecatedPathMethod(
"rArcTo",
"Use Skia.PathBuilder.Make().rArcTo() instead."
);
this.ref.rArcTo(rx, ry, xAxisRotateInDegrees, useSmallArc, isCCW, dx, dy);
return this;
}
arcToTangent(x1: number, y1: number, x2: number, y2: number, radius: number) {
warnDeprecatedPathMethod(
"arcToTangent",
"Use Skia.PathBuilder.Make().arcToTangent() instead."
);
this.ref.arcToTangent(x1, y1, x2, y2, radius);
return this;
}
setFillType(fill: FillType) {
warnDeprecatedPathMethod(
"setFillType",
"Use Skia.PathBuilder.Make().setFillType() instead."
);
this.ref.setFillType(getEnum(this.CanvasKit, "FillType", fill));
return this;
}
setIsVolatile(_volatile: boolean) {
warnDeprecatedPathMethod(
"setIsVolatile",
"Use Skia.PathBuilder.Make().setIsVolatile() instead."
);
// Not supported in CK PathBuilder — no-op
return this;
}
// ---- Mutable path operations (deprecated) ----
offset(dx: number, dy: number) {
warnDeprecatedPathMethod(
"offset",
"Use Skia.PathBuilder.Make().offset() instead."
);
this.ref.offset(dx, dy);
return this;
}
transform(m: InputMatrix) {
warnDeprecatedPathMethod(
"transform",
"Use Skia.PathBuilder.Make().transform() instead."
);
const matrix = toMatrix3x3(m);
this.ref.transform(matrix);
return this;
}
makeAsWinding() {
warnDeprecatedPathMethod(
"makeAsWinding",
"Use Skia.Path.AsWinding(path) instead."
);
const path = this.asPath();
const result = path.makeAsWinding();
path.delete();
if (result === null) {
return null;
}
const old = this.ref;
this.ref = new this.CanvasKit.PathBuilder(result);
result.delete();
old.delete();
return this;
}
simplify() {
warnDeprecatedPathMethod("simplify", "Use Skia.Path.Simplify(path) instead.");
const path = this.asPath();
const result = path.makeSimplified();
path.delete();
if (result === null) {
return false;
}
const old = this.ref;
this.ref = new this.CanvasKit.PathBuilder(result);
result.delete();
old.delete();
return true;
}
op(path: SkPath, op: PathOp) {
warnDeprecatedPathMethod("op", "Use Skia.Path.MakeFromOp() instead.");
const self = this.asPath();
const other = JsiSkPath.fromValue<CKPathBuilder>(path).snapshot();
const result = self.makeCombined(
other,
getEnum(this.CanvasKit, "PathOp", op)
);
self.delete();
other.delete();
if (result === null) {
return false;
}
const old = this.ref;
this.ref = new this.CanvasKit.PathBuilder(result);
result.delete();
old.delete();
return true;
}
dash(on: number, off: number, phase: number) {
warnDeprecatedPathMethod(
"dash",
"Use Skia.Path.Dash(path, on, off, phase) instead."
);
const path = this.asPath();
const result = path.makeDashed(on, off, phase);
path.delete();
if (result === null) {
return false;
}
const old = this.ref;
this.ref = new this.CanvasKit.PathBuilder(result);
result.delete();
old.delete();
return true;
}
stroke(opts?: StrokeOpts) {
warnDeprecatedPathMethod("stroke", "Use Skia.Path.Stroke(path, opts) instead.");
const path = this.asPath();
const result = path.makeStroked(
opts === undefined
? undefined
: {
width: opts.width,
// eslint-disable-next-line camelcase
miter_limit: opts.miter_limit,
precision: opts.precision,
join: optEnum(this.CanvasKit, "StrokeJoin", opts.join),
cap: optEnum(this.CanvasKit, "StrokeCap", opts.cap),
}
);
path.delete();
if (result === null) {
return null;
}
const old = this.ref;
this.ref = new this.CanvasKit.PathBuilder(result);
result.delete();
old.delete();
return this;
}
trim(start: number, stop: number, isComplement: boolean) {
warnDeprecatedPathMethod(
"trim",
"Use Skia.Path.Trim(path, start, end, isComplement) instead."
);
const startT = Math.min(Math.max(start, 0), 1);
const stopT = Math.min(Math.max(stop, 0), 1);
if (startT === 0 && stopT === 1 && !isComplement) {
return this;
}
const path = this.asPath();
const result = path.makeTrimmed(startT, stopT, isComplement);
path.delete();
if (result === null) {
return null;
}
const old = this.ref;
this.ref = new this.CanvasKit.PathBuilder(result);
result.delete();
old.delete();
return this;
}
// ---- Query methods (use snapshot for read-only) ----
countPoints() {
return this.ref.countPoints();
}
computeTightBounds(): SkRect {
const path = this.asPath();
const result = new JsiSkRect(this.CanvasKit, path.computeTightBounds());
path.delete();
return result;
}
contains(x: number, y: number) {
const path = this.asPath();
const result = path.contains(x, y);
path.delete();
return result;
}
copy() {
const path = this.asPath();
const result = new JsiSkPath(
this.CanvasKit,
new this.CanvasKit.PathBuilder(path)
);
path.delete();
return result;
}
equals(other: SkPath) {
const p1 = this.asPath();
const p2 = JsiSkPath.fromValue<CKPathBuilder>(other).snapshot();
const result = p1.equals(p2);
p1.delete();
p2.delete();
return result;
}
getBounds() {
return new JsiSkRect(this.CanvasKit, this.ref.getBounds());
}
getFillType(): FillType {
const path = this.asPath();
const result = path.getFillType().value;
path.delete();
return result;
}
getPoint(index: number): SkPoint {
const path = this.asPath();
const result = new JsiSkPoint(this.CanvasKit, path.getPoint(index));
path.delete();
return result;
}
isEmpty() {
return this.ref.isEmpty();
}
isVolatile() {
return false;
}
getLastPt() {
const count = this.ref.countPoints();
if (count === 0) {
return { x: 0, y: 0 };
}
const path = this.asPath();
const pt = path.getPoint(count - 1);
path.delete();
return { x: pt[0], y: pt[1] };
}
toSVGString() {
const path = this.asPath();
const result = path.toSVGString();
path.delete();
return result;
}
isInterpolatable(path2: SkPath): boolean {
const p1 = this.asPath();
const p2 = JsiSkPath.fromValue<CKPathBuilder>(path2).snapshot();
const result = this.CanvasKit.Path.CanInterpolate(p1, p2);
p1.delete();
p2.delete();
return result;
}
interpolate(end: SkPath, weight: number, output?: SkPath): SkPath | null {
const p1 = this.asPath();
const p2 = JsiSkPath.fromValue<CKPathBuilder>(end).snapshot();
const path = this.CanvasKit.Path.MakeFromPathInterpolation(p1, p2, weight);
p1.delete();
p2.delete();
if (path === null) {
return null;
}
if (output) {
const outRef = output as JsiSkPath;
const old = outRef.ref;
outRef.ref = new this.CanvasKit.PathBuilder(path);
path.delete();
old.delete();
return output;
}
const result = new JsiSkPath(
this.CanvasKit,
new this.CanvasKit.PathBuilder(path)
);
path.delete();
return result;
}
toCmds() {
const path = this.asPath();
const cmds = path.toCmds();
path.delete();
const result = cmds.reduce<PathCommand[]>((acc, cmd, i) => {
if (i === 0) {
acc.push([]);
}
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 acc;
}, []);
return result;
}
}