Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions apps/docs/docs/animations/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ import {useSharedValue, withSpring} from "react-native-reanimated";
import {Gesture, GestureDetector} from "react-native-gesture-handler";
import {usePathValue, Canvas, Path, processTransform3d, Skia} from "@shopify/react-native-skia";

const rrct = Skia.Path.Make();
rrct.addRRect(Skia.RRectXY(Skia.XYWHRect(0, 0, 100, 100), 10, 10));
const rrct = Skia.PathBuilder.Make()
.addRRect(Skia.RRectXY(Skia.XYWHRect(0, 0, 100, 100), 10, 10))
.build();

export const FrostedCard = () => {
const rotateY = useSharedValue(0);
Expand All @@ -60,17 +61,23 @@ export const FrostedCard = () => {
rotateY.value -= event.changeX / 300;
});

const clip = usePathValue((path) => {
"worklet";
path.transform(
processTransform3d([
{ translate: [50, 50] },
{ perspective: 300 },
{ rotateY: rotateY.value },
{ translate: [-50, -50] },
])
);
}, rrct);
const clip = usePathValue(
() => {
"worklet";
},
rrct,
(path) => {
"worklet";
return path.transform(
processTransform3d([
{ translate: [50, 50] },
{ perspective: 300 },
{ rotateY: rotateY.value },
{ translate: [-50, -50] },
])
);
}
);
return (
<GestureDetector gesture={gesture}>
<Canvas style={{ flex: 1 }}>
Expand Down
242 changes: 242 additions & 0 deletions apps/docs/docs/shapes/path-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
---
id: path-migration
title: Path API Migration Guide
sidebar_label: Migration Guide
slug: /shapes/path-migration
---

This guide helps you migrate from the mutable Path API to the new immutable Path API with `PathBuilder`.

## Why the Change?

The new API aligns with Skia's native direction toward immutable paths:
- **Immutable `SkPath`**: Paths are now immutable with query methods only
- **Mutable `SkPathBuilder`**: Use `PathBuilder` for path construction, then call `.build()` to get an immutable path
- **Static factories**: Common shapes can be created directly via `Skia.Path.Circle()`, `Skia.Path.Rect()`, etc.
- **Static operations**: Path operations like `Stroke()`, `Trim()`, `Simplify()` are now static methods on `Skia.Path`

## Basic Migration

### Before (Mutable API)

```tsx
// Building a path
const path = Skia.Path.Make();
path.moveTo(0, 0);
path.lineTo(100, 100);
path.close();

// Adding shapes
const circle = Skia.Path.Make();
circle.addCircle(50, 50, 25);

// Transforming (mutated in place)
path.transform(matrix);

// Operations (mutated in place)
path.stroke({ width: 2 });
path.simplify();
```

### After (Immutable API)

```tsx
// Building a path with PathBuilder
const path = Skia.PathBuilder.Make()
.moveTo(0, 0)
.lineTo(100, 100)
.close()
.build();

// Adding shapes - use static factories
const circle = Skia.Path.Circle(50, 50, 25);

// Transforming - returns new path
const transformed = path.transform(matrix);

// Operations - use static methods
const stroked = Skia.Path.Stroke(path, { width: 2 });
const simplified = Skia.Path.Simplify(path);
```

## Static Factory Methods

Instead of creating an empty path and calling `add*` methods, use static factories:

| Old API | New API |
|---------|---------|
| `path.addCircle(x, y, r)` | `Skia.Path.Circle(x, y, r)` |
| `path.addRect(rect)` | `Skia.Path.Rect(rect)` |
| `path.addOval(rect)` | `Skia.Path.Oval(rect)` |
| `path.addRRect(rrect)` | `Skia.Path.RRect(rrect)` |
| `path.moveTo()/lineTo()` | `Skia.Path.Line(p1, p2)` |
| Multiple `path.lineTo()` | `Skia.Path.Polygon(points, close)` |

## Static Path Operations

Operations that previously mutated the path are now static methods returning new paths:

| Old API | New API |
|---------|---------|
| `path.stroke(opts)` | `Skia.Path.Stroke(path, opts)` |
| `path.trim(start, end)` | `Skia.Path.Trim(path, start, end, false)` |
| `path.simplify()` | `Skia.Path.Simplify(path)` |
| `path.dash(on, off, phase)` | `Skia.Path.Dash(path, on, off, phase)` |
| `path.makeAsWinding()` | `Skia.Path.AsWinding(path)` |
| `path1.interpolate(path2, t)` | `Skia.Path.Interpolate(path1, path2, t)` |

Note: Static operations may return `null` if the operation fails. Handle this appropriately:

```tsx
const stroked = Skia.Path.Stroke(path, { width: 2 });
if (stroked) {
// use stroked path
}
// Or with fallback
const result = Skia.Path.Stroke(path, { width: 2 }) ?? path;
```

## Transform and Offset

These methods now return new paths instead of mutating:

```tsx
// Before
path.transform(matrix); // mutated path
path.offset(dx, dy); // mutated path

// After
const transformed = path.transform(matrix); // new path
const offsetPath = path.offset(dx, dy); // new path
```

## Using PathBuilder

When you need to build complex paths programmatically:

```tsx
const builder = Skia.PathBuilder.Make();
builder.moveTo(0, 0);
builder.lineTo(100, 0);
builder.quadTo(150, 50, 100, 100);
builder.cubicTo(50, 150, 0, 100, 0, 50);
builder.close();

// Get the immutable path
const path = builder.build();
```

`PathBuilder` supports method chaining:

```tsx
const path = Skia.PathBuilder.Make()
.moveTo(0, 0)
.lineTo(100, 100)
.arcTo(50, 50, 0, true, true, 100, 0)
.close()
.build();
```

## Combining Paths

To combine an existing path with new elements:

```tsx
const basePath = Skia.Path.MakeFromSVGString("M10 10 L90 90")!;
const combined = Skia.PathBuilder.MakeFromPath(basePath)
.lineTo(90, 10)
.close()
.build();
```

## Animations with usePathValue

The `usePathValue` hook now takes an optional transform function:

```tsx
// Before
const clip = usePathValue((path) => {
"worklet";
path.transform(matrix.value);
}, initialPath);

// After
const clip = usePathValue(
() => {
"worklet";
// Build operations go here
},
initialPath,
(path) => {
"worklet";
// Post-build transform
return path.transform(matrix.value);
}
);
```

## Dynamic Path Building in Worklets

When building paths dynamically in worklets (e.g., gesture handlers), use `PathBuilder` as a shared value:

```tsx
const pathBuilder = useSharedValue(Skia.PathBuilder.Make());

const gesture = Gesture.Pan()
.onStart((e) => {
pathBuilder.value.reset();
pathBuilder.value.moveTo(e.x, e.y);
})
.onChange((e) => {
pathBuilder.value.lineTo(e.x, e.y);
});

// Convert to path for rendering
const path = useDerivedValue(() => {
return pathBuilder.value.build();
});
```

## Quick Reference

### PathBuilder Methods

Construction:
- `moveTo(x, y)`, `lineTo(x, y)`, `quadTo(...)`, `cubicTo(...)`, `conicTo(...)`
- `rMoveTo(...)`, `rLineTo(...)`, `rQuadTo(...)`, `rCubicTo(...)`, `rConicTo(...)`
- `arcTo(...)`, `arcToOval(...)`, `arcToRotated(...)`
- `addRect(...)`, `addOval(...)`, `addCircle(...)`, `addRRect(...)`, `addArc(...)`, `addPath(...)`
- `close()`, `reset()`
- `setFillType(...)`, `setIsVolatile(...)`
- `build()` → returns immutable `SkPath`

### SkPath Static Methods

Factories:
- `Skia.Path.Circle(x, y, r)`
- `Skia.Path.Rect(rect)`
- `Skia.Path.Oval(rect)`
- `Skia.Path.RRect(rrect)`
- `Skia.Path.Line(p1, p2)`
- `Skia.Path.Polygon(points, close)`

Operations:
- `Skia.Path.Stroke(path, opts)` → `SkPath | null`
- `Skia.Path.Trim(path, start, end, complement)` → `SkPath | null`
- `Skia.Path.Simplify(path)` → `SkPath | null`
- `Skia.Path.Dash(path, on, off, phase)` → `SkPath | null`
- `Skia.Path.AsWinding(path)` → `SkPath | null`
- `Skia.Path.Interpolate(start, end, weight)` → `SkPath | null`

### SkPath Instance Methods (Immutable)

Query:
- `getBounds()`, `computeTightBounds()`, `contains(x, y)`
- `getFillType()`, `isVolatile()`, `isEmpty()`
- `countPoints()`, `getPoint(index)`, `getLastPt()`
- `toSVGString()`, `toCmds()`, `equals(other)`, `copy()`
- `isInterpolatable(other)`

Transform (returns new path):
- `transform(matrix)` → `SkPath`
- `offset(dx, dy)` → `SkPath`
39 changes: 20 additions & 19 deletions apps/docs/docs/shapes/path.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ In Skia, paths are semantically identical to [SVG Paths](https://developer.mozil

| Name | Type | Description |
|:----------|:----------|:--------------------------------------------------------------|
| 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()`. |
| 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`. |
| start | `number` | Trims the start of the path. Value is in the range `[0, 1]` (default is 0). |
| end | `number` | Trims the end of the path. Value is in the range `[0, 1]` (default is 1). |
| 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` |
Expand Down Expand Up @@ -40,19 +40,20 @@ const SVGNotation = () => {
```tsx twoslash
import {Canvas, Path, Skia} from "@shopify/react-native-skia";

const path = Skia.Path.Make();
path.moveTo(128, 0);
path.lineTo(168, 80);
path.lineTo(256, 93);
path.lineTo(192, 155);
path.lineTo(207, 244);
path.lineTo(128, 202);
path.lineTo(49, 244);
path.lineTo(64, 155);
path.lineTo(0, 93);
path.lineTo(88, 80);
path.lineTo(128, 0);
path.close();
const path = Skia.PathBuilder.Make()
.moveTo(128, 0)
.lineTo(168, 80)
.lineTo(256, 93)
.lineTo(192, 155)
.lineTo(207, 244)
.lineTo(128, 202)
.lineTo(49, 244)
.lineTo(64, 155)
.lineTo(0, 93)
.lineTo(88, 80)
.lineTo(128, 0)
.close()
.build();

const PathDemo = () => {
return (
Expand Down Expand Up @@ -105,19 +106,19 @@ import {Canvas, Skia, Fill, Path} from "@shopify/react-native-skia";
const star = () => {
const R = 115.2;
const C = 128.0;
const path = Skia.Path.Make();
path.moveTo(C + R, C);
const builder = Skia.PathBuilder.Make();
builder.moveTo(C + R, C);
for (let i = 1; i < 8; ++i) {
const a = 2.6927937 * i;
path.lineTo(C + R * Math.cos(a), C + R * Math.sin(a));
builder.lineTo(C + R * Math.cos(a), C + R * Math.sin(a));
}
return path;
return builder.build();
};

export const HelloWorld = () => {
const path = star();
return (
<Canvas style={{ flex: 1 }}>
<Canvas style={{ flex: 1 }}>
<Fill color="white" />
<Path path={path} style="stroke" strokeWidth={4} color="#3EB489"/>
<Path path={path} color="lightblue" fillType="evenOdd" />
Expand Down
5 changes: 2 additions & 3 deletions apps/docs/docs/text/path.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Draws text along a path.

| Name | Type | Description |
|:------------|:-------------------|:-------------------------------------------------------------|
| 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()` |
| 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` |
| text | `string` | Text to draw |
| font | `SkFont` | Font to use |

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

const size = 128;
const path = Skia.Path.Make();
path.addCircle(size, size, size/2);
const path = Skia.Path.Circle(size, size, size/2);

export const HelloWorld = () => {
const font = useFont(require("./my-font.ttf"), 24);
Expand Down
1 change: 1 addition & 0 deletions apps/docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const sidebars = {
label: "Shapes",
items: [
"shapes/path",
"shapes/path-migration",
"shapes/polygons",
"shapes/ellipses",
"shapes/atlas",
Expand Down
Binary file added apps/docs/static/img/group/clip-path.png.test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading