canvaskit-wasm 0.39 build 2026-04-29

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