The viewport clips the reels through a MaskStrategy so buffer cells never leak past the grid. Everything here is additive; the default RectMaskStrategy is unchanged.
| Board | Strategy |
|---|---|
| Square frame art | RectMaskStrategy (default) |
| Rounded frame, reels flush | RoundedRectMaskStrategy (scope: 'set') |
| Reels as separate cards, with a cross gap | RoundedRectMaskStrategy (scope: 'reel') |
| Rounded frame, one rect per reel, any gap | RoundedRectMaskStrategy (scope: 'outer') |
| Hold & Win board, a mask per cell | cellMask((cell, { corners }) => ...) |
| Pyramid / MultiWays, rounded, reels flush | SilhouetteMaskStrategy |
| Hexes, holes, cut-outs | PathMaskStrategy |
inset(strategy, px) and composeMasks(a, b, ...) wrap any of them, yours included.
A rounded window#
scope: 'set' rounds the union bounding box. Safe at any cross gap, so it is the default.
Loading recipe…
Each reel as its own card#
scope: 'reel' rounds every reel box instead.
Gotcha: needs a non-zero CROSS gap. At gap 0 neighbours share an edge and rounding both sides of it notches every seam. The strategy warns once.
Loading recipe…
One rect per reel, the window’s corners only#
scope: 'outer' keeps one rect per reel, so buffer cells past a short reel stay hidden as with the default, and rounds a corner only where it sits on a corner of the union box: the first reel’s outer pair, the last reel’s outer pair, nothing in between. Inner edges stay square, so it is fine at cross gap 0, where scope: 'reel' notches. corners: { topLeft: true, ... } narrows any scope to named screen corners.
Gotcha: on a jagged set the box corners may touch no reel, and then nothing rounds. Use SilhouetteMaskStrategy there.
Loading recipe…
A mask per Hold & Win cell, from its row and column#
Every Hold & Win cell is its own 1x1 reel set with its own mask, and cellMask takes a function called once per cell with { reel, cell } (column, row) and { cols, rows, corners }. Return any strategy per cell: here a plain rect everywhere and one rounded corner on each of the four corner cells, so a gapless board clips as one rounded window built from fifteen masks. corners is that per-cell corner set precomputed, for the short form (_, { corners }) => new RoundedRectMaskStrategy({ radius, corners }).
Loading recipe…
Rounding a jagged board#
A pyramid has no single rectangle to round: per-reel rounding notches the seams, bounding-box rounding hides the staircase. SilhouetteMaskStrategy walks the union outline and rounds every vertex, concave steps included via concaveRadius. A uniform set degrades to a rounded rect.
Gotcha: reels must butt together (cross gap 0). With a gap they are genuinely disjoint; the strategy says so and falls back to per-reel rounded rects.
Loading recipe…
Any shape, without a class#
PathMaskStrategy takes a (graphics, context) => void. The context carries rects, width, height, axis and bleed. Do not call g.clear() - the strategy already did.
Gotcha: cut() subtracts from the last shape already filled, so fill first. Draw the hole before filling and you get an empty mask and an invisible board, with no error.
Loading recipe…
Insetting and composing#
inset(strategy, px) shrinks any strategy’s output (negative grows) - the fix for art bleeding a pixel past the frame. Pass { top, right, bottom, left } instead of one number when the frame’s lips differ: inset(new RoundedRectMaskStrategy({ radius: 43 }), { top: 4, bottom: 12 }). Sides are screen sides in every orientation and an omitted side is untouched. composeMasks(...) unions several into the viewport’s one mask, for a board plus a banner.
Gotcha: union only. A Graphics mask is the union of its filled shapes; there is no way to intersect or subtract two strategies. Use .cut() inside a PathMaskStrategy instead.
Loading recipe…
A different trim per side#
Frame art rarely has four equal lips. inset(strategy, { top, right, bottom, left }) names the sides the way the artist measured them - screen sides in every orientation, an omitted side untouched - so a rounded window with a thick bottom lip is one wrapped strategy, not a hand-drawn path.
Loading recipe…
Writing your own#
Implement build(ctx) and update(g, ctx), set version = MASK_STRATEGY_VERSION, pass it to .maskStrategy(...). Reach for PathMaskStrategy first; a class only earns its keep when it holds state across draws.
Respect two things: ctx.axis, because rects are screen-space in every orientation (axis.toLocal(w, h) gives { cross, main }), and ctx.bleed, which inflates the CROSS axis only. Add the optional draw(g, ctx) and your strategy composes like the built-ins.