Search Docs
Loading...
Skip to content

Follow Another Block

Make a block follow the position, rotation and scale of another block, like a parented layer in a motion design tool.

6 mins
estimated time
GitHub

A transform parent moves a block without changing the layer list. The block follows the other block wherever it sits on the page, also while that block is animated, and keeps its own opacity, visibility and stacking order. The editor has no control for transform parents, so you set them with the block API.

This guide covers how to animate a hidden block that drives others, how to link a block and read the link back, how chains of links work, what a block inherits from its transform parent, how to read the animated position, how to remove a link, the errors to expect, and how to export a frame.

Animating a Hidden Parent#

A transform parent can be any graphic or text block. The example draws its blocks as circles with a small local function.

// Creates a circle graphic of the given size and color on the page
fun createCircle(
size: Float,
color: RGBAColor,
): DesignBlock {
val block = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = block, shape = engine.block.createShape(ShapeType.Ellipse))
engine.block.setFill(block = block, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(block = block, color = color)
engine.block.setWidth(block = block, value = size)
engine.block.setHeight(block = block, value = size)
engine.block.appendChild(parent = page, child = block)
engine.block.setDuration(block = block, duration = 5.0)
return block
}

We use a small circle at the page center as a pivot. Its anchor, transform/anchor/x and transform/anchor/y, sits at its center, so its rotation keyframes turn it in place, once over 5 seconds. For keyframes and anchors, see Keyframes.

setVisible(block = pivot, visible = false) hides the pivot. A hidden block still works as a transform parent, like a null layer in a motion design tool, so one invisible block can move several others.

val pivot = createCircle(size = 20F, color = Color.fromRGBA(r = 0F, g = 0F, b = 0F, a = 1F))
engine.block.setPositionX(block = pivot, value = 950F)
engine.block.setPositionY(block = pivot, value = 530F)
engine.block.setFloat(block = pivot, property = "transform/anchor/x", value = 10F)
engine.block.setFloat(block = pivot, property = "transform/anchor/y", value = 10F)
engine.block.setKeyframes(
block = pivot,
property = "rotation",
keyframes = listOf(
Keyframe(time = 0F, value = KeyframeValue.Float(0F)),
Keyframe(time = 1F, value = KeyframeValue.Float(2F * PI.toFloat())),
),
)
engine.block.setVisible(block = pivot, visible = false)

Linking a Block#

setTransformParent(block, parent) makes the block follow the transform of the parent. getTransformParent(block) returns the parent. Both blocks must be graphic or text blocks.

The block’s position, rotation and scale are then read in the local space of the parent. The planet’s position of (300, -50) is an offset from the pivot’s top-left corner, measured along the pivot’s turning axes, so the planet circles the page center while the pivot turns. The block does not keep its place on screen when you link or unlink it, so set its position for the new space after you change the link.

val planet = createCircle(size = 120F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F))
engine.block.setTransformParent(block = planet, parent = pivot)
// The pivot
val planetTransformParent = engine.block.getTransformParent(block = planet)
// The position is now an offset from the pivot's top-left corner,
// in the pivot's turning space
engine.block.setPositionX(block = planet, value = 300F)
engine.block.setPositionY(block = planet, value = -50F)

Building a Chain#

A block can follow a block that follows a third block. The planet has its own rotation track about its center, and the moon follows the planet, so the moon circles the planet while the planet circles the pivot. The text label also follows the planet. Each link adds the transform of its parent, in any order of the blocks on the page.

// The planet spins twice about its center while it circles the pivot
engine.block.setFloat(block = planet, property = "transform/anchor/x", value = 60F)
engine.block.setFloat(block = planet, property = "transform/anchor/y", value = 60F)
engine.block.setKeyframes(
block = planet,
property = "rotation",
keyframes = listOf(
Keyframe(time = 0F, value = KeyframeValue.Float(0F)),
Keyframe(time = 1F, value = KeyframeValue.Float(4F * PI.toFloat())),
),
)
// The moon follows the planet, which follows the pivot
val moon = createCircle(size = 40F, color = Color.fromRGBA(r = 0.85F, g = 0.85F, b = 0.8F, a = 1F))
engine.block.setTransformParent(block = moon, parent = planet)
engine.block.setPositionX(block = moon, value = 160F)
engine.block.setPositionY(block = moon, value = 40F)
// A text label follows the planet, too
val label = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(block = label, text = "Planet")
engine.block.setTextFontSize(block = label, fontSize = 36F)
engine.block.setWidthMode(block = label, mode = SizeMode.AUTO)
engine.block.setHeightMode(block = label, mode = SizeMode.AUTO)
engine.block.appendChild(parent = page, child = label)
engine.block.setTransformParent(block = label, parent = planet)
engine.block.setPositionX(block = label, value = 0F)
engine.block.setPositionY(block = label, value = 140F)

What a Follower Inherits#

Only the transform is inherited. A follower keeps its own opacity and visibility, its place in the stacking order and its parent in the block hierarchy. The planet is half transparent, while the moon stays opaque, and getParent(block = moon) still returns the page.

1.0
engine.block.setOpacity(block = planet, value = 0.5F)
val moonOpacity = engine.block.getOpacity(block = moon)
// The page
val moonParent = engine.block.getParent(block = moon)

Reading the Animated Position#

A follower moves when its transform parent plays keyframes, auto-orients or runs a preset animation. The global bounding box of the follower includes that motion at the current playback time. It uses the static position, rotation and scale of the follower itself, so the follower’s own keyframes and animations do not change the box. After setPlaybackTime(block = page, time = 1.25), a quarter of the duration, the pivot has turned by 90 degrees, so getGlobalBoundingBoxX and getGlobalBoundingBoxY place the planet below the page center.

// A quarter of the way through, the pivot has turned by 90 degrees
engine.block.setPlaybackTime(block = page, time = 1.25)
// 900, 830
val planetX = engine.block.getGlobalBoundingBoxX(block = planet)
val planetY = engine.block.getGlobalBoundingBoxY(block = planet)
Log.i(TAG, "planet at $planetX, $planetY")

Pass null as the parent to remove the link. getTransformParent then returns null, and the label’s position is read in page space again, so we give it a page position.

// The label's position is read in page space again, so place it there
engine.block.setTransformParent(block = label, parent = null)
// null
val labelTransformParent = engine.block.getTransformParent(block = label)
engine.block.setPositionX(block = label, value = 80F)
engine.block.setPositionY(block = label, value = 80F)

Handling Errors#

A failed setTransformParent throws an EngineException with a code and adds no link. A block cannot follow itself, a block inside it or a block that follows it, directly or through other blocks. A page, a group and every other block type that is not a graphic or text block are refused on either side.

val cycleErrorCode = try {
// The pivot cannot follow a block that follows it
engine.block.setTransformParent(block = pivot, parent = moon)
null
} catch (error: EngineException) {
// BLOCK.TRANSFORM_PARENT_CYCLE
error.code
}
val unsupportedErrorCode = try {
// Only graphic and text blocks take part in a link
engine.block.setTransformParent(block = planet, parent = page)
null
} catch (error: EngineException) {
// BLOCK.TRANSFORM_PARENT_UNSUPPORTED
error.code
}

Exporting a Frame#

The playback time is still 1.25 seconds, so we export the page as PNG. The frame shows the planet below the page center with its moon, and no pivot.

// The playback time is still 1.25 seconds, so the frame shows the planet below the pivot
val previewPng = engine.block.export(block = page, mimeType = MimeType.PNG)
  • Destroying the transform parent removes the link.
  • fillParent ends the link, because the block then fills its own parent.
  • Undo and redo, saving and loading, and duplicating keep the link. A copy follows the same parent. In a copy of a group, a block that followed another member follows the copy of that member.
  • Blocks saved to a string or an archive without their transform parent keep their place on the page and lose the link. Their position, rotation and scale keyframes keep playing in place when the transform parent has no keyframes or animations and no mirror, shear or unequal scale. Otherwise they stay in the space of the transform parent.
  • A pasted copy does not follow the transform parent again, also when that block is in the scene.
  • Moving, rotating, scaling or flipping a group or a selection that holds both blocks transforms the follower once.
  • An auto-orienting block turns in the space of its transform parent.
  • When the engine enforces scopes, setTransformParent needs the layer/move scope.

Troubleshooting#

  • A link would make a cycle (BLOCK.TRANSFORM_PARENT_CYCLE): the parent is the block itself, sits inside it, or already follows it. Link the blocks the other way round, or pick another parent.
  • The block type cannot take part in a link (BLOCK.TRANSFORM_PARENT_UNSUPPORTED): pass graphic or text blocks on both sides. To move a group, link its children instead.
  • The link cannot change (BLOCK.POSITION_LOCKED): the transform of the block is locked. Unlock it before you link or unlink it. The lock of the parent does not matter.
  • A link stops working after a hierarchy change: moving the transform parent into the follower, for example with appendChild(parent, child), turns the link into a cycle. The engine then ignores the link when it draws the block and reports no error. Remove the link, or move the parent out again. Destroying any block or loading the scene removes such a link. Then move the parent out and link the blocks again.
  • The block jumps when you link it: its position is now read in the space of the parent. Set the position again after the link changes.

API Reference#

Method Purpose
engine.block.setTransformParent(block=_, parent=_) Make a block follow the transform of another block, or pass null to remove the link
engine.block.getTransformParent(block=_) Get the block that a block follows, or null
engine.block.create(blockType=_) Create the graphic and text blocks
engine.block.createShape(type=_) Create the ellipse shape of a circle
engine.block.setShape(block=_, shape=_) Give a graphic its shape
engine.block.createFill(fillType=_) Create a color fill
engine.block.setFillSolidColor(block=_, color=_) Set the fill color
engine.block.setFill(block=_, fill=_) Give a graphic its fill
engine.block.setWidth(block=_, value=_) Set the width of a block
engine.block.setHeight(block=_, value=_) Set the height of a block
engine.block.appendChild(parent=_, child=_) Add a block to the page
engine.block.setDuration(block=_, duration=_) Set the duration that keyframe times refer to
engine.block.setPositionX(block=_, value=_) Set the x position, in the space of the transform parent when a block has one
engine.block.setPositionY(block=_, value=_) Set the y position, in the space of the transform parent when a block has one
engine.block.setFloat(block=_, property="transform/anchor/x", value=_) Set the x position of the anchor that rotation turns about
engine.block.setFloat(block=_, property="transform/anchor/y", value=_) Set the y position of the anchor that rotation turns about
engine.block.setKeyframes(block=_, property="rotation", keyframes=_) Animate the rotation of a block
engine.block.setVisible(block=_, visible=_) Hide the transform parent, which still moves its followers
engine.block.replaceText(block=_, text=_) Set the text of the label
engine.block.setTextFontSize(block=_, fontSize=_) Set the font size of the label
engine.block.setWidthMode(block=_, mode=_) Size the label to its text
engine.block.setHeightMode(block=_, mode=_) Size the label to its text
engine.block.setOpacity(block=_, value=_) Set the opacity, which a follower does not inherit
engine.block.getOpacity(block=_) Read the opacity of a follower
engine.block.getParent(block=_) Get the parent in the block hierarchy, which a link does not change
engine.block.setPlaybackTime(block=_, time=_) Seek before you read the animated position or export a frame
engine.block.getGlobalBoundingBoxX(block=_) Read the x position of a follower at the current playback time
engine.block.getGlobalBoundingBoxY(block=_) Read the y position of a follower at the current playback time
engine.block.export(block=_, mimeType=_) Export the frame as PNG

Next Steps#