Search Docs
Loading...
Skip to content

Time Remap

Freeze, reverse or replay the children of a composition group by remapping their clock.

6 mins
estimated time
GitHub

A composition group gives its children their own clock. playback/timeRemap sets the time on that clock, in seconds, that the children show. A static value freezes them, and KeyframeValue.Float keyframes drive them over the group’s duration. The editor has no time remap controls, so you set them with the block API.

This guide covers how to turn time remap on, how to freeze the children, how to play them in reverse or forward and back, how to read the remap back, and the error to expect. It ends with a frame export.

Creating a Composition Group#

Time remap acts on the children of a composition group. Each lane of the example is a group with a gray line and a ball on page, the 5-second page of the scene. The ball has a position/x track and crosses the page in 5 seconds of the group’s clock. group groups the two blocks, and group/composition gives the group its own clock for its children.

// Creates a lane: a composition group with a line and a ball that crosses
// the page in 5 seconds of the group's own clock
fun createLane(
y: Float,
color: RGBAColor,
): DesignBlock {
val line = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = line, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block = line, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(block = line, color = Color.fromRGBA(r = 0.85F, g = 0.85F, b = 0.85F, a = 1F))
engine.block.setWidth(block = line, value = 1680F)
engine.block.setHeight(block = line, value = 6F)
engine.block.setPositionX(block = line, value = 120F)
engine.block.setPositionY(block = line, value = y + 37F)
engine.block.appendChild(parent = page, child = line)
engine.block.setDuration(block = line, duration = 5.0)
val ball = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = ball, shape = engine.block.createShape(ShapeType.Ellipse))
engine.block.setFill(block = ball, fill = engine.block.createFill(FillType.Color))
engine.block.setFillSolidColor(block = ball, color = color)
engine.block.setWidth(block = ball, value = 80F)
engine.block.setHeight(block = ball, value = 80F)
engine.block.setPositionY(block = ball, value = y)
engine.block.appendChild(parent = page, child = ball)
engine.block.setDuration(block = ball, duration = 5.0)
engine.block.setKeyframes(
block = ball,
property = "position/x",
keyframes = listOf(
Keyframe(time = 0F, value = KeyframeValue.Float(120F)),
Keyframe(time = 1F, value = KeyframeValue.Float(1720F)),
),
)
val lane = engine.block.group(blocks = listOf(line, ball))
engine.block.setDuration(block = lane, duration = 5.0)
// A composition group gives its children their own clock
engine.block.setBoolean(block = lane, property = "group/composition", value = true)
return lane
}

Creating the Lanes#

The example creates four lanes. The first one plays normally, and the other three get a time remap below. playback/timeRemapEnabled is off by default, and getBoolean reads it.

val normal = createLane(y = 150F, color = Color.fromRGBA(r = 0.6F, g = 0.6F, b = 0.6F, a = 1F))
val frozen = createLane(y = 400F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F))
val reversed = createLane(y = 650F, color = Color.fromRGBA(r = 0.9F, g = 0.3F, b = 0.4F, a = 1F))
val pingPong = createLane(y = 900F, color = Color.fromRGBA(r = 0.2F, g = 0.7F, b = 0.3F, a = 1F))
// false
val normalRemapEnabled = engine.block.getBoolean(block = normal, property = "playback/timeRemapEnabled")

Freezing the Children#

setBoolean turns on playback/timeRemapEnabled. A static playback/timeRemap, set with setFloat, then holds the children at that time on their clock, here 2.5 seconds, so the blue ball stays in the middle of its lane.

// A static remap holds the children at 2.5 seconds of their own time
engine.block.setBoolean(block = frozen, property = "playback/timeRemapEnabled", value = true)
engine.block.setFloat(block = frozen, property = "playback/timeRemap", value = 2.5F)

Playing in Reverse#

A track on playback/timeRemap drives the children while the group plays. The keyframe times are normalized over the group’s duration, from 0 to 1. The values are times on the children’s clock, in seconds. The track from just below 5 to 0 plays the red ball from the end of its lane back to the start. The values stay below 5, because a child shows only while the clock is before its end.

// The track values are times on the children's clock, in seconds. They
// stay below 5, because a clock at a child's end hides the child.
engine.block.setBoolean(block = reversed, property = "playback/timeRemapEnabled", value = true)
engine.block.setKeyframes(
block = reversed,
property = "playback/timeRemap",
keyframes = listOf(
Keyframe(time = 0F, value = KeyframeValue.Float(4.99F)),
Keyframe(time = 1F, value = KeyframeValue.Float(0F)),
),
)

Playing Forward and Back#

Three keyframes play the children forward to just below 5 seconds and back to 0. The AnimationEasingType.EASE_IN_OUT easing slows the green ball at both ends and at the turn.

val easeInOut = KeyframeEasing.Preset(AnimationEasingType.EASE_IN_OUT)
engine.block.setBoolean(block = pingPong, property = "playback/timeRemapEnabled", value = true)
engine.block.setKeyframes(
block = pingPong,
property = "playback/timeRemap",
keyframes = listOf(
Keyframe(time = 0F, value = KeyframeValue.Float(0F), easing = easeInOut),
Keyframe(time = 0.5F, value = KeyframeValue.Float(4.99F), easing = easeInOut),
Keyframe(time = 1F, value = KeyframeValue.Float(0F)),
),
)

Reading the Remap Back#

A track leaves the static value of playback/timeRemap unchanged, so getFloat still reads 0 for the reversed lane. isPropertyAnimatable is true for playback/timeRemap on a group and false on other blocks.

0.0
val reversedStaticRemap = engine.block.getFloat(block = reversed, property = "playback/timeRemap")
// true
val remapIsAnimatable = engine.block.isPropertyAnimatable(block = reversed, property = "playback/timeRemap")
Log.i(TAG, "$reversedStaticRemap, $remapIsAnimatable")

Handling Errors#

Only a group has a time remap. On any other block, a call throws an EngineException with a code.

val errorCode = try {
// Only a group has a time remap
val ball = engine.block.getChildren(block = normal)[1]
engine.block.setFloat(block = ball, property = "playback/timeRemap", value = 1F)
null
} catch (error: EngineException) {
// EDITOR.BLOCK_NO_TIME_REMAP_SUPPORT
error.code
}

Exporting a Frame#

We export the page as PNG at 1.5 seconds. The gray ball is at 30 percent of its lane, the blue ball in the middle, the red ball at 70 percent, and the green ball past the middle on its way out.

// Export the frame at 1.5 seconds
engine.block.setPlaybackTime(block = page, time = 1.5)
val previewPng = engine.block.export(block = page, mimeType = MimeType.PNG)
Log.i(TAG, "PNG bytes: ${previewPng.remaining()}")

Time Remap and Other Block Operations#

  • Without group/composition, the remap has no effect, and the children play on the page clock.
  • playback/speed of a composition group multiplies the remapped time.
  • A child shows only while the remapped time is at or after its time offset and before its end, so a clock at a child’s end, or a reversed or frozen clock, can hide it.
  • A TrackMode.RELATIVE remap track adds its values to the static remap.
  • Saving and loading, undo and redo keep the remap and its keyframes.

Troubleshooting#

  • The block has no time remap (EDITOR.BLOCK_NO_TIME_REMAP_SUPPORT): set playback/timeRemap on a group.
  • The block takes no remap keyframes (BLOCK.KEYFRAMES_PROPERTY_NOT_ANIMATABLE): set playback/timeRemap keyframes on a group.
  • The value is not a number (BLOCK.VALUE_NOT_FINITE): pass a finite time in seconds.
  • The keyframes are refused (BLOCK.KEYFRAMES_VALUE_KIND_MISMATCH): use KeyframeValue.Float keyframes for playback/timeRemap.
  • The children do not change: turn on group/composition and playback/timeRemapEnabled for the group.
  • A child disappears: the remapped time is outside the child’s time offset and duration.

API Reference#

Method Purpose
engine.block.create(blockType=_) Create a graphic block
engine.block.createShape(type=_) Create a rect or ellipse shape
engine.block.setShape(block=_, shape=_) Give a graphic its shape
engine.block.createFill(fillType=_) Create a color fill
engine.block.setFill(block=_, fill=_) Give a graphic its fill
engine.block.setFillSolidColor(block=_, color=_) Set the fill color
engine.block.setWidth(block=_, value=_) Set the width of a block
engine.block.setHeight(block=_, value=_) Set the height of a block
engine.block.setPositionX(block=_, value=_) Set the x position of a block
engine.block.setPositionY(block=_, value=_) Set the y position of a block
engine.block.appendChild(parent=_, child=_) Add a block to the page
engine.block.setDuration(block=_, duration=_) Set the duration of a block or group
engine.block.group(blocks=_) Group blocks into a group
engine.block.setBoolean(block=_, property="group/composition", value=_) Give a group its own clock
engine.block.setBoolean(block=_, property="playback/timeRemapEnabled", value=_) Turn the remap on
engine.block.getBoolean(block=_, property="playback/timeRemapEnabled") Check whether the remap is on
engine.block.setFloat(block=_, property="playback/timeRemap", value=_) Set the static remap, in seconds
engine.block.getFloat(block=_, property="playback/timeRemap") Read the static remap
engine.block.setKeyframes(block=_, property=_, keyframes=_) Animate the ball or the remap
engine.block.isPropertyAnimatable(block=_, property="playback/timeRemap") Check that a block takes remap keyframes
engine.block.getChildren(block=_) Get the children of a group
engine.block.setPlaybackTime(block=_, time=_) Seek before you export a frame
engine.block.export(block=_, mimeType=_) Export the frame as PNG

Next Steps#