Skip to main content

Path API Migration Guide

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)

// 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)

// 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 APINew 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 APINew 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:

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:

// 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:

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:

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:

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:

// 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:

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