Make a block follow the position, rotation and scale of another block, like a parented layer in a motion design tool, and export a frame of the animation.
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.
Creating the Scene#
We start the headless engine and create a scene with a page of 5 seconds, so the transform parent can animate. Transform parents also work in a design scene.
// Initialize the headless Creative Engineconst engine = await CreativeEngine.init({ baseURL: process.env.IMGLY_LOCAL_ASSETS_URL // license: process.env.CESDK_LICENSE});
try { // Create a scene with a 5 second page const scene = engine.scene.create('DepthStack'); const page = engine.block.create('page'); engine.block.setWidth(page, 1920); engine.block.setHeight(page, 1080); engine.block.appendChild(scene, page); engine.block.setDuration(page, 5);Animating a Hidden Parent#
A transform parent can be any graphic or text block. The example draws its blocks as circles with a small helper function.
/** Creates a circle graphic of the given size and color on the page. */function createCircle( engine: CreativeEngine, page: number, size: number, color: { r: number; g: number; b: number; a: number }): number { const block = engine.block.create('graphic'); engine.block.setShape(block, engine.block.createShape('ellipse')); const fill = engine.block.createFill('color'); engine.block.setColor(fill, 'fill/color/value', color); engine.block.setFill(block, fill); engine.block.setWidth(block, size); engine.block.setHeight(block, size); engine.block.appendChild(page, block); engine.block.setDuration(block, 5); 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(pivot, 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.
const pivot = createCircle(engine, page, 20, { r: 0, g: 0, b: 0, a: 1 });engine.block.setPositionX(pivot, 950);engine.block.setPositionY(pivot, 530);engine.block.setFloat(pivot, 'transform/anchor/x', 10);engine.block.setFloat(pivot, 'transform/anchor/y', 10);engine.block.setKeyframes(pivot, 'rotation', [ { time: 0, value: { type: 'Float', value: 0 } }, { time: 1, value: { type: 'Float', value: 2 * Math.PI } }]);engine.block.setVisible(pivot, false);Linking a Block#
setTransformParent(block, parent) makes the block follow the transform of the parent. getTransformParent() 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.
const planet = createCircle(engine, page, 120, { r: 0.2, g: 0.4, b: 0.9, a: 1});engine.block.setTransformParent(planet, pivot);console.log(engine.block.getTransformParent(planet) === pivot); // true// The position is now an offset from the pivot's top-left corner,// in the pivot's turning spaceengine.block.setPositionX(planet, 300);engine.block.setPositionY(planet, -50);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 pivotengine.block.setFloat(planet, 'transform/anchor/x', 60);engine.block.setFloat(planet, 'transform/anchor/y', 60);engine.block.setKeyframes(planet, 'rotation', [ { time: 0, value: { type: 'Float', value: 0 } }, { time: 1, value: { type: 'Float', value: 4 * Math.PI } }]);
// The moon follows the planet, which follows the pivotconst moon = createCircle(engine, page, 40, { r: 0.85, g: 0.85, b: 0.8, a: 1});engine.block.setTransformParent(moon, planet);engine.block.setPositionX(moon, 160);engine.block.setPositionY(moon, 40);
// A text label follows the planet, tooconst label = engine.block.create('text');engine.block.replaceText(label, 'Planet');engine.block.setTextFontSize(label, 36);engine.block.setWidthMode(label, 'Auto');engine.block.setHeightMode(label, 'Auto');engine.block.appendChild(page, label);engine.block.setTransformParent(label, planet);engine.block.setPositionX(label, 0);engine.block.setPositionY(label, 140);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() still returns the page for the moon.
engine.block.setOpacity(planet, 0.5);console.log(engine.block.getOpacity(moon)); // 1console.log(engine.block.getParent(moon) === page); // trueReading 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(page, 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 degreesengine.block.setPlaybackTime(page, 1.25);console.log( engine.block.getGlobalBoundingBoxX(planet), engine.block.getGlobalBoundingBoxY(planet)); // 900 830Removing the Link#
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 thereengine.block.setTransformParent(label, null);console.log(engine.block.getTransformParent(label)); // nullengine.block.setPositionX(label, 80);engine.block.setPositionY(label, 80);Handling Errors#
A failed setTransformParent() throws an error 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.
try { // The pivot cannot follow a block that follows it engine.block.setTransformParent(pivot, moon);} catch (error) { // Engine errors carry a stable catalog id in `code` const { code } = error as { code?: string }; console.log(code); // 'BLOCK.TRANSFORM_PARENT_CYCLE'}try { // Only graphic and text blocks take part in a link engine.block.setTransformParent(planet, page);} catch (error) { const { code } = error as { code?: string }; console.log(code); // 'BLOCK.TRANSFORM_PARENT_UNSUPPORTED'}Links and Other Block Operations#
- 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 thelayer/movescope.
Exporting a Frame#
The playback time is still 1.25 seconds, so we export the page as a PNG image. 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 pivotmkdirSync('output', { recursive: true });const blob = await engine.block.export(page, { mimeType: 'image/png' });writeFileSync( 'output/transform-parent.png', Buffer.from(await blob.arrayBuffer()));console.log('Frame saved to output/transform-parent.png');Cleaning Up#
We dispose of the engine in a finally block, so it frees its resources also after an error.
// Always dispose of the engine to free resourcesengine.dispose();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, 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 | Description |
|---|---|
block.setTransformParent(block, parent) |
Make a block follow the transform of another block, or pass null to remove the link |
block.getTransformParent(block) |
Get the block that a block follows, or null |
block.setKeyframes(block, property, keyframes) |
Animate the transform parent |
block.setVisible(block, visible) |
Hide the transform parent, which still moves its followers |
block.setOpacity(block, opacity) |
Set the opacity, which a follower does not inherit |
block.getParent(block) |
Get the parent in the block hierarchy, which a link does not change |
block.setPlaybackTime(block, time) |
Seek before you read the animated position |
block.getGlobalBoundingBoxX(block) |
Read the x position of a follower at the current playback time |
block.getGlobalBoundingBoxY(block) |
Read the y position of a follower at the current playback time |
Next Steps#
- Keyframes: animate the transform parent with keyframe tracks.
- Create Animations: add preset animations, which a follower also follows.
- Import Lottie Animations: the Lottie import links each layer to its Lottie parent layer with a transform parent.