Animate the characters, words or lines of a text block with text animators, such as a letter-by-letter reveal, a highlighted word or a wave.
Text presets animate a whole text with fixed settings. A text animator gives you control over each glyph. It selects a range of characters, words or lines and changes their position, scale, rotation, skew, opacity, colors, stroke width, spacing and blur. Every property of a text animator accepts keyframes, so the range and the changes can move over time. Text animators render in playback and in video export. The editor has no text animator controls, so you set them with the block API. For text presets, see Text Animations.
This guide covers creating and attaching text animators, the glyph properties and the range selector. It also covers keyframes, the order of several animators, removing them, keyframes on the text content, and errors.
Creating the Text Block#
We create a headline with a duration of 5 seconds. The keyframes of its text animators use this duration.
// A 5 second headline in the middle of the pagelet text = try engine.block.create(.text)try engine.block.replaceText(text, text: "Text animators move every letter")try engine.block.setTextFontSize(text, fontSize: 120)try engine.block.setEnum(text, property: "text/horizontalAlignment", value: "Center")try engine.block.setWidth(text, value: 1600)try engine.block.setHeightMode(text, mode: .auto)try engine.block.setPositionX(text, value: 160)try engine.block.setPositionY(text, value: 380)try engine.block.appendChild(to: page, child: text)try engine.block.setDuration(text, duration: 5)Creating a Text Animator#
supportsTextAnimators(_:) returns true for text and caption blocks. createTextAnimator() creates a text animator, and appendTextAnimator(_:animatorID:) attaches it to the end of the text’s animator list.
let textSupportsAnimators = try engine.block.supportsTextAnimators(text) // truelet pageSupportsAnimators = try engine.block.supportsTextAnimators(page) // false
// The covered glyphs sit 60 units lower and are hiddenlet reveal = try engine.block.createTextAnimator()try engine.block.setFloat(reveal, property: "text_animator/positionY", value: 60)try engine.block.setBool(reveal, property: "text_animator/opacityEnabled", value: true)try engine.block.setFloat(reveal, property: "text_animator/opacity", value: 0)try engine.block.appendTextAnimator(text, animatorID: reveal)All properties of a text animator start with text_animator/. A new text animator changes nothing, because its offsets are 0 and its scale is 1. This animator moves the glyphs 60 units down and hides them. The range selector decides which glyphs get these changes. By default it covers the whole text.
| Property | Unit | Effect on a covered glyph |
|---|---|---|
positionX, positionY |
Design units | Moves the glyph |
anchorX, anchorY |
Design units | Moves the pivot for scale, rotation and skew |
scaleX, scaleY |
Factor, 1 is no change | Scales the glyph |
rotation |
Radians | Rotates the glyph |
skew, skewAxis |
Degrees | Skews the glyph along an axis |
opacity |
0 to 1 | Fades the glyph when opacityEnabled is true |
fillColor |
Color | Colors the glyph when fillColorEnabled is true |
strokeColor |
Color | Colors the stroke when strokeColorEnabled is true |
strokeWidth |
Design units | Adds to the stroke width when the text has a stroke |
tracking |
Design units | Adds space, half before and half after the glyph |
blurX, blurY |
Design units | Blurs the glyph |
Animating the Range with Keyframes#
Every text animator property accepts keyframes with setKeyframes(_:property:keyframes:mode:). The keyframe times refer to the duration of the text block. Here selectorStart moves from 0 to 1 in the first 2.5 seconds. The range then covers fewer glyphs, from the left. Each glyph that leaves the range rises to its place and fades in.
// Move the start of the range from 0 to 1 in the first half of the// text's duration. The uncovered glyphs rise and fade in, left to right.try engine.block.setKeyframes(reveal, property: "text_animator/selectorStart", keyframes: [ Keyframe(time: 0, value: .float(0), easing: .preset(.easeOut)), Keyframe(time: 0.5, value: .float(1)),])For more on keyframe values and easing, see Keyframes.
The Range Selector#
The range selector gives each glyph a coverage from 0 to 1. A glyph with full coverage gets the full change of the animator. These properties control the selector:
selectorBasedOnis the element that the selector counts:"Characters","CharactersExcludingSpaces","Words"or"Lines".selectorUnitsis"Percentage"by default, where 0 to 1 spans all elements."Index"counts elements from 0.selectorStart,selectorEndandselectorOffsetdefine the range. The offset moves the whole range.selectorShapeis the coverage curve over the range:"Square","RampUp","RampDown","Triangle","Round"or"Smooth".selectorSmoothnesssoftens the edges of a square.selectorAmountscales the coverage, from -1 to 1.selectorEaseHighandselectorEaseLowbend the curve.selectorRandomizeshuffles the order of the elements. The sameselectorSeedgives the same order on every platform.selectorEnabledset tofalsecovers every glyph.
This animator covers the second word, colors it orange and scales it up.
// Cover the second word only: words are counted from 0 in index unitslet highlight = try engine.block.createTextAnimator()try engine.block.setEnum(highlight, property: "text_animator/selectorBasedOn", value: "Words")try engine.block.setEnum(highlight, property: "text_animator/selectorUnits", value: "Index")try engine.block.setFloat(highlight, property: "text_animator/selectorStart", value: 1)try engine.block.setFloat(highlight, property: "text_animator/selectorEnd", value: 2)
// Color and scale the covered glyphstry engine.block.setBool(highlight, property: "text_animator/fillColorEnabled", value: true)try engine.block.setColor( highlight, property: "text_animator/fillColor", color: .rgba(r: 1, g: 0.5, b: 0, a: 1),)try engine.block.setFloat(highlight, property: "text_animator/scaleX", value: 1.2)try engine.block.setFloat(highlight, property: "text_animator/scaleY", value: 1.2)In index units the default selectorEnd of 1 covers only the first element. Set the end to the index after the last element that you want to cover.
Combining Several Animators#
A text block can have several text animators. They apply in list order, and each adds its change to the result of the animators before it. insertTextAnimator(_:animatorID:index:) attaches an animator at an index, and getTextAnimators(_:) returns the list.
// Insert the highlight before the revealtry engine.block.insertTextAnimator(text, animatorID: highlight, index: 0)let orderAfterInsert = try engine.block.getTextAnimators(text) // [highlight, reveal]A third animator runs a wave across the text. The triangle shape covers a quarter of the characters and peaks in its middle. A keyframed selectorOffset moves this range from before the first character to after the last one.
// A triangle covers a quarter of the characters and peaks in its middlelet wave = try engine.block.createTextAnimator()try engine.block.setEnum(wave, property: "text_animator/selectorShape", value: "Triangle")try engine.block.setFloat(wave, property: "text_animator/selectorEnd", value: 0.25)try engine.block.setFloat(wave, property: "text_animator/positionY", value: -40)try engine.block.setFloat(wave, property: "text_animator/rotation", value: -0.2)
// Move the range across the text over the whole durationtry engine.block.setKeyframes(wave, property: "text_animator/selectorOffset", keyframes: [ Keyframe(time: 0, value: .float(-0.25)), Keyframe(time: 1, value: .float(1)),])try engine.block.appendTextAnimator(text, animatorID: wave)Removing a Text Animator#
removeTextAnimator(_:index:) removes the animator at an index. It does not destroy the animator, so you can attach it again or destroy it with destroy(_:). To change the order, remove an animator and insert it at the new index.
// Remove the highlight at index 0. The animator stays valid.try engine.block.removeTextAnimator(text, index: 0)let highlightIsValidAfterRemove = engine.block.isValid(highlight) // true
// Append it again so that it applies lasttry engine.block.appendTextAnimator(text, animatorID: highlight)let orderAfterReappend = try engine.block.getTextAnimators(text) // [reveal, wave, highlight]print(orderAfterInsert, highlightIsValidAfterRemove, orderAfterReappend)Animating the Text Content#
The text/text property of a text block accepts .discrete(.string(...)) keyframes. The shown text changes at the time of each keyframe. This countdown shows 3, then 2, then 1.
// A second text block counts down from 3 to 1let countdown = try engine.block.create(.text)try engine.block.replaceText(countdown, text: "3")try engine.block.setTextFontSize(countdown, fontSize: 160)try engine.block.setWidthMode(countdown, mode: .auto)try engine.block.setHeightMode(countdown, mode: .auto)try engine.block.setPositionX(countdown, value: 900)try engine.block.setPositionY(countdown, value: 700)try engine.block.appendChild(to: page, child: countdown)try engine.block.setDuration(countdown, duration: 5)
try engine.block.setKeyframes(countdown, property: "text/text", keyframes: [ Keyframe(time: 0, value: .discrete(.string("3"))), Keyframe(time: 0.33, value: .discrete(.string("2"))), Keyframe(time: 0.67, value: .discrete(.string("1"))),])The keyframed text uses the style at the start of the static text. getString(_:property:) and editing in the canvas use the static text, not the keyframed one. Caption blocks accept the same keyframes on caption/text.
Handling Errors#
Invalid input throws an EngineError. Its catalogCode names the problem. A text animator belongs to one text block at a time, so attaching it to a second block fails.
var errorCode: EngineErrorCode?do { // The reveal already belongs to the headline try engine.block.appendTextAnimator(countdown, animatorID: reveal)} catch let error as EngineError { errorCode = error.catalogCode print(errorCode == .blockTextAnimatorAlreadyAttached) // true}Exporting a Frame#
We set the page’s playback time to 2 seconds and export the page as PNG. At that time most of the headline has risen into place, the wave lifts the letters of the second quarter, and the countdown shows 2.
// Export the frame at 2 seconds, during the revealtry engine.block.setPlaybackTime(page, time: 2)let pngData = try await engine.block.export(page, mimeType: .png)Text Animators and Other Block Operations#
Text animators take part in the usual block operations:
- Preset animations apply together with text animators.
- Undo and redo restore the animators of a text and their properties.
- Saving and loading a scene keeps the animators and their keyframes.
- Duplicating a text block copies its animators in their order.
- Destroying a text block destroys its animators. Destroying an animator removes it from its text block.
- Changing the design unit converts the length properties of an animator, such as
tracking. Factors such asscaleXstay the same.
appendTextAnimator(_:animatorID:), insertTextAnimator(_:animatorID:index:) and removeTextAnimator(_:index:) need the appearance/animation scope.
Troubleshooting#
- The block does not accept text animators (
.blockTextAnimatorsUnsupported): use a text or caption block. Check it withsupportsTextAnimators(_:). - The block to attach is not a text animator (
.blockTextAnimatorNotAnAnimator): create the block withcreateTextAnimator(). - The animator already belongs to a block (
.blockTextAnimatorAlreadyAttached): remove it from its text block first, or create a new animator. - The index is out of bounds (
.blockTextAnimatorIndexOutOfBounds): to insert, use an index from 0 to the length ofgetTextAnimators(_:). To remove, use a lower index. - Only the first character changes: in
"Index"units the default end of 1 covers only element 0. SetselectorEnd. - The opacity or a color does not change: set
opacityEnabled,fillColorEnabledorstrokeColorEnabledtotrue. - The stroke width does not change: enable the stroke of the text block.
- A negative amount does not fade the glyphs: a negative coverage moves, scales and rotates glyphs, but it does not change opacity or colors.
API Reference#
Methods#
| Method | Description |
|---|---|
engine.block.supportsTextAnimators(_:) |
Check whether a block accepts text animators |
engine.block.createTextAnimator() |
Create a text animator |
engine.block.appendTextAnimator(_:animatorID:) |
Attach an animator at the end of the list |
engine.block.insertTextAnimator(_:animatorID:index:) |
Attach an animator at an index |
engine.block.getTextAnimators(_:) |
Get the animators of a block in the order they apply |
engine.block.removeTextAnimator(_:index:) |
Remove the animator at an index without destroying it |
engine.block.isValid(_:) |
Check that a removed animator still exists |
engine.block.setFloat(_:property:value:) |
Set a number property of a text animator |
engine.block.setInt(_:property:value:) |
Set an integer property of a text animator, such as selectorSeed |
engine.block.setBool(_:property:value:) |
Turn the opacity or a color of a text animator on |
engine.block.setEnum(_:property:value:) |
Set the selector shape, units or elements, or the text alignment |
engine.block.setColor(_:property:color:) |
Set the fill or stroke color of a text animator |
engine.block.setKeyframes(_:property:keyframes:mode:) |
Animate a text animator property or the text content |
engine.block.create(_:) |
Create the text blocks |
engine.block.replaceText(_:text:in:) |
Set the static text |
engine.block.setTextFontSize(_:fontSize:in:) |
Set the font size |
engine.block.setWidth(_:value:) |
Set the text block width |
engine.block.setWidthMode(_:mode:) |
Let the width follow the text |
engine.block.setHeightMode(_:mode:) |
Let the height follow the text |
engine.block.setPositionX(_:value:) |
Set the x position |
engine.block.setPositionY(_:value:) |
Set the y position |
engine.block.appendChild(to:child:) |
Add the text block to the page |
engine.block.setDuration(_:duration:) |
Set the duration that keyframe times refer to |
engine.block.setPlaybackTime(_:time:) |
Set the time to render |
engine.block.export(_:mimeType:options:onPreExport:uriResolver:onProgress:) |
Export the page at the current playback time |
Next Steps#
- Keyframes: animate single block properties with keyframes and easing.
- Text Animations: apply preset animations to text blocks.
- Animation Overview: understand animation concepts and capabilities.