Search Docs
Loading...
Skip to content

Masks and Mattes

Show only part of a block with shape masks, or with a track matte that uses the rendering of another block.

10 mins
estimated time
GitHub

A mask is a shape that cuts its block down. A block can have more than one mask, and the masks combine in their order. A track matte masks a block with the rendered alpha or luminance of another block. Masks and mattes render on the canvas and in raster and PDF export. The editor has no mask or matte controls, so you set them with the block API. The Lottie importer on web and Node.js also creates masks and mattes from Lottie files.

This guide covers masks, the mask properties and modes, the mask stack, track mattes, the matte source rules and the export limits.

Creating a Mask#

Graphic, text and group blocks accept masks. supportsMasks returns true for them. createMask creates a mask block. Give it a shape with setShape and attach it with appendMask.

val purple = engine.block.createFill(FillType.Color)
engine.block.setColor(
block = purple,
property = "fill/color/value",
value = Color.fromRGBA(r = 0.5F, g = 0.2F, b = 0.9F, a = 1F),
)
val ringBlock = addSquare(x = 50F, fill = purple)
val graphicSupportsMasks = engine.block.supportsMasks(block = ringBlock) // true
val pageSupportsMasks = engine.block.supportsMasks(block = page) // false
// The ellipse fills the frame of the masked block
val outerMask = engine.block.createMask()
engine.block.setShape(block = outerMask, shape = engine.block.createShape(ShapeType.Ellipse))
engine.block.appendMask(block = ringBlock, maskBlock = outerMask)

The engine evaluates the mask shape in the frame of the masked block, so an ellipse shape fills the whole square. Any shape works, for example an ellipse, a star or a vector path. For the shape types, see Create Shapes. A mask without a shape has no effect.

Mask Properties#

Each mask has five properties. Set them with setEnum, setFloat and setBoolean on the mask block.

Property Type Default Effect
mask/mode Enum "Add" How the mask combines with the masks before it
mask/opacity Float 1 The coverage of the mask, from 0 to 1
mask/feather Float 0 The softness of the edge, in design units
mask/expansion Float 0 Grows the shape with a positive value and shrinks it with a negative value, in design units
mask/inverted Bool false Covers the area outside the shape instead of the area inside

We soften the edge of the ellipse and let 80 percent of the block show through it.

// Soften the edge by 20 units and let 80% of the block show through
engine.block.setFloat(block = outerMask, property = "mask/feather", value = 20F)
engine.block.setFloat(block = outerMask, property = "mask/opacity", value = 0.8F)

You can animate mask/opacity, mask/feather and mask/expansion with keyframes.

Combining Masks#

The masks of a block combine in the order of the stack. The mask/mode of each mask sets how it combines with the result of the masks before it.

Mode Result
"Add" Adds the shape
"Subtract" Removes the shape
"Intersect" Keeps only the area where the shape and the result overlap
"Lighten" Keeps the higher coverage of the shape and the result
"Darken" Keeps the lower coverage of the shape and the result
"Difference" Adds the shape and removes the area where both overlap
"None" Has no effect

We shrink a second ellipse by 100 units on each side and subtract it from the first ellipse. The result is a ring.

// Shrink a second ellipse by 100 units and subtract it to cut a ring
val innerMask = engine.block.createMask()
engine.block.setShape(block = innerMask, shape = engine.block.createShape(ShapeType.Ellipse))
engine.block.setFloat(block = innerMask, property = "mask/expansion", value = -100F)
engine.block.setEnum(block = innerMask, property = "mask/mode", value = "Subtract")
engine.block.appendMask(block = ringBlock, maskBlock = innerMask)

The engine skips "None" masks. When the first mask that applies uses "Subtract", it removes its shape from the whole block.

Inverting a Mask#

mask/inverted makes a mask cover the area outside its shape. An inverted star mask cuts a star-shaped hole into the block.

val teal = engine.block.createFill(FillType.Color)
engine.block.setColor(
block = teal,
property = "fill/color/value",
value = Color.fromRGBA(r = 0F, g = 0.6F, b = 0.6F, a = 1F),
)
val starBlock = addSquare(x = 550F, fill = teal)
// An inverted mask keeps the area outside its shape: a star-shaped hole
val starMask = engine.block.createMask()
engine.block.setShape(block = starMask, shape = engine.block.createShape(ShapeType.Star))
engine.block.setBoolean(block = starMask, property = "mask/inverted", value = true)
engine.block.appendMask(block = starBlock, maskBlock = starMask)

Managing the Mask Stack#

getMasks returns the masks of a block in their order. removeMask removes the mask at an index and does not destroy it. insertMask adds a mask at an index from 0 to the number of masks.

// [outerMask, innerMask]
val masks = engine.block.getMasks(block = ringBlock)
// Removing a mask does not destroy it
engine.block.removeMask(block = ringBlock, index = 1)
val maskCountAfterRemove = engine.block.getMasks(block = ringBlock).size
val removedMaskIsValid = engine.block.isValid(block = innerMask) // true
// Insert it again after the outer mask
engine.block.insertMask(block = ringBlock, maskBlock = innerMask, index = 1)

A mask can belong to more than one block. When you change it, all these blocks change.

Setting a Track Matte#

setMatte masks a block with the rendering of a source block. The mode selects the channel of the source.

Mode The block shows where the source is
MatteMode.ALPHA Opaque. This is the default.
MatteMode.ALPHA_INVERTED Transparent
MatteMode.LUMA Bright
MatteMode.LUMA_INVERTED Dark or transparent

We use a square with a white to black gradient as the source. With MatteMode.LUMA, the orange square fades from the top to the bottom.

val orange = engine.block.createFill(FillType.Color)
engine.block.setColor(
block = orange,
property = "fill/color/value",
value = Color.fromRGBA(r = 1F, g = 0.5F, b = 0F, a = 1F),
)
val target = addSquare(x = 1050F, fill = orange)
// The matte source: a gradient from white at the top to black at the bottom
val gradient = engine.block.createFill(FillType.LinearGradient)
engine.block.setGradientColorStops(
block = gradient,
property = "fill/gradient/colors",
colorStops = listOf(
GradientColorStop(stop = 0F, color = Color.fromRGBA(r = 1F, g = 1F, b = 1F, a = 1F)),
GradientColorStop(stop = 1F, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 1F)),
),
)
val source = addSquare(x = 1050F, fill = gradient)
// The target shows where the source is bright. The source is not drawn.
engine.block.setMatte(block = target, source = source, mode = MatteMode.LUMA)

The engine does not draw the matte source on its own. One source can be the matte of more than one block. When the source is a group, all children of the group form the matte. The source keeps its own position, so place it over the block.

Reading and Removing a Matte#

getMatte returns the source and the mode, or null when the block has no matte. To remove a matte, call setMatte with null as the source. The source then draws again.

// BlockMatte(source = source, mode = LUMA)
val matte = engine.block.getMatte(block = target)
// null
val ringMatte = engine.block.getMatte(block = ringBlock)
Log.i(TAG, "matte: $matte, ring matte: $ringMatte")

Matte Source Rules#

setMatte checks the source and throws an EngineException when a rule is broken.

  • The block and the source are graphic, text or group blocks.
  • The source is on the same page as the block.
  • The source is not the block itself or one of its parents.
  • The source is not a block that the block or one of its parents masks, and it does not contain such a block.
val errorCode = try {
// A block cannot use itself as its matte source
engine.block.setMatte(block = target, source = target)
null
} catch (error: EngineException) {
// BLOCK.MATTE_SOURCE_INVALID
error.code
}

insertChild and appendChild keep these rules too. A move that puts a matted block inside its source, or the block and its source on different pages, throws BLOCK.MATTE_SOURCE_INVALID. Move the block and its source together, for example as one group.

A source can draw nothing, for example when it is hidden or outside its time range. Then MatteMode.ALPHA or MatteMode.LUMA mattes hide the block, and MatteMode.ALPHA_INVERTED or MatteMode.LUMA_INVERTED mattes show it whole.

Exporting#

We export the page as PNG and as SVG. The PNG shows the soft ring, the star-shaped hole and the faded orange square.

val previewPng = engine.block.export(block = page, mimeType = MimeType.PNG)
// SVG export ignores the mask opacity and feather, and draws the matted block whole
val svg = engine.block.export(block = page, mimeType = MimeType.SVG)

SVG export has limits:

  • SVG export ignores mask/opacity and mask/feather. The mask shapes still cut the block.
  • SVG export does not apply mattes. A matted block exports whole, and the source is not drawn.
  • When you export a matted block alone, the export keeps its matte.

For more about export formats, see Export Overview.

Masks, Mattes and Other Block Operations#

  • Undo and redo restore the mask stack, the mask properties and the matte.
  • Saving and loading a scene keeps the masks and the matte with its mode.
  • Duplicating a block copies its masks and their shapes. A duplicate of the matted block alone uses the same source. A duplicate of a group that holds both blocks uses the copied source.
  • Destroying a block destroys its own masks. A mask that another block also uses stays. Destroying a mask removes it from the stack, and destroying the source removes the matte.
  • Scaling a block or changing the design unit also converts mask/feather and mask/expansion.

appendMask, insertMask, removeMask and setMatte need the layer/clipping scope. setMatte also needs layer/visibility on the source, because the source stops drawing on its own.

Troubleshooting#

  • The block does not accept masks (BLOCK.MASKS_UNSUPPORTED): only graphic, text and group blocks have a mask stack. Check supportsMasks first.
  • The mask is not accepted (BLOCK.MASK_INVALID): pass a block that createMask made.
  • The index is out of range (BLOCK.MASK_INDEX_OUT_OF_BOUNDS): insert at an index from 0 to the number of masks, and remove at an index below it.
  • The block cannot have or be a matte (BLOCK.MATTE_UNSUPPORTED): use graphic, text or group blocks on both sides.
  • The source is rejected (BLOCK.MATTE_SOURCE_INVALID): use a source on the same page that follows the matte source rules.
  • The block is not visible: a MatteMode.ALPHA or MatteMode.LUMA matte hides the block when its source is hidden or outside its time range.

API Reference#

Method Purpose
engine.block.supportsMasks(block=_) Check whether a block accepts masks.
engine.block.createMask() Create a mask block.
engine.block.setShape(block=_, shape=_) Give the mask its shape.
engine.block.appendMask(block=_, maskBlock=_) Add a mask at the end of the stack.
engine.block.insertMask(block=_, maskBlock=_, index=_) Add a mask at an index.
engine.block.getMasks(block=_) Get the masks of a block in their order.
engine.block.removeMask(block=_, index=_) Remove the mask at an index.
engine.block.setMatte(block=_, source=_, mode=_) Set or remove the matte of a block.
engine.block.getMatte(block=_) Get the BlockMatte of a block, or null.
engine.block.export(block=_, mimeType=_) Export the page as PNG or SVG.

Next Steps#

  • Blend Modes: control how a block mixes with the blocks below it.
  • Keyframes: animate the mask properties over time.
  • Import Lottie: turn Lottie masks and mattes into blocks.