Search Docs
Loading...
Skip to content

Shape Modifiers

Change the path of a shape with modifiers. Trim an outline, repeat a shape, round its corners, or bend it.

10 mins
estimated time
GitHub

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 values:

Type Effect
ModifierType.Trim Keeps only a part of the path
ModifierType.Repeater Draws the shape several times, each copy one step further
ModifierType.RoundCorners Rounds the corners between straight segments
ModifierType.Offset Grows or shrinks the shape
ModifierType.Zigzag Adds peaks and valleys to the segments
ModifierType.PuckerBloat Pulls the corners towards the center or pushes them away
ModifierType.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 page
fun addGraphic(
shapeType: ShapeType,
x: Float,
y: Float,
width: Float,
height: Float,
color: Color,
): DesignBlock {
val graphic = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = graphic, shape = engine.block.createShape(shapeType))
val fill = engine.block.createFill(FillType.Color)
engine.block.setColor(block = fill, property = "fill/color/value", value = color)
engine.block.setFill(block = graphic, fill = fill)
engine.block.setWidth(block = graphic, value = width)
engine.block.setHeight(block = graphic, value = height)
engine.block.setPositionX(block = graphic, value = x)
engine.block.setPositionY(block = graphic, value = y)
engine.block.setDuration(block = graphic, duration = 5.0)
engine.block.appendChild(parent = 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 ModifierType.Trim modifier and add it to the end of the list with appendModifier.

val orange = Color.fromRGBA(r = 1F, g = 0.5F, b = 0F, a = 1F)
val outline = addGraphic(ShapeType.Star, x = 120F, y = 360F, width = 360F, height = 360F, color = orange)
// Modifiers belong to the shape block, not to the graphic block
val outlineShape = engine.block.getShape(block = outline)
val shapeSupportsModifiers = engine.block.supportsModifiers(block = outlineShape) // true
val graphicSupportsModifiers = engine.block.supportsModifiers(block = outline) // false
Log.i(TAG, "shape: $shapeSupportsModifiers, graphic: $graphicSupportsModifiers")
val trim = engine.block.createModifier(ModifierType.Trim)
engine.block.appendModifier(block = outlineShape, modifierBlock = 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 stroke
engine.block.setFillEnabled(block = outline, enabled = false)
engine.block.setStrokeEnabled(block = outline, enabled = true)
engine.block.setStrokeWidth(block = outline, width = 16F)
engine.block.setStrokeColor(block = outline, color = orange)
// Keep the first 75% of the path, moved by an eighth of the path length
engine.block.setFloat(block = trim, property = "modifier/trim/start", value = 0F)
engine.block.setFloat(block = trim, property = "modifier/trim/end", value = 0.75F)
engine.block.setFloat(block = trim, property = "modifier/trim/offset", value = 0.125F)
// "Simultaneous" trims each contour on its own, "Individual" trims all contours as one path
engine.block.setEnum(block = 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 block
engine.block.setKeyframes(
block = trim,
property = "modifier/trim/end",
keyframes = listOf(
Keyframe(
time = 0F,
value = KeyframeValue.Float(0F),
easing = KeyframeEasing.Preset(AnimationEasingType.EASE_IN_OUT),
),
Keyframe(time = 0.6F, value = KeyframeValue.Float(1F)),
),
)

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:

  • positionX and positionY move each copy. The default is 100 units to the right.
  • scaleX and scaleY scale each copy, and rotation turns it clockwise in degrees. Both use the point anchorX, anchorY in the frame of the block.
  • offset shifts all copies by a number of steps.
  • startOpacity is the opacity of the first copy. The copies fade towards endOpacity.
  • composite sets 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 edge
val blue = Color.fromRGBA(r = 0.2F, g = 0.5F, b = 1F, a = 1F)
val bar = addGraphic(ShapeType.Rect, x = 720F, y = 320F, width = 40F, height = 140F, color = blue)
val repeater = engine.block.createModifier(ModifierType.Repeater)
engine.block.setFloat(block = repeater, property = "modifier/repeater/copies", value = 12F)
// Each copy turns 30 degrees further about the anchor and does not move
engine.block.setFloat(block = repeater, property = "modifier/repeater/anchorX", value = 20F)
engine.block.setFloat(block = repeater, property = "modifier/repeater/anchorY", value = 220F)
engine.block.setFloat(block = repeater, property = "modifier/repeater/rotation", value = 30F)
engine.block.setFloat(block = repeater, property = "modifier/repeater/positionX", value = 0F)
engine.block.setFloat(block = repeater, property = "modifier/repeater/positionY", value = 0F)
// The copies fade from full opacity towards 10%
engine.block.setFloat(block = repeater, property = "modifier/repeater/startOpacity", value = 1F)
engine.block.setFloat(block = repeater, property = "modifier/repeater/endOpacity", value = 0.1F)
// "Above" draws each copy on top of the copy before it
engine.block.setEnum(block = repeater, property = "modifier/repeater/composite", value = "Above")
engine.block.appendModifier(block = engine.block.getShape(block = bar), modifierBlock = 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 bars
engine.block.setKeyframes(
block = repeater,
property = "modifier/repeater/copies",
keyframes = listOf(
Keyframe(time = 0F, value = KeyframeValue.Float(1F)),
Keyframe(time = 0.8F, value = KeyframeValue.Float(12F)),
),
)

Modifier Order#

The modifiers of a shape apply in list order, so each modifier changes the result of the modifier before it. insertModifier 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.

val purple = Color.fromRGBA(r = 0.5F, g = 0.2F, b = 0.9F, a = 1F)
val badge = addGraphic(ShapeType.Rect, x = 1000F, y = 360F, width = 360F, height = 360F, color = purple)
val badgeShape = engine.block.getShape(block = badge)
// Smooth peaks and valleys, 6 per side, 12 units from the path
val zigzag = engine.block.createModifier(ModifierType.Zigzag)
engine.block.setFloat(block = zigzag, property = "modifier/zigzag/size", value = 12F)
engine.block.setFloat(block = zigzag, property = "modifier/zigzag/ridges", value = 6F)
engine.block.setEnum(block = zigzag, property = "modifier/zigzag/pointType", value = "Smooth")
engine.block.appendModifier(block = badgeShape, modifierBlock = zigzag)
// Insert the rounding at index 0, so it applies before the zig zag
val roundCorners = engine.block.createModifier(ModifierType.RoundCorners)
engine.block.setFloat(block = roundCorners, property = "modifier/round_corners/radius", value = 60F)
engine.block.insertModifier(block = badgeShape, modifierBlock = 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.

val teal = Color.fromRGBA(r = 0F, g = 0.6F, b = 0.6F, a = 1F)
val flower = addGraphic(ShapeType.Star, x = 1440F, y = 360F, width = 360F, height = 360F, color = teal)
val flowerShape = engine.block.getShape(block = flower)
// A negative amount bloats: it pushes the vertices away from the center
val puckerBloat = engine.block.createModifier(ModifierType.PuckerBloat)
engine.block.setFloat(block = puckerBloat, property = "modifier/pucker_bloat/amount", value = -0.3F)
engine.block.appendModifier(block = flowerShape, modifierBlock = puckerBloat)
// Turn the path 45 degrees at the center of the block, less towards the edge
val twist = engine.block.createModifier(ModifierType.Twist)
engine.block.setFloat(block = twist, property = "modifier/twist/angle", value = 45F)
engine.block.setFloat(block = twist, property = "modifier/twist/centerX", value = 180F)
engine.block.setFloat(block = twist, property = "modifier/twist/centerY", value = 180F)
engine.block.appendModifier(block = flowerShape, modifierBlock = twist)
// Grow the result by 12 units with round corners
val offset = engine.block.createModifier(ModifierType.Offset)
engine.block.setFloat(block = offset, property = "modifier/offset/amount", value = 12F)
engine.block.setEnum(block = offset, property = "modifier/offset/join", value = "Round")
engine.block.appendModifier(block = flowerShape, modifierBlock = offset)

Reading and Finding Modifiers#

getModifiers returns the modifiers of a shape in the order they apply. findByType with a ModifierType finds every modifier of that type in the scene.

// [//ly.img.ubq/modifier/round_corners, //ly.img.ubq/modifier/zigzag]
val badgeModifierTypes = engine.block.getModifiers(block = badgeShape).map { engine.block.getType(it) }
// Find every trim modifier in the scene
val foundTrims = engine.block.findByType(ModifierType.Trim) // [trim]
Log.i(TAG, "badge: $badgeModifierTypes, trims: $foundTrims")

Removing and Destroying Modifiers#

removeModifier 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.
engine.block.removeModifier(block = badgeShape, index = 1)
val zigzagValidAfterRemove = engine.block.isValid(block = zigzag) // true
val badgeModifierCountAfterRemove = engine.block.getModifiers(block = badgeShape).size // 1
// Destroy it when no shape uses it any more
engine.block.destroy(block = zigzag)

Destroying a graphic block also destroys the modifiers of its shape.

Handling Errors#

Invalid input throws an EngineException. Its code names the problem. A modifier method on a graphic block is a common mistake, because the modifiers belong to its shape.

val errorCode = try {
// A graphic block has no modifiers. Pass its shape instead.
engine.block.getModifiers(block = outline)
null
} catch (error: EngineException) {
// BLOCK.MODIFIERS_UNSUPPORTED
error.code
}

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 animation
engine.block.setPlaybackTime(block = page, time = 2.5)
val previewPng = engine.block.export(block = page, mimeType = MimeType.PNG)

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, insertModifier and removeModifier 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 (BLOCK.MODIFIERS_UNSUPPORTED): pass the shape block. Get it with getShape.
  • The block to add is not a modifier (BLOCK.MODIFIER_NOT_A_MODIFIER): create the block with createModifier.
  • The index is outside the list (BLOCK.MODIFIER_INDEX_OUT_OF_BOUNDS): insertModifier takes an index from 0 to the list length. removeModifier takes an index below the list length.
  • A trimmed shape draws nothing: check that the trim start and end differ.

API Reference#

Method Purpose
engine.block.createModifier(type=_) Create a modifier block.
engine.block.supportsModifiers(block=_) Check whether a block accepts modifiers.
engine.block.getModifiers(block=_) Get the modifiers of a shape in the order they apply.
engine.block.appendModifier(block=_, modifierBlock=_) Add a modifier at the end of the list.
engine.block.insertModifier(block=_, modifierBlock=_, index=_) Add a modifier at an index.
engine.block.removeModifier(block=_, index=_) Remove a modifier from the list without destroying it.
engine.block.findByType(type=_) Find every modifier of a ModifierType.
engine.block.setFloat(block=_, property=_, value=_) Set a number property of a modifier.
engine.block.setEnum(block=_, property=_, value=_) Set an option of a modifier.
engine.block.setKeyframes(block=_, property=_, keyframes=_) Animate a modifier property.
engine.block.destroy(block=_) Destroy a modifier and remove it from every list.
engine.block.getShape(block=_) Get the shape block that owns the modifiers.
engine.block.setPlaybackTime(block=_, time=_) Set the time to render.
engine.block.export(block=_, mimeType=_) 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.