canvaskit-wasm 0.39 build 2026-04-29

PathOp

The boolean operation passed to CK.Path.MakeFromOp(a, b, op). Combines two paths into one, treating each as a 2D region. Output is a fresh Path made of cubic Bezier segments wherever Skia rounds intersection corners.

const op = controls.select( 'op', ['Union', 'Intersect', 'Difference', 'ReverseDifference', 'XOR'], 'Intersect' ); const a = new CK.Path(); a.addCircle(165, 130, 70); const b = new CK.Path(); b.addCircle(255, 130, 70); const ghost = new CK.Paint(); ghost.setStyle(CK.PaintStyle.Stroke); ghost.setColor(CK.Color(160, 160, 160, 0.6)); const fill = new CK.Paint(); fill.setColor(CK.Color(60, 160, 90, 0.4)); 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); canvas.drawPath(a, ghost); canvas.drawPath(b, ghost); const out = CK.Path.MakeFromOp(a, b, CK.PathOp[op()]); if (out) { canvas.drawPath(out, fill); canvas.drawPath(out, edge); out.delete(); } surface.flush(); });

Here is another example where you could see points of the result path

const op = controls.select( 'mode', ['shapes', 'union', 'intersect', 'difference', 'xor'], 'intersect' ); const star = new CK.Path(); makeStar(star, 200, 130, 80, 35, 5); const planeSvg = "M16.63,105.75c0.01-4.03,2.3-7.97,6.03-12.38L1.09,79.73c-1.36-0.59-1.33-1.42-0.54-2.4l4.57-3.9c0.83-0.51,1.71-0.73,2.66-0.47l26.62,4.5l22.18-24.02L4.8,18.41c-1.31-0.77-1.42-1.64-0.07-2.65l7.47-5.96l67.5,18.97L99.64,7.45c6.69-5.79,13.19-8.38,18.18-7.15c2.75,0.68,3.72,1.5,4.57,4.08c1.65,5.06-0.91,11.86-6.96,18.86L94.11,43.18l18.97,67.5l-5.96,7.47c-1.01,1.34-1.88,1.23-2.65-0.07L69.43,66.31L45.41,88.48l4.5,26.62c0.26,0.94,0.05,1.82-0.47,2.66l-3.9,4.57c-0.97,0.79-1.81,0.82-2.4-0.54l-13.64-21.57c-4.43,3.74-8.37,6.03-12.42,6.03C16.71,106.24,16.63,106.11,16.63,105.75L16.63,105.75z"; const planeBase = CK.Path.MakeFromSVGString(planeSvg); const pb = planeBase.computeTightBounds(); planeBase.offset(-(pb[0] + pb[2]) / 2, -(pb[1] + pb[3]) / 2); const k = 0.9, c45 = cos(PI / 4), s45 = sin(PI / 4); planeBase.transform([k * c45, -k * s45, 0, k * s45, k * c45, 0]); const plane = new CK.Path(); const ghost = new CK.Paint(); ghost.setStyle(CK.PaintStyle.Stroke); ghost.setColor(CK.Color(160, 160, 160, 0.6)); const fill = new CK.Paint(); fill.setColor(CK.Color(60, 160, 90, 0.3)); 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); const PATH_OPS = { union: CK.PathOp.Union, intersect: CK.PathOp.Intersect, difference: CK.PathOp.Difference, xor: CK.PathOp.XOR, }; let lastOp = 'intersect'; loop(() => { plane.reset(); plane.addPath(planeBase); if(op() !== lastOp){ lastOp = op(); mouse.x = 200; mouse.y = 130; } plane.offset(mouse.x || 200, mouse.y || 130); canvas.clear(CK.WHITE); canvas.drawPath(star, ghost); canvas.drawPath(plane, ghost); if (op() === 'shapes') { canvas.drawAnchors(star); canvas.drawAnchors(plane); } else { const out = CK.Path.MakeFromOp(plane, star, PATH_OPS[op()]); canvas.drawPath(out, fill); canvas.drawPath(out, edge); canvas.drawTangents(out); out.delete(); } surface.flush(); }); function makeStar(p, cx, cy, R, r, n) { for (let i = 0; i < n * 2; i++) { const a = -PI / 2 + i * PI / n; const rr = i % 2 === 0 ? R : r; if (i === 0) p.moveTo(cx + cos(a) * rr, cy + sin(a) * rr); else p.lineTo(cx + cos(a) * rr, cy + sin(a) * rr); } p.close(); }

All values

ValueResultNotes
DifferenceA − BEverything in a that's not in b.
IntersectA ∩ BOnly the overlap.
UnionA ∪ BEverything in either path.
XORA ⊕ BEverything in exactly one of the two — overlap is excluded.
ReverseDifferenceB − AEverything in b that's not in a. Same as Difference with the inputs swapped.

See also

  • Path.MakeFromOp — the static factory that consumes this enum.
  • Path — what comes out the other side, ready for drawPath.
  • Paths and cubics — why boolean output uses cubics (planned).