Overlays
Overlays are effects, so they go in an effects { } block. They run after geometry has settled, which is what makes
their measurements fractions of the final frame. An overlay on a clip is measured against the clip’s frame, and one on
the composition against the output frame.
imageOverlay draws a picture into a corner.
val composition = compositionOf { clip(MediaSource.of("/path/to/clip.mp4")) effects { imageOverlay( image = ImageSource.of("/path/to/logo.png"), corner = Corner.BottomEnd, scale = 0.15f, opacity = 0.8f, ) } }scaleis the picture’s width as a fraction of the frame’s width, a fifth by default. The height follows the picture’s own aspect.marginis the inset from the corner as a fraction of the frame’s shorter side, 4% by default.opacityruns from0fto1f.
The image comes from an ImageSource, which is a path, a URI or encoded bytes. The picture is drawn in the color its
file describes, and an untagged picture is read as sRGB. SDR output clips a tagged picture’s colors to BT.709, and an
HDR grade keeps them inside BT.2020. When a platform’s decoder can’t land the picture that way, the effect resolves as
degraded or unsupported and says why.
An export reads a path or URI picture as it is on disk when the export starts, so a file changed after planning draws as changed and a deleted one is refused by name. A browser fetches a URI again for each export, and refuses it by name if the fetch takes longer than 60 seconds. Bytes are held by value and never change.
textOverlay burns text into the frame. A preview and its export break lines on the same words.
val composition = compositionOf { clip(MediaSource.of("/path/to/clip.mp4")) effects { textOverlay( text = "Day one", at = 2.seconds..6.seconds, style = TextStyle( fontSize = 0.05f, weight = FontWeight.Bold, backgroundColor = 0x80000000.toInt(), ), anchor = Anchor.TopCenter, ) } }Every size in a TextStyle is a fraction of the frame. fontSize is the cap height against the frame’s height, 6% by
default, and maxWidth is where lines wrap, 90% of the width by default. Colors are packed ARGB, as 0xAARRGGBB, and
backgroundColor draws a plate behind the text.
Text has no margin of its own. The anchor names the same point on the text and on the frame, so Anchor.BottomCenter,
the default, puts the bottom edge of the text on the bottom edge of the frame.
When it shows
Section titled “When it shows”Without a window an overlay draws for its whole scope. Both calls take at, a range of composition time where the end is
exclusive, or a visibleDuring TimeRange if you already have one.
Animation
Section titled “Animation”Every overlay takes an animation, asked once per output frame how to draw. fade ramps the opacity.
val composition = compositionOf { clip(MediaSource.of("/path/to/clip.mp4")) effects { textOverlay( text = "Day one", at = 2.seconds..6.seconds, animation = fade(inDuration = 500.milliseconds, outDuration = 1.seconds), ) } }fade takes either side alone, and a side you leave out has no ramp. So fade(inDuration = 500.milliseconds) only
fades in. Each ramp is linear, and a range shorter than the two ramps takes the lower of the two opacities.
slideIn and slideOut move the overlay between its authored position and an offset. + runs two animations together.
Opacities and scales multiply, and offsets add.
val appear = fade(inDuration = 300.milliseconds)val arrive = appear + slideIn(OverlayOffset(x = -0.2f, y = 0f), 300.milliseconds)
val composition = compositionOf { clip(MediaSource.of("/path/to/clip.mp4")) effects { imageOverlay( image = ImageSource.of("/path/to/logo.png"), corner = Corner.TopStart, animation = arrive, ) } }An offset is a fraction of the frame, with positive x towards the end edge and positive y down. An offset can carry
the overlay off the frame, and the part that leaves isn’t drawn.
Anything else is a lambda, since OverlayAnimation is a fun interface. It reads an OverlayTime, where elapsed and
remaining count across the range the overlay is drawn over and composition is the frame’s place on the timeline. It
answers an OverlayFrame with an opacity, an offset and a scale.
val pulse = OverlayAnimation { time -> val grown = 1f + (time.elapsed.inWholeMilliseconds % 1000) / 10000f OverlayFrame(scale = grown) }The scale grows the overlay away from the point it’s anchored by. On text it resamples the raster, and never lays the glyphs out again.
A range with no end reads remaining as infinite, so the out side of a fade holds full opacity and slideOut holds the
authored position.
Animations are compared, since two overlays that draw differently can’t share a cached frame. The built-in helpers compare by value. A lambda compares by identity, so hold one instance and reuse it, or every rebuilt composition misses the thumbnail cache.
An overlay at zero opacity, empty text or a window outside its scope draws nothing. The plan leaves it out and lists it on its inert notices.