Effects
Effects are declared inside an effects { } block. The calls live in filmstrip-effects, which comes with the
filmstrip bundle.
Where an effect applies
Section titled “Where an effect applies”An effects block can sit on a clip, a track or the composition. A clip’s effects run first, then its track’s, then
the composition’s.
val composition = compositionOf { clip(MediaSource.of("/path/to/intro.mp4")) { effects { brightness(1.2f) } } clip(MediaSource.of("/path/to/main.mp4")) { effects { grayscale() } } effects { crop(AspectRatio.Square) } }The order you write effects in doesn’t matter. The plan sorts them into stages, so a watermark added before a crop is still drawn after it.
| Stage | What runs there |
|---|---|
Geometry |
Rotate, flip, crop and scale. This stage decides the frame size |
Color |
Per-pixel color effects |
Spatial |
Effects that sample neighbouring pixels |
Composite |
Overlays |
Geometry
Section titled “Geometry”val composition = compositionOf { clip(MediaSource.of("/path/to/clip.mp4")) { effects { rotate(90) flip(FlipAxis.Horizontal) crop(AspectRatio.Portrait, anchor = Anchor.TopCenter) scale(720) } } }rotateturns the frame counter-clockwise and bakes the turn into the pixels. A source’s own orientation is already corrected, so this is a turn on top of that. A multiple of 90 turns the frame exactly. Any other angle grows the frame to the bounding box of the turned picture, with black in the corners.flipmirrors acrossFlipAxis.HorizontalorFlipAxis.Vertical.crop(aspect)reframes to an aspect ratio, keeping the region around its anchor.scalesets the output height. The width follows from the aspect, and each side rounds down to the encoder’s alignment, which the plan reports as aResolutionClampedadjustment.
crop also takes a rectangle in fractions of the frame, measured after whatever geometry ran before it. A rectangle
with no area or reaching outside the frame throws IllegalArgumentException when you build it.
val composition = compositionOf { clip(MediaSource.of("/path/to/clip.mp4")) { effects { crop(NormalizedRect(left = 0.1f, top = 0.1f, right = 0.9f, bottom = 0.9f)) } } }Fit says how a frame lands in a shape it doesn’t match. Contain letterboxes, writing real bars into the file. Crop
fills and cuts the overflow. Stretch fills each axis on its own.
scale defaults to Contain and crop(aspect) defaults to Crop. Each clip gets its own fit, from the last scale
across its clip, track and composition effects. A crop with Fit.Contain or Fit.Stretch makes a canvas of that
aspect instead of cutting. A composition effect after it runs on the whole canvas, bars included. A clip effect after it
leaves the bars clear until the fill lands, and only an effect that moves pixels, like vhs, reaches
into one.
Whatever a clip leaves uncovered shows the composition’s fill, which defaults to black. Fill.Solid takes a color and
Fill.Blur draws a blurred copy of the clip behind it.
val composition = compositionOf { clip(MediaSource.of("/path/to/landscape.mp4")) clip(MediaSource.of("/path/to/portrait.mp4")) { effects { scale(1080, Fit.Crop) } } fill(Fill.Blur) }A gap where no clip plays has no frame to blur, so a blurred fill shows black there.
val composition = compositionOf { clip(MediaSource.of("/path/to/clip.mp4")) { effects { contrast(1.1f) saturation(0.8f) sepia(0.25f) } } }| Call | What it does |
|---|---|
brightness |
Multiplies every channel. 1f leaves the frame unchanged |
contrast |
Scales channels about mid grey. 1f is unchanged |
saturation |
Scales color. 0f is grey and grayscale() is that shortcut |
hueRotate |
Turns the hue by a number of degrees |
sepia |
Blends toward a sepia tone. 1f is the full effect |
invert |
Blends toward the negative. 1f is the full effect |
rgbAdjustment |
Scales red, green and blue on their own |
colorMatrix |
A hand-written ColorMatrix |
A run of color effects multiplies out before anything clamps, so a factor above one followed by its reciprocal gives
back the frame you started with. A clip’s run includes its track’s effects, and the composition’s effects run separately.
A value that isn’t a finite number throws IllegalArgumentException when you build the effect.
An effect that changes nothing, like a brightness of one, is left out of the plan and listed on its inert notices.
Pan and zoom
Section titled “Pan and zoom”kenBurns travels between two regions of the frame over the clip’s length. It’s marked @ExperimentalFilmstripApi.
val composition = compositionOf { clip(MediaSource.of("/path/to/clip.mp4")) { effects { kenBurns( from = NormalizedRect.Full, to = NormalizedRect(left = 0.25f, top = 0.25f, right = 0.75f, bottom = 0.75f), ) } } }A region whose shape differs from the frame’s stretches to fill it. Its result depends on where a frame sits in the clip, so the effect is only valid on a clip. A plan refuses it on a track, on the composition and on a clip of a looping track.