Skip to content

Commit 6492f65

Browse files
authored
feat(🌳): new immutable Path API (#3734)
See migration guide at https://shopify.github.io/react-native-skia/docs/shapes/path-migration
1 parent ce61c40 commit 6492f65

78 files changed

Lines changed: 3955 additions & 1369 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

β€Žapps/docs/docs/animations/hooks.mdβ€Ž

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ import {useSharedValue, withSpring} from "react-native-reanimated";
5050
import {Gesture, GestureDetector} from "react-native-gesture-handler";
5151
import {usePathValue, Canvas, Path, processTransform3d, Skia} from "@shopify/react-native-skia";
5252

53-
const rrct = Skia.Path.Make();
54-
rrct.addRRect(Skia.RRectXY(Skia.XYWHRect(0, 0, 100, 100), 10, 10));
53+
const rrct = Skia.PathBuilder.Make()
54+
.addRRect(Skia.RRectXY(Skia.XYWHRect(0, 0, 100, 100), 10, 10))
55+
.build();
5556

5657
export const FrostedCard = () => {
5758
const rotateY = useSharedValue(0);
@@ -60,17 +61,23 @@ export const FrostedCard = () => {
6061
rotateY.value -= event.changeX / 300;
6162
});
6263

63-
const clip = usePathValue((path) => {
64-
"worklet";
65-
path.transform(
66-
processTransform3d([
67-
{ translate: [50, 50] },
68-
{ perspective: 300 },
69-
{ rotateY: rotateY.value },
70-
{ translate: [-50, -50] },
71-
])
72-
);
73-
}, rrct);
64+
const clip = usePathValue(
65+
() => {
66+
"worklet";
67+
},
68+
rrct,
69+
(path) => {
70+
"worklet";
71+
return path.transform(
72+
processTransform3d([
73+
{ translate: [50, 50] },
74+
{ perspective: 300 },
75+
{ rotateY: rotateY.value },
76+
{ translate: [-50, -50] },
77+
])
78+
);
79+
}
80+
);
7481
return (
7582
<GestureDetector gesture={gesture}>
7683
<Canvas style={{ flex: 1 }}>
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
---
2+
id: path-migration
3+
title: Path API Migration Guide
4+
sidebar_label: Migration Guide
5+
slug: /shapes/path-migration
6+
---
7+
8+
This guide helps you migrate from the mutable Path API to the new immutable Path API with `PathBuilder`.
9+
10+
## Why the Change?
11+
12+
The new API aligns with Skia's native direction toward immutable paths:
13+
- **Immutable `SkPath`**: Paths are now immutable with query methods only
14+
- **Mutable `SkPathBuilder`**: Use `PathBuilder` for path construction, then call `.build()` to get an immutable path
15+
- **Static factories**: Common shapes can be created directly via `Skia.Path.Circle()`, `Skia.Path.Rect()`, etc.
16+
- **Static operations**: Path operations like `Stroke()`, `Trim()`, `Simplify()` are now static methods on `Skia.Path`
17+
18+
## Basic Migration
19+
20+
### Before (Mutable API)
21+
22+
```tsx
23+
// Building a path
24+
const path = Skia.Path.Make();
25+
path.moveTo(0, 0);
26+
path.lineTo(100, 100);
27+
path.close();
28+
29+
// Adding shapes
30+
const circle = Skia.Path.Make();
31+
circle.addCircle(50, 50, 25);
32+
33+
// Transforming (mutated in place)
34+
path.transform(matrix);
35+
36+
// Operations (mutated in place)
37+
path.stroke({ width: 2 });
38+
path.simplify();
39+
```
40+
41+
### After (Immutable API)
42+
43+
```tsx
44+
// Building a path with PathBuilder
45+
const path = Skia.PathBuilder.Make()
46+
.moveTo(0, 0)
47+
.lineTo(100, 100)
48+
.close()
49+
.build();
50+
51+
// Adding shapes - use static factories
52+
const circle = Skia.Path.Circle(50, 50, 25);
53+
54+
// Transforming - returns new path
55+
const transformed = path.transform(matrix);
56+
57+
// Operations - use static methods
58+
const stroked = Skia.Path.Stroke(path, { width: 2 });
59+
const simplified = Skia.Path.Simplify(path);
60+
```
61+
62+
## Static Factory Methods
63+
64+
Instead of creating an empty path and calling `add*` methods, use static factories:
65+
66+
| Old API | New API |
67+
|---------|---------|
68+
| `path.addCircle(x, y, r)` | `Skia.Path.Circle(x, y, r)` |
69+
| `path.addRect(rect)` | `Skia.Path.Rect(rect)` |
70+
| `path.addOval(rect)` | `Skia.Path.Oval(rect)` |
71+
| `path.addRRect(rrect)` | `Skia.Path.RRect(rrect)` |
72+
| `path.moveTo()/lineTo()` | `Skia.Path.Line(p1, p2)` |
73+
| Multiple `path.lineTo()` | `Skia.Path.Polygon(points, close)` |
74+
75+
## Static Path Operations
76+
77+
Operations that previously mutated the path are now static methods returning new paths:
78+
79+
| Old API | New API |
80+
|---------|---------|
81+
| `path.stroke(opts)` | `Skia.Path.Stroke(path, opts)` |
82+
| `path.trim(start, end)` | `Skia.Path.Trim(path, start, end, false)` |
83+
| `path.simplify()` | `Skia.Path.Simplify(path)` |
84+
| `path.dash(on, off, phase)` | `Skia.Path.Dash(path, on, off, phase)` |
85+
| `path.makeAsWinding()` | `Skia.Path.AsWinding(path)` |
86+
| `path1.interpolate(path2, t)` | `Skia.Path.Interpolate(path1, path2, t)` |
87+
88+
Note: Static operations may return `null` if the operation fails. Handle this appropriately:
89+
90+
```tsx
91+
const stroked = Skia.Path.Stroke(path, { width: 2 });
92+
if (stroked) {
93+
// use stroked path
94+
}
95+
// Or with fallback
96+
const result = Skia.Path.Stroke(path, { width: 2 }) ?? path;
97+
```
98+
99+
## Transform and Offset
100+
101+
These methods now return new paths instead of mutating:
102+
103+
```tsx
104+
// Before
105+
path.transform(matrix); // mutated path
106+
path.offset(dx, dy); // mutated path
107+
108+
// After
109+
const transformed = path.transform(matrix); // new path
110+
const offsetPath = path.offset(dx, dy); // new path
111+
```
112+
113+
## Using PathBuilder
114+
115+
When you need to build complex paths programmatically:
116+
117+
```tsx
118+
const builder = Skia.PathBuilder.Make();
119+
builder.moveTo(0, 0);
120+
builder.lineTo(100, 0);
121+
builder.quadTo(150, 50, 100, 100);
122+
builder.cubicTo(50, 150, 0, 100, 0, 50);
123+
builder.close();
124+
125+
// Get the immutable path
126+
const path = builder.build();
127+
```
128+
129+
`PathBuilder` supports method chaining:
130+
131+
```tsx
132+
const path = Skia.PathBuilder.Make()
133+
.moveTo(0, 0)
134+
.lineTo(100, 100)
135+
.arcTo(50, 50, 0, true, true, 100, 0)
136+
.close()
137+
.build();
138+
```
139+
140+
## Combining Paths
141+
142+
To combine an existing path with new elements:
143+
144+
```tsx
145+
const basePath = Skia.Path.MakeFromSVGString("M10 10 L90 90")!;
146+
const combined = Skia.PathBuilder.MakeFromPath(basePath)
147+
.lineTo(90, 10)
148+
.close()
149+
.build();
150+
```
151+
152+
## Animations with usePathValue
153+
154+
The `usePathValue` hook now takes an optional transform function:
155+
156+
```tsx
157+
// Before
158+
const clip = usePathValue((path) => {
159+
"worklet";
160+
path.transform(matrix.value);
161+
}, initialPath);
162+
163+
// After
164+
const clip = usePathValue(
165+
() => {
166+
"worklet";
167+
// Build operations go here
168+
},
169+
initialPath,
170+
(path) => {
171+
"worklet";
172+
// Post-build transform
173+
return path.transform(matrix.value);
174+
}
175+
);
176+
```
177+
178+
## Dynamic Path Building in Worklets
179+
180+
When building paths dynamically in worklets (e.g., gesture handlers), use `PathBuilder` as a shared value:
181+
182+
```tsx
183+
const pathBuilder = useSharedValue(Skia.PathBuilder.Make());
184+
185+
const gesture = Gesture.Pan()
186+
.onStart((e) => {
187+
pathBuilder.value.reset();
188+
pathBuilder.value.moveTo(e.x, e.y);
189+
})
190+
.onChange((e) => {
191+
pathBuilder.value.lineTo(e.x, e.y);
192+
});
193+
194+
// Convert to path for rendering
195+
const path = useDerivedValue(() => {
196+
return pathBuilder.value.build();
197+
});
198+
```
199+
200+
## Quick Reference
201+
202+
### PathBuilder Methods
203+
204+
Construction:
205+
- `moveTo(x, y)`, `lineTo(x, y)`, `quadTo(...)`, `cubicTo(...)`, `conicTo(...)`
206+
- `rMoveTo(...)`, `rLineTo(...)`, `rQuadTo(...)`, `rCubicTo(...)`, `rConicTo(...)`
207+
- `arcTo(...)`, `arcToOval(...)`, `arcToRotated(...)`
208+
- `addRect(...)`, `addOval(...)`, `addCircle(...)`, `addRRect(...)`, `addArc(...)`, `addPath(...)`
209+
- `close()`, `reset()`
210+
- `setFillType(...)`, `setIsVolatile(...)`
211+
- `build()` β†’ returns immutable `SkPath`
212+
213+
### SkPath Static Methods
214+
215+
Factories:
216+
- `Skia.Path.Circle(x, y, r)`
217+
- `Skia.Path.Rect(rect)`
218+
- `Skia.Path.Oval(rect)`
219+
- `Skia.Path.RRect(rrect)`
220+
- `Skia.Path.Line(p1, p2)`
221+
- `Skia.Path.Polygon(points, close)`
222+
223+
Operations:
224+
- `Skia.Path.Stroke(path, opts)` β†’ `SkPath | null`
225+
- `Skia.Path.Trim(path, start, end, complement)` β†’ `SkPath | null`
226+
- `Skia.Path.Simplify(path)` β†’ `SkPath | null`
227+
- `Skia.Path.Dash(path, on, off, phase)` β†’ `SkPath | null`
228+
- `Skia.Path.AsWinding(path)` β†’ `SkPath | null`
229+
- `Skia.Path.Interpolate(start, end, weight)` β†’ `SkPath | null`
230+
231+
### SkPath Instance Methods (Immutable)
232+
233+
Query:
234+
- `getBounds()`, `computeTightBounds()`, `contains(x, y)`
235+
- `getFillType()`, `isVolatile()`, `isEmpty()`
236+
- `countPoints()`, `getPoint(index)`, `getLastPt()`
237+
- `toSVGString()`, `toCmds()`, `equals(other)`, `copy()`
238+
- `isInterpolatable(other)`
239+
240+
Transform (returns new path):
241+
- `transform(matrix)` β†’ `SkPath`
242+
- `offset(dx, dy)` β†’ `SkPath`

β€Žapps/docs/docs/shapes/path.mdβ€Ž

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ In Skia, paths are semantically identical to [SVG Paths](https://developer.mozil
99

1010
| Name | Type | Description |
1111
|:----------|:----------|:--------------------------------------------------------------|
12-
| path | `SkPath` or `string` | Path to draw. Can be a string using the [SVG Path notation](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#line_commands) or an object created with `Skia.Path.Make()`. |
12+
| path | `SkPath` or `string` | Path to draw. Can be a string using the [SVG Path notation](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#line_commands) or an object created with `Skia.PathBuilder`. |
1313
| start | `number` | Trims the start of the path. Value is in the range `[0, 1]` (default is 0). |
1414
| end | `number` | Trims the end of the path. Value is in the range `[0, 1]` (default is 1). |
1515
| stroke | `StrokeOptions` | Turns this path into the filled equivalent of the stroked path. This will fail if the path is a hairline. `StrokeOptions` describes how the stroked path should look. It contains three properties: `width`, `strokeMiterLimit` and, `precision` |
@@ -40,19 +40,20 @@ const SVGNotation = () => {
4040
```tsx twoslash
4141
import {Canvas, Path, Skia} from "@shopify/react-native-skia";
4242

43-
const path = Skia.Path.Make();
44-
path.moveTo(128, 0);
45-
path.lineTo(168, 80);
46-
path.lineTo(256, 93);
47-
path.lineTo(192, 155);
48-
path.lineTo(207, 244);
49-
path.lineTo(128, 202);
50-
path.lineTo(49, 244);
51-
path.lineTo(64, 155);
52-
path.lineTo(0, 93);
53-
path.lineTo(88, 80);
54-
path.lineTo(128, 0);
55-
path.close();
43+
const path = Skia.PathBuilder.Make()
44+
.moveTo(128, 0)
45+
.lineTo(168, 80)
46+
.lineTo(256, 93)
47+
.lineTo(192, 155)
48+
.lineTo(207, 244)
49+
.lineTo(128, 202)
50+
.lineTo(49, 244)
51+
.lineTo(64, 155)
52+
.lineTo(0, 93)
53+
.lineTo(88, 80)
54+
.lineTo(128, 0)
55+
.close()
56+
.build();
5657

5758
const PathDemo = () => {
5859
return (
@@ -105,19 +106,19 @@ import {Canvas, Skia, Fill, Path} from "@shopify/react-native-skia";
105106
const star = () => {
106107
const R = 115.2;
107108
const C = 128.0;
108-
const path = Skia.Path.Make();
109-
path.moveTo(C + R, C);
109+
const builder = Skia.PathBuilder.Make();
110+
builder.moveTo(C + R, C);
110111
for (let i = 1; i < 8; ++i) {
111112
const a = 2.6927937 * i;
112-
path.lineTo(C + R * Math.cos(a), C + R * Math.sin(a));
113+
builder.lineTo(C + R * Math.cos(a), C + R * Math.sin(a));
113114
}
114-
return path;
115+
return builder.build();
115116
};
116117

117118
export const HelloWorld = () => {
118119
const path = star();
119120
return (
120-
<Canvas style={{ flex: 1 }}>
121+
<Canvas style={{ flex: 1 }}>
121122
<Fill color="white" />
122123
<Path path={path} style="stroke" strokeWidth={4} color="#3EB489"/>
123124
<Path path={path} color="lightblue" fillType="evenOdd" />

β€Žapps/docs/docs/text/path.mdβ€Ž

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Draws text along a path.
99

1010
| Name | Type | Description |
1111
|:------------|:-------------------|:-------------------------------------------------------------|
12-
| path | `Path` or `string` | Path to draw. Can be a string using the [SVG Path notation](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#line_commands) or an object created with `Skia.Path.Make()` |
12+
| path | `Path` or `string` | Path to draw. Can be a string using the [SVG Path notation](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#line_commands) or an object created with `Skia.PathBuilder` |
1313
| text | `string` | Text to draw |
1414
| font | `SkFont` | Font to use |
1515

@@ -19,8 +19,7 @@ Draws text along a path.
1919
import {Canvas, Group, TextPath, Skia, useFont, vec, Fill} from "@shopify/react-native-skia";
2020

2121
const size = 128;
22-
const path = Skia.Path.Make();
23-
path.addCircle(size, size, size/2);
22+
const path = Skia.Path.Circle(size, size, size/2);
2423

2524
export const HelloWorld = () => {
2625
const font = useFont(require("./my-font.ttf"), 24);

β€Žapps/docs/sidebars.jsβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const sidebars = {
5454
label: "Shapes",
5555
items: [
5656
"shapes/path",
57+
"shapes/path-migration",
5758
"shapes/polygons",
5859
"shapes/ellipses",
5960
"shapes/atlas",
22.6 KB
Loading
830 KB
Loading

0 commit comments

Comments
Β (0)