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, and export a frame of the animation.

6 mins
estimated time
Download
StackBlitz
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.

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 Engine
const 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 space
engine.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 pivot
engine.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 pivot
const 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, too
const 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)); // 1
console.log(engine.block.getParent(moon) === page); // true

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(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 degrees
engine.block.setPlaybackTime(page, 1.25);
console.log(
engine.block.getGlobalBoundingBoxX(planet),
engine.block.getGlobalBoundingBoxY(planet)
); // 900 830

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(label, null);
console.log(engine.block.getTransformParent(label)); // null
engine.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'
}
  • 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.

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 pivot
mkdirSync('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 resources
engine.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#