Paths and cubics
Skia represents every curve in a Path with cubic Bezier segments. A circle is four cubics; the result of Path.MakeFromOp (boolean union/intersect) is mostly cubics; the SVG d= parser reads C / S / Q / T / A and emits cubics under the hood. Understanding cubics is understanding what Skia stores.
What a cubic is
Four points define one cubic Bezier segment:
- start — where the segment begins (carried over from the previous moveTo / lineTo / cubicTo end).
- control 1 — pulls the start of the curve toward itself.
- control 2 — pulls the end of the curve toward itself.
- end — where the segment finishes.
The curve always passes through start and end, never through the controls. The controls determine how aggressively the curve bends.
const p = new CK.Path();
const stroke = new CK.Paint();
stroke.setStyle(CK.PaintStyle.Stroke);
stroke.setStrokeWidth(2);
stroke.setColor(CK.Color(40, 90, 180, 1));
stroke.setAntiAlias(true);
loop(() => {
p.reset();
p.moveTo(60, 200);
// Drag your cursor to move control point 2.
p.cubicTo(120, 60, mouse.x || 280, mouse.y || 80, 360, 200);
canvas.clear(CK.WHITE);
canvas.drawPath(p, stroke);
canvas.drawAnchors(p);
canvas.drawTangents(p);
surface.flush();
});
The red lines drawn by canvas.drawTangents(path) are the segments from each anchor to its associated control point. Move your cursor — the second control point follows it; the curve bulges toward it.
Why every curve becomes a cubic
CanvasKit accepts more than just cubics. You can call quadTo (one control point), arcToTangent (specify a radius), addCircle (no control points at all). Internally, Skia normalizes everything into cubics or lines because:
- A single math model means GPU shaders, hit-testing, stroking, dashing, boolean ops, and serialization all share one code path.
- Cubics can approximate any other curve to arbitrary precision (a circle is exact within ~0.06% from 4 cubics).
So when you call path.toCmds(), you don't see a circle verb — you see four cubicTo verbs.
Verbs in toCmds()
path.toCmds() returns a flat Float32Array of [verb, …coords, verb, …coords, …]. Verb codes:
| Code | Verb | Coords |
|---|---|---|
| 0 | move |
x, y |
| 1 | line |
x, y |
| 2 | quad |
cx, cy, x, y |
| 3 | conic |
cx, cy, x, y, w |
| 4 | cubic |
c1x, c1y, c2x, c2y, x, y |
| 5 | close |
— |
The canvas.drawAnchors(path) and canvas.drawTangents(path) runtime helpers walk this array — same code anyone visualizing curves would write.
Boolean output is mostly cubics
CK.Path.MakeFromOp(a, b, op) produces a path whose corners are rounded with cubic approximations of arcs. Even if a and b were straight-line polygons, the intersection corners come back as cubics whenever Skia rounds them.
const a = new CK.Path(); a.addRect(CK.LTRBRect(80, 60, 240, 220));
const b = new CK.Path(); b.addCircle(220, 130, 60);
const fill = new CK.Paint();
fill.setColor(CK.Color(60, 160, 90, 0.35));
fill.setAntiAlias(true);
const edge = new CK.Paint();
edge.setStyle(CK.PaintStyle.Stroke);
edge.setStrokeWidth(2);
edge.setColor(CK.Color(60, 160, 90, 1));
edge.setAntiAlias(true);
loop(() => {
canvas.clear(CK.WHITE);
const out = CK.Path.MakeFromOp(a, b, CK.PathOp.Difference);
canvas.drawPath(out, fill);
canvas.drawPath(out, edge);
// Tangents only render where the verb is cubic — that's why the rectangle's
// straight edges stay clean and only the rounded notch shows handles.
canvas.drawTangents(out);
out.delete();
surface.flush();
});
See also
Path— every method that builds or inspects geometry.Path.toCmds,Path.toSVGString.Path.MakeFromOp,PathOp.PathEffect— modify the geometry before rasterization.