Move a block along a straight or curved path with vec2 position keyframes and spatial tangents.
position is a vec2 property. One KeyframeValue.Vec2 keyframe track moves the /x and /y properties of a block together, and spatial tangents curve the path between two keyframes. The editor has no motion path controls, so you set them with the block API.
This guide covers how to curve a path with spatial tangents, how to move in a straight line, how to read a path back, how a single-axis track changes one axis, and the errors to expect. It ends with a frame export.
Creating the Blocks#
The example draws its blocks as circles. A small local function creates each one on the page with a duration of 5 seconds.
// Creates a circle graphic of the given size and color on the pagefun 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}Curving a Path#
setKeyframes with KeyframeValue.Vec2 keyframes sets the path of a block. Both tangents of a segment sit on its first keyframe. spatialOut leaves that keyframe’s value and is relative to it. spatialIn enters the next keyframe’s value and is relative to that value. The last keyframe uses neither. The ball leaves its start straight up, enters the top of its arc from the left, leaves it to the right, and enters its end from above.
setVec2 sets the static position, here at the start of the path, and getVec2 reads it back. The keyframes replace the static position at every playback time.
// Both tangents of a segment sit on its first keyframe. `spatialOut` is// relative to this keyframe's value, `spatialIn` to the next one's.val ball = createCircle(size = 80F, color = Color.fromRGBA(r = 0.2F, g = 0.4F, b = 0.9F, a = 1F))engine.block.setVec2(block = ball, property = "position", value = Vector2(200F, 800F))engine.block.setKeyframes( block = ball, property = "position", keyframes = listOf( Keyframe( time = 0F, value = KeyframeValue.Vec2(Vector2(200F, 800F)), spatialOut = Vector2(0F, -400F), spatialIn = Vector2(-400F, 0F), ), Keyframe( time = 0.5F, value = KeyframeValue.Vec2(Vector2(920F, 300F)), spatialOut = Vector2(400F, 0F), spatialIn = Vector2(0F, -400F), ), Keyframe(time = 1F, value = KeyframeValue.Vec2(Vector2(1640F, 800F))), ),)val ballPosition = engine.block.getVec2(block = ball, property = "position")// Vector2(x=200.0, y=800.0)Log.i(TAG, "$ballPosition")Moving in a Straight Line#
Keyframes without tangents move a block in a straight line between their values. The dot slides along the bottom of the page.
val dot = createCircle(size = 40F, color = Color.fromRGBA(r = 0.9F, g = 0.3F, b = 0.4F, a = 1F))engine.block.setVec2(block = dot, property = "position", value = Vector2(200F, 980F))engine.block.setKeyframes( block = dot, property = "position", keyframes = listOf( Keyframe(time = 0F, value = KeyframeValue.Vec2(Vector2(200F, 980F))), Keyframe(time = 1F, value = KeyframeValue.Vec2(Vector2(1680F, 980F))), ),)Reading the Path Back#
getKeyframes returns the keyframes of a property, sorted by time, with their tangents.
val keyframes = engine.block.getKeyframes(block = ball, property = "position")// 3, Vector2(x=0.0, y=-400.0)Log.i(TAG, "${keyframes.size}, ${keyframes[0].spatialOut}")Changing One Axis#
A position/x or position/y track with KeyframeValue.Float keyframes overrides that axis of a position track. The green ball moves right with its position track, while its position/y track makes it hop twice.
// The `position/y` track replaces the y-axis of the `position` trackval hopper = createCircle(size = 60F, color = Color.fromRGBA(r = 0.2F, g = 0.7F, b = 0.3F, a = 1F))engine.block.setKeyframes( block = hopper, property = "position", keyframes = listOf( Keyframe(time = 0F, value = KeyframeValue.Vec2(Vector2(200F, 100F))), Keyframe(time = 1F, value = KeyframeValue.Vec2(Vector2(1660F, 100F))), ),)engine.block.setKeyframes( block = hopper, property = "position/y", keyframes = listOf( Keyframe(time = 0F, value = KeyframeValue.Float(160F)), Keyframe(time = 0.25F, value = KeyframeValue.Float(40F)), Keyframe(time = 0.5F, value = KeyframeValue.Float(160F)), Keyframe(time = 0.75F, value = KeyframeValue.Float(40F)), Keyframe(time = 1F, value = KeyframeValue.Float(160F)), ),)Handling Errors#
A failed setKeyframes throws an EngineException with a code. The track stays unchanged. A position track takes vec2 values only, and only a vec2 keyframe takes spatial tangents.
val valueKindErrorCode = try { // A motion path takes vec2 values only engine.block.setKeyframes( block = ball, property = "position", keyframes = listOf(Keyframe(time = 0F, value = KeyframeValue.Float(0F))), ) null} catch (error: EngineException) { // BLOCK.KEYFRAMES_VALUE_KIND_MISMATCH error.code}val tangentErrorCode = try { // A single axis takes no spatial tangents engine.block.setKeyframes( block = ball, property = "position/x", keyframes = listOf( Keyframe(time = 0F, value = KeyframeValue.Float(0F), spatialOut = Vector2(10F, 0F)), ), ) null} catch (error: EngineException) { // BLOCK.KEYFRAMES_SPATIAL_TANGENT_UNSUPPORTED error.code}Exporting a Frame#
We export the page as PNG at 1.25 seconds. The frame shows the blue ball on the rising part of its arc, the red dot a quarter of the way along the bottom, and the green ball at the top of its first hop.
// Export the frame at 1.25 seconds, while the ball climbs its pathengine.block.setPlaybackTime(block = page, time = 1.25)val previewPng = engine.block.export(block = page, mimeType = MimeType.PNG)Log.i(TAG, "PNG bytes: ${previewPng.remaining()}")Motion Paths and Other Block Operations#
- Position keyframes are in design units, also when the position mode of the block is
PositionMode.PERCENT. - Scaling the block or changing the design unit also converts its position keyframes.
- A
TrackMode.RELATIVEpositiontrack adds its offsets to the static position, so the path moves with the block. For relative tracks and easing, see Keyframes. shape/positionplaces a subshape with the same vec2 keyframes and tangents.- When the engine enforces scopes, setting keyframes needs the
appearance/animationscope. Aposition,position/xorposition/ytrack also needslayer/move, and ashape/positiontrack needsshape/change.
Troubleshooting#
- The path does not take the values (
BLOCK.KEYFRAMES_VALUE_KIND_MISMATCH): pass vec2 keyframes forpositionandshape/position. - The tangents are refused (
BLOCK.KEYFRAMES_SPATIAL_TANGENT_UNSUPPORTED): setspatialOutandspatialInonly on vec2 keyframes, not onposition/xorposition/y. - The block does not follow the curve: put both tangents of a segment on its first keyframe. A
spatialInon the last keyframe has no effect. - One axis ignores the path: a
position/xorposition/ytrack overrides that axis. Remove it to use the path on both axes.
API Reference#
| Method | Purpose |
|---|---|
engine.block.create(blockType=_) |
Create a graphic block |
engine.block.createShape(type=_) |
Create the 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.appendChild(parent=_, child=_) |
Add a block to the page |
engine.block.setDuration(block=_, duration=_) |
Set the duration that keyframe times refer to |
engine.block.setKeyframes(block=_, property=_, keyframes=_) |
Set the path or a single-axis track |
engine.block.getKeyframes(block=_, property=_) |
Read the keyframes and tangents of a property |
engine.block.setVec2(block=_, property="position", value=_) |
Set the static position |
engine.block.getVec2(block=_, property="position") |
Read the static position |
engine.block.setPlaybackTime(block=_, time=_) |
Seek before you export a frame |
engine.block.export(block=_, mimeType=_) |
Export the frame as PNG |
Next Steps#
- Keyframes: easing, relative tracks and the other keyframe value kinds.
- Create Animations: add preset animations to blocks.