Change the path of a shape with modifiers. Trim an outline, repeat a shape, round its corners, or bend it.
A modifier is a block that changes the path of a shape. You attach it to a shape block, and the engine applies it before it draws the fill and the stroke. A shape can have a list of modifiers, which apply in order. Every modifier property accepts keyframes. Imported Lottie animations use the same modifiers, see Lottie.
This guide covers how to create, order, animate and remove modifiers, and what each modifier type does.
Modifier Types#
createModifier(_:) accepts these ModifierType cases:
| Type | Effect |
|---|---|
.trim |
Keeps only a part of the path |
.repeater |
Draws the shape several times, each copy one step further |
.roundCorners |
Rounds the corners between straight segments |
.offset |
Grows or shrinks the shape |
.zigzag |
Adds peaks and valleys to the segments |
.puckerBloat |
Pulls the corners towards the center or pushes them away |
.twist |
Turns the path around a center, more near the center than at the edge |
Creating a Modifier#
We use a small helper that adds a 5 second graphic block with a shape and a color fill to the page. The duration matters only for the animations later in this guide.
// Adds a 5 second graphic with a shape and a color fill to the pagefunc addGraphic( _ shapeType: ShapeType, x: Float, y: Float, width: Float, height: Float, color: Color,) throws -> DesignBlockID { let graphic = try engine.block.create(.graphic) try engine.block.setShape(graphic, shape: engine.block.createShape(shapeType)) let fill = try engine.block.createFill(.color) try engine.block.setColor(fill, property: "fill/color/value", color: color) try engine.block.setFill(graphic, fill: fill) try engine.block.setWidth(graphic, value: width) try engine.block.setHeight(graphic, value: height) try engine.block.setPositionX(graphic, value: x) try engine.block.setPositionY(graphic, value: y) try engine.block.setDuration(graphic, duration: 5) try engine.block.appendChild(to: page, child: graphic) return graphic}Modifiers belong to the shape block, not to the graphic block. supportsModifiers(_:) returns true for every shape block and false for all other blocks. We get the shape with getShape(_:), create a .trim modifier and add it to the end of the list with appendModifier(_:modifierID:).
let orange = Color.rgba(r: 1, g: 0.5, b: 0, a: 1)let outline = try addGraphic(.star, x: 120, y: 360, width: 360, height: 360, color: orange)
// Modifiers belong to the shape block, not to the graphic blocklet outlineShape = try engine.block.getShape(outline)print(try engine.block.supportsModifiers(outlineShape)) // trueprint(try engine.block.supportsModifiers(outline)) // false
let trim = try engine.block.createModifier(.trim)try engine.block.appendModifier(outlineShape, modifierID: trim)Trimming a Path#
A trim keeps the part of the path between modifier/trim/start and modifier/trim/end. Both are fractions of the path length from 0 to 1, with the defaults 0 and 1. modifier/trim/offset moves the kept part along the path. An offset of 1 is a full turn. Equal start and end values draw nothing.
// Draw the outline only: no fill and a thick stroketry engine.block.setFillEnabled(outline, enabled: false)try engine.block.setStrokeEnabled(outline, enabled: true)try engine.block.setStrokeWidth(outline, width: 16)try engine.block.setStrokeColor(outline, color: orange)
// Keep the first 75% of the path, moved by an eighth of the path lengthtry engine.block.setFloat(trim, property: "modifier/trim/start", value: 0)try engine.block.setFloat(trim, property: "modifier/trim/end", value: 0.75)try engine.block.setFloat(trim, property: "modifier/trim/offset", value: 0.125)// "Simultaneous" trims each contour on its own, "Individual" trims all contours as one pathtry engine.block.setEnum(trim, property: "modifier/trim/mode", value: "Simultaneous")modifier/trim/mode controls a path with several contours, such as a shape with a hole. "Simultaneous", the default, trims each contour on its own with the same start and end. "Individual" trims all contours as one path, in path order. On a shape with a stroke and no fill, a trim draws a part of the outline.
Animating Modifiers#
Every modifier property accepts keyframes. The keyframe times refer to the duration of the graphic block that uses the shape. Here the trim end grows from 0 to 1 in the first 3 seconds, so the star outline draws itself.
// Draw the whole outline during the first 3 seconds of the 5 second blocktry engine.block.setKeyframes(trim, property: "modifier/trim/end", keyframes: [ Keyframe(time: 0, value: .float(0), easing: .preset(.easeInOut)), Keyframe(time: 0.6, value: .float(1)),])An absolute track replaces the static value, so the static end of 0.75 does not show while the track exists. For more on tracks and easing, see Keyframes.
Repeating a Shape#
A repeater draws modifier/repeater/copies copies of the shape, 3 by default. Each copy is transformed one step further than the copy before it:
positionXandpositionYmove each copy. The default is 100 units to the right.scaleXandscaleYscale each copy, androtationturns it clockwise in degrees. Both use the pointanchorX,anchorYin the frame of the block.offsetshifts all copies by a number of steps.startOpacityis the opacity of the first copy. The copies fade towardsendOpacity.compositesets the paint order. With"Below", the default, the first copy is on top. With"Above", the last copy is on top.
We turn a bar 30 degrees per copy about a point below it, so 12 copies form a ring.
// A 40 x 140 bar, repeated around a point 220 units below its top edgelet blue = Color.rgba(r: 0.2, g: 0.5, b: 1, a: 1)let bar = try addGraphic(.rect, x: 720, y: 320, width: 40, height: 140, color: blue)let repeater = try engine.block.createModifier(.repeater)try engine.block.setFloat(repeater, property: "modifier/repeater/copies", value: 12)// Each copy turns 30 degrees further about the anchor and does not movetry engine.block.setFloat(repeater, property: "modifier/repeater/anchorX", value: 20)try engine.block.setFloat(repeater, property: "modifier/repeater/anchorY", value: 220)try engine.block.setFloat(repeater, property: "modifier/repeater/rotation", value: 30)try engine.block.setFloat(repeater, property: "modifier/repeater/positionX", value: 0)try engine.block.setFloat(repeater, property: "modifier/repeater/positionY", value: 0)// The copies fade from full opacity towards 10%try engine.block.setFloat(repeater, property: "modifier/repeater/startOpacity", value: 1)try engine.block.setFloat(repeater, property: "modifier/repeater/endOpacity", value: 0.1)// "Above" draws each copy on top of the copy before ittry engine.block.setEnum(repeater, property: "modifier/repeater/composite", value: "Above")try engine.block.appendModifier(engine.block.getShape(bar), modifierID: repeater)Every copy uses the same path, so a trim or another modifier changes all copies alike. The number of copies is rounded to a whole number. Animating it adds the copies one by one. On a subshape of a compound shape or on the shape of a mask, a repeater repeats the outline. The copies there form one outline, without the opacity fade.
// Grow the ring from 1 to 12 barstry engine.block.setKeyframes(repeater, property: "modifier/repeater/copies", keyframes: [ Keyframe(time: 0, value: .float(1)), Keyframe(time: 0.8, value: .float(12)),])Modifier Order#
The modifiers of a shape apply in list order, so each modifier changes the result of the modifier before it. insertModifier(_:modifierID:index:) adds a modifier at an index from 0 to the length of the list. We add a zig zag first, then insert a corner rounding at index 0 so that it applies before the zig zag.
let purple = Color.rgba(r: 0.5, g: 0.2, b: 0.9, a: 1)let badge = try addGraphic(.rect, x: 1000, y: 360, width: 360, height: 360, color: purple)let badgeShape = try engine.block.getShape(badge)
// Smooth peaks and valleys, 6 per side, 12 units from the pathlet zigzag = try engine.block.createModifier(.zigzag)try engine.block.setFloat(zigzag, property: "modifier/zigzag/size", value: 12)try engine.block.setFloat(zigzag, property: "modifier/zigzag/ridges", value: 6)try engine.block.setEnum(zigzag, property: "modifier/zigzag/pointType", value: "Smooth")try engine.block.appendModifier(badgeShape, modifierID: zigzag)
// Insert the rounding at index 0, so it applies before the zig zaglet roundCorners = try engine.block.createModifier(.roundCorners)try engine.block.setFloat(roundCorners, property: "modifier/round_corners/radius", value: 60)try engine.block.insertModifier(badgeShape, modifierID: roundCorners, index: 0)modifier/zigzag/size is the distance of the peaks and valleys from the path. modifier/zigzag/ridges is the number of peaks and valleys per segment, 5 by default. modifier/zigzag/pointType is "Corner" for sharp points or "Smooth" for round points. One modifier can be in the lists of several shapes.
Changing the Geometry#
The other geometry modifiers each have a small set of properties:
| Property | Default | Effect |
|---|---|---|
modifier/round_corners/radius |
0 | The corner radius |
modifier/offset/amount |
0 | The distance to grow the shape. A negative value shrinks it. |
modifier/offset/join |
"Miter" |
The offset corners: "Bevel", "Miter" or "Round" |
modifier/offset/miterLimit |
4 | The miter limit, as a multiple of the amount |
modifier/pucker_bloat/amount |
0 | The fraction of the way from each corner to the center. 1 moves the corners to the center. A negative value moves them away. |
modifier/twist/angle |
0 | The turn at the center in degrees, clockwise |
modifier/twist/centerX, centerY |
0 | The center of the twist in the frame of the block |
We bloat a star, twist it around the center of its block and grow the result by 12 units.
let teal = Color.rgba(r: 0, g: 0.6, b: 0.6, a: 1)let flower = try addGraphic(.star, x: 1440, y: 360, width: 360, height: 360, color: teal)let flowerShape = try engine.block.getShape(flower)
// A negative amount bloats: it pushes the vertices away from the centerlet puckerBloat = try engine.block.createModifier(.puckerBloat)try engine.block.setFloat(puckerBloat, property: "modifier/pucker_bloat/amount", value: -0.3)try engine.block.appendModifier(flowerShape, modifierID: puckerBloat)
// Turn the path 45 degrees at the center of the block, less towards the edgelet twist = try engine.block.createModifier(.twist)try engine.block.setFloat(twist, property: "modifier/twist/angle", value: 45)try engine.block.setFloat(twist, property: "modifier/twist/centerX", value: 180)try engine.block.setFloat(twist, property: "modifier/twist/centerY", value: 180)try engine.block.appendModifier(flowerShape, modifierID: twist)
// Grow the result by 12 units with round cornerslet offset = try engine.block.createModifier(.offset)try engine.block.setFloat(offset, property: "modifier/offset/amount", value: 12)try engine.block.setEnum(offset, property: "modifier/offset/join", value: "Round")try engine.block.appendModifier(flowerShape, modifierID: offset)Reading and Finding Modifiers#
getModifiers(_:) returns the modifiers of a shape in the order they apply. find(byType:) with a ModifierType finds every modifier of that type in the scene.
let badgeModifierTypes = try engine.block.getModifiers(badgeShape).map { try engine.block.getType($0) }print(badgeModifierTypes) // ["//ly.img.ubq/modifier/round_corners", "//ly.img.ubq/modifier/zigzag"]
// Find every trim modifier in the scenelet foundTrims = try engine.block.find(byType: ModifierType.trim)print(foundTrims == [trim]) // trueRemoving and Destroying Modifiers#
removeModifier(_:index:) removes the modifier at an index from the list. It does not destroy the modifier block, so you can add it again. Call destroy(_:) to delete a modifier. This also removes it from the list of every shape that uses it.
// Remove the zig zag at index 1. The modifier block still exists.try engine.block.removeModifier(badgeShape, index: 1)print(engine.block.isValid(zigzag)) // true
// Destroy it when no shape uses it any moretry engine.block.destroy(zigzag)Destroying a graphic block also destroys the modifiers of its shape.
Handling Errors#
Invalid input throws an EngineError. Its catalogCode names the problem. A modifier method on a graphic block is a common mistake, because the modifiers belong to its shape.
do { // A graphic block has no modifiers. Pass its shape instead. _ = try engine.block.getModifiers(outline)} catch let error as EngineError { print(error.catalogCode == .blockModifiersUnsupported) // true}Exporting the Result#
We set the playback time of the page to 2.5 seconds and export it as a PNG. The export shows the animated trim and the animated repeater at that time.
// Export the frame at 2.5 seconds, halfway through the animationtry engine.block.setPlaybackTime(page, time: 2.5)let png = try await engine.block.export(page, mimeType: .png)print("Exported \(png.count) bytes")Modifiers and Other Block Operations#
Modifiers take part in the usual block operations:
- Undo and redo restore the modifier list and the modifier values.
- Saving and loading a scene keeps the modifiers, their order and their values.
- Duplicating a block copies the modifiers of its shape in order.
- Scaling a block or changing the design unit converts the length properties and their keyframes. These are the corner radius, the offset amount, the zig zag size, the twist center, and the repeater anchor and position.
appendModifier(_:modifierID:), insertModifier(_:modifierID:index:) and removeModifier(_:index:) need the shape/change scope on every block that uses the shape. Keyframes on a modifier need the appearance/animation scope.
Troubleshooting#
- The block does not accept modifiers (
.blockModifiersUnsupported): pass the shape block. Get it withgetShape(_:). - The block to add is not a modifier (
.blockModifierNotAModifier): create the block withcreateModifier(_:). - The index is outside the list (
.blockModifierIndexOutOfBounds):insertModifier(_:modifierID:index:)takes an index from 0 to the list length.removeModifier(_:index:)takes an index below the list length. - A trimmed shape draws nothing: check that the trim start and end differ.
API Reference#
| Method | Description |
|---|---|
engine.block.createModifier(_:) |
Create a modifier block |
engine.block.supportsModifiers(_:) |
Check whether a block accepts modifiers |
engine.block.getModifiers(_:) |
Get the modifiers of a shape in the order they apply |
engine.block.appendModifier(_:modifierID:) |
Add a modifier at the end of the list |
engine.block.insertModifier(_:modifierID:index:) |
Add a modifier at an index |
engine.block.removeModifier(_:index:) |
Remove a modifier from the list without destroying it |
engine.block.find(byType:) |
Find every modifier of a ModifierType |
engine.block.setFloat(_:property:value:) |
Set a number property of a modifier |
engine.block.setEnum(_:property:value:) |
Set an option of a modifier |
engine.block.setKeyframes(_:property:keyframes:mode:) |
Animate a modifier property |
engine.block.destroy(_:) |
Destroy a modifier and remove it from every list |
engine.block.getShape(_:) |
Get the shape block that owns the modifiers |
engine.block.setPlaybackTime(_:time:) |
Set the time to render |
engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:) |
Export the page at the current playback time |
Next Steps#
- Keyframes: animate modifier properties and other block properties over time.
- Edit Shapes: change the properties of the shape itself.
- Combine Shapes: combine shapes with boolean operations.