Skip to content
pixi-silk
GitHub repository
Shapes & strokes

Shape operations

Union, subtract, intersect and exclude shapes and SVG paths in PixiJS with fill({ op }) and boolean(). Results take strokes, gradients, blur and hit tests.

Updated pixi-silk 1.0.1View as Markdown

Shape operations combine several shapes into one: cut a hole in a card, make a ring from two circles or build an icon from a few primitives.

Pick an operation. Turn on blur to see the result blur as one shape.
g.circle(60, 60, 50).circle(60, 60, 30).fill({ color: 0x30d158, op: 'subtract' });

That draws a ring: the first circle minus the second. The result is painted once, so a translucent fill has no darker overlaps.

Combine the shapes of a path#

Add op to fill() or stroke(). It combines every closed shape on the active path.

g.rect(0, 0, 80, 80).circle(80, 80, 40).fill({ color: 0x0a84ff, op: 'intersect' }); // a quarter disc
g.circle(40, 40, 30).circle(70, 40, 30).fill({ color: 0xff375f, op: 'exclude' });   // two lenses
opResult
'union'Everything any shape covers. The same as union: true.
'subtract'The first shape minus every later one
'intersect'Only what every shape covers
'exclude'What an odd number of shapes cover

A stroke() right after the fill follows the outline of the result. A stroke with its own op strokes the outline without a fill.

g.roundRect(0, 0, 120, 80, 16)
    .circle(60, 40, 24)
    .fill({ color: 0x1c1c1e, op: 'subtract' })
    .stroke({ width: 2, color: 0xffffff });

Open shapes take no part. Arcs and areas in the path stroke on their own.

Nest operations with boolean()#

boolean(op, a, b) adds one shape to the active path: what callback a draws, combined by op with what callback b draws. The shapes inside each callback are merged first. A callback can call boolean() again, so any tree of operations works.

// a ring with a notch, and a dot in the middle
g.boolean(
    'union',
    (g) =>
        g.boolean(
            'subtract',
            (g) => g.circle(50, 50, 40),
            (g) => g.circle(50, 50, 24).rect(46, 0, 8, 30),
        ),
    (g) => g.circle(50, 50, 8),
).fill(0xff9f0a);

The callbacks only add shapes. Do not call fill() or stroke() inside them. Up to 8 operands can wait on each other at once. A deeper tree is skipped with a warning.

Cut with SVG paths#

g.svgPath('M12 2 L22 22 H2 Z').fill(0xffd60a);
g.svgPath('M4 4 H20 V20 H4 Z').circle(12, 12, 4).fill({ color: 0xffffff, op: 'subtract' });

svgPath(d) reads the d attribute of an SVG path into the active path. It supports M, L, H, V, C, S, Q, T, A and Z, absolute and relative. A fill draws it as one even-odd shape, so the inner square of M0 0 H60 V60 H0 Z M20 20 V40 H40 V20 Z is a hole.

In a shape operation, a path from svgPath() or moveTo() is one operand with all its subpaths. With 'union', each subpath counts on its own.

Paint, effects and hit tests#

Everything that paints a closed shape works on the result: colours, gradients, blur, a progressive blur, spread, inner shadows and blend: 'add'. Gradients in shape units span the bounds of the result.

The outline of the result is exact. Inside the shape and far outside it, the combined distance is a close bound instead. A blur, spread or inner shadow wider than a corner can round that corner a little more than on a plain shape. Keep blurs in proportion for crisp cutouts.

containsPoint() follows the result. A ring is hollow, a cutout lets clicks through and an intersection only hits where the shapes overlap.

Cost#

A combined shape is one instance. Each pixel evaluates every shape in it once, so ten shapes in an operation cost about what ten separate shapes cost. A path in an operation costs one step per edge.

API reference: SilkGraphics, ShapeOp