Show only part of a block with shape masks, or with a track matte that uses the rendering of another block.
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.
let purple = try engine.block.createFill(.color)try engine.block.setColor(purple, property: "fill/color/value", color: .rgba(r: 0.5, g: 0.2, b: 0.9, a: 1))let ringBlock = try addSquare(x: 50, fill: purple)
print(try engine.block.supportsMasks(ringBlock)) // trueprint(try engine.block.supportsMasks(page)) // false
// The ellipse fills the frame of the masked blocklet outerMask = try engine.block.createMask()try engine.block.setShape(outerMask, shape: engine.block.createShape(.ellipse))try engine.block.appendMask(ringBlock, maskID: 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 setBool 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 throughtry engine.block.setFloat(outerMask, property: "mask/feather", value: 20)try engine.block.setFloat(outerMask, property: "mask/opacity", value: 0.8)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 ringlet innerMask = try engine.block.createMask()try engine.block.setShape(innerMask, shape: engine.block.createShape(.ellipse))try engine.block.setFloat(innerMask, property: "mask/expansion", value: -100)try engine.block.setEnum(innerMask, property: "mask/mode", value: "Subtract")try engine.block.appendMask(ringBlock, maskID: 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.
let teal = try engine.block.createFill(.color)try engine.block.setColor(teal, property: "fill/color/value", color: .rgba(r: 0, g: 0.6, b: 0.6, a: 1))let starBlock = try addSquare(x: 550, fill: teal)
// An inverted mask keeps the area outside its shape: a star-shaped holelet starMask = try engine.block.createMask()try engine.block.setShape(starMask, shape: engine.block.createShape(.star))try engine.block.setBool(starMask, property: "mask/inverted", value: true)try engine.block.appendMask(starBlock, maskID: 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.
print(try engine.block.getMasks(ringBlock)) // [outerMask, innerMask]
// Removing a mask does not destroy ittry engine.block.removeMask(ringBlock, index: 1)print(engine.block.isValid(innerMask)) // true
// Insert it again after the outer masktry engine.block.insertMask(ringBlock, maskID: 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 |
|---|---|
.alpha |
Opaque. This is the default. |
.alphaInverted |
Transparent |
.luma |
Bright |
.lumaInverted |
Dark or transparent |
We use a square with a white to black gradient as the source. With .luma, the orange square fades from the top to the bottom.
let orange = try engine.block.createFill(.color)try engine.block.setColor(orange, property: "fill/color/value", color: .rgba(r: 1, g: 0.5, b: 0, a: 1))let target = try addSquare(x: 1050, fill: orange)
// The matte source: a gradient from white at the top to black at the bottomlet gradient = try engine.block.createFill(.linearGradient)try engine.block.setGradientColorStops( gradient, property: "fill/gradient/colors", colors: [ GradientColorStop(color: .rgba(r: 1, g: 1, b: 1, a: 1), stop: 0), GradientColorStop(color: .rgba(r: 0, g: 0, b: 0, a: 1), stop: 1), ],)let source = try addSquare(x: 1050, fill: gradient)
// The target shows where the source is bright. The source is not drawn.try engine.block.setMatte(target, source: source, mode: .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 nil when the block has no matte. To remove a matte, call setMatte with nil as the source. The source then draws again.
let matte = try engine.block.getMatte(target)print(matte == BlockMatte(source: source, mode: .luma)) // trueprint(try engine.block.getMatte(ringBlock) == nil) // trueMatte Source Rules#
setMatte checks the source and throws an EngineError 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.
do { // A block cannot use itself as its matte source try engine.block.setMatte(target, source: target)} catch let error as EngineError { print(error.catalogCode == .blockMatteSourceInvalid) // true}insertChild(into:child:at:) and appendChild(to:child:) keep these rules too. A move that puts a matted block inside its source, or the block and its source on different pages, throws .blockMatteSourceInvalid. 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 .alpha or .luma mattes hide the block, and .alphaInverted or .lumaInverted 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.
let png = try await engine.block.export(page, mimeType: .png)
// SVG export ignores the mask opacity and feather, and draws the matted block wholelet svg = try await engine.block.export(page, mimeType: .svg)print("Exported \(png.count) PNG bytes and \(svg.count) SVG bytes")SVG export has limits:
- SVG export ignores
mask/opacityandmask/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/featherandmask/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 (
.blockMasksUnsupported): only graphic, text and group blocks have a mask stack. ChecksupportsMasksfirst. - The mask is not accepted (
.blockMaskInvalid): pass a block thatcreateMaskmade. - The index is out of range (
.blockMaskIndexOutOfBounds): 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 (
.blockMatteUnsupported): use graphic, text or group blocks on both sides. - The source is rejected (
.blockMatteSourceInvalid): use a source on the same page that follows the matte source rules. - The block is not visible: an
.alphaor.lumamatte hides the block when its source is hidden or outside its time range.
API Reference#
| Method | Description |
|---|---|
engine.block.supportsMasks(_:) |
Check whether a block accepts masks |
engine.block.createMask() |
Create a mask block |
engine.block.setShape(_:shape:) |
Give the mask its shape |
engine.block.appendMask(_:maskID:) |
Add a mask at the end of the stack |
engine.block.insertMask(_:maskID:index:) |
Add a mask at an index |
engine.block.getMasks(_:) |
Get the masks of a block in their order |
engine.block.removeMask(_:index:) |
Remove the mask at an index |
engine.block.setMatte(_:source:mode:) |
Set or remove the matte of a block |
engine.block.getMatte(_:) |
Get the BlockMatte of a block, or nil |
engine.block.export(_: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.