Skip to content

Effects

Effects are declared inside an effects { } block. The calls live in filmstrip-effects, which comes with the filmstrip bundle.

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
val composition =
compositionOf {
clip(MediaSource.of("/path/to/clip.mp4")) {
effects {
rotate(90)
flip(FlipAxis.Horizontal)
crop(AspectRatio.Portrait, anchor = Anchor.TopCenter)
scale(720)
}
}
}
  • rotate turns 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.
  • flip mirrors across FlipAxis.Horizontal or FlipAxis.Vertical.
  • crop(aspect) reframes to an aspect ratio, keeping the region around its anchor.
  • scale sets the output height. The width follows from the aspect, and each side rounds down to the encoder’s alignment, which the plan reports as a ResolutionClamped adjustment.

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.

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.