What “Marker-Driven” Actually Means

When Captyne generates animated caption layers, it places named markers on each word layer. These markers are not just visual labels — they are the single source of truth for every animation in a preset. If your preset is built correctly, an animator can change a word’s entire timing by dragging one marker, and every effect updates automatically.

This is the core difference between a basic After Effects caption setup and a Captyne preset. A basic setup has keyframes at hardcoded time positions. A Captyne preset has expressions that read marker positions at runtime, so “the in-animation completes at frame 12” becomes “the in-animation completes at whatever time IN_END is placed” — fully flexible, forever.

The Four Core Markers

Every word layer Captyne generates carries four named markers. Understanding what each one represents is essential before building a preset.

Word Layer Timeline
In
Hold
Out
IN_START
IN_END
MID*
OUT_START
OUT_END
▲ animation plays
▲ word fully visible
▲ exit plays
IN_START
The frame where the in-animation begins. Aligns to the word’s audio timestamp by default.
IN_END
The frame where the in-animation completes and the word is fully visible. Drag to shorten or extend the in-animation.
MID  (optional)
Optional trigger point during the hold phase. Use it for a sweep, pulse, or color shift that fires while the word is fully on screen.
OUT_START  /  OUT_END
OUT_START kicks off the exit animation. OUT_END is when the word is fully gone. The gap between them is the out-animation duration.

How Expressions Read Marker Positions

In After Effects, you access a named marker’s time using marker.key("name").time. Every expression in a Captyne preset uses this to make animations relative to marker positions rather than absolute frame numbers. The pattern looks like this:

// Read marker times into variables var t0 = marker.key("IN_START").time; var t1 = marker.key("IN_END").time; var t2 = marker.key("OUT_START").time; var t3 = marker.key("OUT_END").time; // Normalized progress through the in-phase (0 = not started, 1 = complete) var inProgress = linear(time, t0, t1, 0, 1); // Clamp to prevent values going outside the 0— range inProgress = clamp(inProgress, 0, 1);

The inProgress pattern — a 0-to-1 normalized value — is the foundation of every marker-driven expression. Once you have it, you can drive any property by scaling or remapping it.

Always wrap marker reads in try/catch in production presets. If a word layer is missing a marker (e.g. no MID on simple presets), accessing marker.key("MID") throws an error and breaks the expression. The preset template provides safe wrapper functions — use those in your final saved preset.

Step 1 — Open the Preset Creator

Preset Creator → Create New Preset

In the Captyne panel, go to Presets → Create New Preset. The builder opens with a blank word layer template in a test composition. This template already contains:

Before writing any expressions, spend a minute dragging the markers in the test comp timeline. IN_END controls how fast the word appears. Moving OUT_START left shortens the hold. This physical intuition will make every expression decision clearer.

Step 2 — Build the In-Animation (Opacity + Scale)

Property: Opacity & Scale on CAPTYNE_WORD

The in-animation runs from IN_START to IN_END. Apply the following expression to the Opacity property of the CAPTYNE_WORD text layer. This handles all four phases — before, in, hold, and out — in a single expression:

// -- Opacity: full lifecycle driven by markers ------------------------- var t0 = marker.key("IN_START").time; var t1 = marker.key("IN_END").time; var t2 = marker.key("OUT_START").time; var t3 = marker.key("OUT_END").time; if (time < t0) 0; // before: invisible else if (time <= t1) linear(time, t0, t1, 0, 100); // in: fade up else if (time <= t2) 100; // hold: fully visible else if (time <= t3) linear(time, t2, t3, 100, 0); // out: fade down else 0; // after: invisible

Now add a subtle scale pop on the in-phase by applying a similar expression to the Scale property:

// -- Scale: pops from 85% to 100% during in-phase, holds, then shrinks -- var t0 = marker.key("IN_START").time; var t1 = marker.key("IN_END").time; var t2 = marker.key("OUT_START").time; var t3 = marker.key("OUT_END").time; var s; if (time < t0) s = 85; else if (time <= t1) s = linear(time, t0, t1, 85, 100); else if (time <= t2) s = 100; else if (time <= t3) s = linear(time, t2, t3, 100, 85); else s = 85; [s, s] // return as [x, y] scale array

The values 85 and 100 above are just a starting point. Try 0 and 100 for a hard pop, or 92 and 100 for a very subtle settle. These are the only numbers you’ll be changing — the timing always follows the markers.

Step 3 — Add Easing to the In-Animation

Replace linear() with ease()

The linear() function creates mechanical, robotic motion. Replace it with After Effects’ built-in ease() for a smooth deceleration into the hold phase, or easeIn()/easeOut() to control which end gets the ease:

// Eased opacity — decelerates as it reaches full opacity if (time <= t1) ease(time, t0, t1, 0, 100); // Eased scale — overshoot effect using custom cubic // (bounces slightly past 100% before settling) var p = clamp(linear(time, t0, t1, 0, 1), 0, 1); var overshoot = 1 + (0.12 * Math.sin(p * Math.PI)); // sine arc = single overshoot linear(time, t0, t1, 85, 100) * (p < 1 ? overshoot : 1);

For most caption presets, ease() on opacity and easeOut() on scale gives a natural, confident feel. Experiment in the test comp — you can see results instantly without generating any real caption layers.

Step 4 — Build the MID Marker Effect (Hold-Phase Animation)

Optional: Add a sweep, pulse, or glow during hold

The MID marker lets you fire an animation event while the word is fully visible — between IN_END and OUT_START. This is how the built-in Light Sweep and Neon Glow presets work: the word appears, then a secondary effect plays during the hold phase.

A common pattern is a brightness sweep: a glow or highlight that travels across the word from left to right starting at MID.

// -- MID sweep: drives the position of a CC Light Sweep effect -------- // Apply this to the Center parameter of CC Light Sweep on CAPTYNE_WORD try { var tMid = marker.key("MID").time; var tMidEnd = tMid + 0.4; // sweep lasts 0.4 sec (no extra marker needed) var w = sourceRectAtTime(time).width; var left = [thisComp.width/2 - w/2 - 40, 0]; // start left of word var right = [thisComp.width/2 + w/2 + 40, 0]; // end right of word if (time < tMid) left; else if (time <= tMidEnd) ease(time, tMid, tMidEnd, left, right); else right; } catch(e) { // No MID marker on this word — keep sweep offscreen [thisComp.width * 2, 0]; }
💡

The try/catch here is essential. If a generated word layer has no MID marker (Captyne only adds it when you include MID in the preset definition), the expression would throw an error without it. The catch block returns a safe fallback position instead.

The key technique in the sweep expression is sourceRectAtTime(time).width — this reads the actual pixel width of the text layer at runtime. That is what makes the sweep automatically adapt to words of any length. A three-letter word and a twelve-letter word each get a sweep that travels the right distance, with no per-word configuration.

Step 5 — Build the Out-Animation

Mirror the in-phase from OUT_START to OUT_END

The out-animation is structurally identical to the in-animation, just reversed and starting at OUT_START. If the in-phase fades up and scales from 85 to 100, the out-phase scales from 100 to 85 and fades down. You already handled this in the full lifecycle expression from Step 2.

For out-animations that are different from the in (for example: the word slides up to exit instead of fading), you can write a separate out-phase branch:

// -- Position Y: enters from below, exits upward ----------------------- var t0 = marker.key("IN_START").time; var t1 = marker.key("IN_END").time; var t2 = marker.key("OUT_START").time; var t3 = marker.key("OUT_END").time; var drop = 30; // pixels below final position if (time < t0) [position[0], position[1] + drop]; // starts below else if (time <= t1) ease(time, t0, t1, // slides into place [position[0], position[1] + drop], [position[0], position[1]]); else if (time <= t2) [position[0], position[1]]; // hold else if (time <= t3) ease(time, t2, t3, // exits upward [position[0], position[1]], [position[0], position[1] - drop]); else [position[0], position[1] - drop]; // after: above

Notice that position[0] and position[1] reference the layer’s own keyframed position value, not hardcoded coordinates. This is critical — it means the expression works correctly regardless of where Captyne places the word in the composition.

Step 6 — Connecting Spatial Effects to Word Width

sourceRectAtTime() is your spatial anchor

Many effects need to know the physical dimensions of the word — where its left edge is, where its center is, how wide it is. After Effects provides this through sourceRectAtTime(time) on a text layer, which returns a rectangle object with left, top, width, and height.

// Get the bounding box of the word at the current frame var r = sourceRectAtTime(time); var wordLeft = r.left; // left edge in layer space var wordRight = r.left + r.width; // right edge var wordCenter = r.left + r.width / 2; // horizontal center var wordWidth = r.width; // Convert layer-space point to comp space (needed for effect Center params) var compCenter = fromCompToSurface([thisComp.width/2 + wordCenter, thisComp.height/2]);

Use wordWidth to set effect radii, glow sizes, or sweep distances. Use compCenter to position effects that need an absolute comp-space coordinate (like CC Light Sweep’s Center point). This is the mechanism that makes every built-in Captyne preset automatically adapt to any word length.

Step 7 — Test with Variable Word Lengths

Preset Creator → Preview Panel

The preset builder’s preview panel shows your preset applied to several test words simultaneously: a short word (3–4 characters), a medium word (7–8 characters), and a long word (12+ characters). Before saving, verify:

What to checkWhy it matters
Opacity / scale starts and ends correctlyExpression errors often show as stuck values (always 0 or always 100)
Sweep or glow covers the full word widthShort sweep on a long word means the sourceRect read is wrong
Animation completes before OUT_STARTIf in-animation bleeds into hold, IN_END is too close to IN_START
No expression errors in the info panelAny red error indicator means the expression will fail on generation
Dragging markers updates the animation in real timeIf not, you have a hardcoded frame number somewhere that overrides the expression

Check the longest test word carefully. The most common preset bug is a sweep or glow that looks correct on a short word but undershoots on a long one because sourceRectAtTime was called at the wrong layer time or the width was cached before the text was fully rendered.

Step 8 — Declare Markers in the Preset Definition

Preset Creator → Markers Tab

The preset definition file tells Captyne which markers to place on each generated word layer. Open the Markers tab in the preset builder and configure:

These defaults are what Captyne uses when it auto-places markers during generation. Users can always drag them later — these are just sensible starting positions so the preset looks good without any manual adjustment.

Step 9 — Save and Apply Your Preset

Preset Creator → Save Preset

Give your preset a name, choose an accent color for the dot in the preset list, and click Save Preset. It immediately appears in the Captyne panel’s preset list alongside the built-in styles.

To test on real content: open any composition with audio, run Transcribe, select your new preset from the list, and click Generate Captions. Every generated word layer will have the exact marker structure your preset declared, and your expressions will drive all the animations.

Complete Minimal Preset: Reference Template

Here is a complete working preset template combining all the expressions from this tutorial — opacity fade, scale pop, and an optional MID sweep — in a single copy-paste-ready block:

// ==================================// Captyne Preset: Fade + Scale + Optional MID Sweep // Apply each expression block to its labeled property on CAPTYNE_WORD // ==================================/span> // -- SHARED HEADER (paste at top of each expression) ----------------- var t0 = marker.key("IN_START").time; var t1 = marker.key("IN_END").time; var t2 = marker.key("OUT_START").time; var t3 = marker.key("OUT_END").time; // -- OPACITY ---------------------------------------------------------- if (time < t0) 0; else if (time <= t1) ease(time, t0, t1, 0, 100); else if (time <= t2) 100; else if (time <= t3) ease(time, t2, t3, 100, 0); else 0; // -- SCALE ------------------------------------------------------------- var s; if (time < t0) s = 85; else if (time <= t1) s = ease(time, t0, t1, 85, 100); else if (time <= t2) s = 100; else if (time <= t3) s = ease(time, t2, t3, 100, 85); else s = 85; [s, s]; // -- CC LIGHT SWEEP: Center -------------------------------------------- try { var tM = marker.key("MID").time; var tME = tM + 0.35; var w = sourceRectAtTime(time).width; var cx = thisComp.width / 2; var cy = thisComp.height / 2; var L = [cx - w/2 - 50, cy]; var R = [cx + w/2 + 50, cy]; if (time < tM) L; else if (time <= tME) ease(time, tM, tME, L, R); else R; } catch(e) { [thisComp.width * 2, thisComp.height / 2]; }

Ready to build your own preset?

The preset creator is included in every Captyne beta install. Apply for free access below.

Apply for Free Beta Access →

Common Mistakes and How to Avoid Them

Hardcoded frame numbers in expressions

If you write linear(time, 0, 0.3, 0, 100) instead of linear(time, t0, t1, 0, 100), the animation is no longer marker-driven — it always starts at frame 0 regardless of where IN_START is. Always use the marker time variables, never raw numbers for timing.

Missing try/catch on MID marker reads

Every marker.key("MID") call must be inside a try/catch. If the preset is ever applied to a word layer that has no MID marker, the expression errors silently and the effect may freeze or snap to an unexpected value.

Position expressions not referencing the layer’s own position

If you write a position expression that returns absolute coordinates (e.g. [960, 540]), it will break the moment Captyne places the word somewhere other than dead center. Always offset from position[0] and position[1] to respect whatever position Captyne assigns.

sourceRectAtTime called before IN_START

Text layers can have zero-size bounding boxes before they are visible. Call sourceRectAtTime(t1) (using IN_END time as the sample point) rather than the current time if your effect needs a stable width reference throughout the layer’s life, rather than a potentially-zero value at early frames.