knowledge base/practices

A stroke on a union path redraws the seams that a single fill erased

Fact

fill() and stroke() do not treat a path's subpaths the same way. Filling composites the union of every subpath, so overlapping circles added to one path come out as one flat silhouette. Stroking outlines each subpath individually — including the ones buried inside the union — so the same path drawn with stroke() shows a full circle around every shape, not the outline of the combined form.

There is no fill rule that changes this. nonzero and evenodd decide which regions are inside; stroking is not an inside/outside question at all.

Why it matters

It bites immediately after doing the right thing. The way to get a blob with a uniform edge is to build every shape into one path and fill once; adding a rim or a highlight by calling stroke() on that same path then reinstates exactly the internal seams the single fill was there to remove.

The result reads as a rendering artifact rather than as the outline that was asked for, and the usual reflexes — lowering the alpha, changing the fill rule, reordering the shapes — all leave it untouched, because none of them is the variable.

How to apply

  • Fill the union path, then build a separate path holding only the silhouette you actually want outlined, and stroke that.
  • When no such single shape exists, get the edge from a gradient fill or an inner shadow instead of a stroke; a radial gradient across the union gives depth with no seams.
  • Deliberately stroking every subpath is a legitimate effect — a mesh, a wire drawing — and is the same call, so say which one you meant in a comment.
  • The related fill-side trap is overlapping translucent shapes compounding per fill().