Image Filters Overview

An image filter is a pixel operation applied to one media layer's own pixels — an Image node, or an image/video fill — via the filters prop. A node effect applies to a node and everything drawn inside it; a filter stops at the layer it is attached to.

That difference is the whole point. To make a card's background image look like an oil painting, an effect would have to go on the card — and would paint the card's text and icons as brushwork too, so the background has to be split into its own node behind the content. As a filter it stays on the fill:

<Rect
  width="fill" height="fill" cornerRadius={32}
  fill={Fills.image('./bg.jpg', { fit: 'fill', filters: ImageFilters.oilPaint(4) })}
>
  <Text text="still sharp" fontSize={64} />
</Rect>

A filter is also confined to the shape carrying it: it is clipped to the fill's own outline, corner radius and all.

Where filters apply

// On an Image node
<Image src="./photo.jpg" fit="fill" width={600} height={400} filters={ImageFilters.grayscale(1)} />

// On an image fill
<Rect fill={Fills.image('./photo.jpg', { filters: ImageFilters.blur(5) })} />

// On a video fill — same filters, plus the video-only temporal ones
<Rect fill={Fills.video('./clip.mp4', { filters: VideoFilters.posterizeTime(6).halftone(8) })} />

Using the ImageFilters builder

ImageFilters is an empty, immutable chain: each method returns a new chain with the filter appended, so chains are safe to share and branch.

import { ImageFilters } from 'motion-script';

<Image src="./photo.jpg" fit="fill" width={600} height={400}
  filters={ImageFilters.grayscale(1).blur(3).colorAdjustment({ brightness: 0.1 })}
/>

Filters interpolate, so a chain can be tweened like any other fill property:

yield* card().to(
  { fill: Fills.image('./bg.jpg', { filters: ImageFilters.halftone(24) }) },
  2, easeInOut('quad'),
);

Available filters

Nearly every scene effect is also a filter — same name, same options, same implementation — so ImageFilters.dither(…) and Effects.dither(…) do the same thing to different things. See each effect's page for its options.

GroupFilters
Blur & lightblur, directionalBlur, radialBlur, progressiveBlur, bloom, streak, godRays, sharpen
Colourgrayscale, exposure, alpha, invert, vintage, duotone, posterize, threshold, bitCrush, curves, colorAdjustment, colorMatrix
Texture & patterndither, halftone, grain, scanlines, pixelate, oilPaint, ascii, edges, vignette, texture
Displacement & lenschromaticAberration, rgbShift, scatter, blockDisplace, bulge, twirl, wave, kaleidoscope, displace, sksl
Video only (VideoFilters)posterizeTime, echo

Four effects are deliberately not filters, because a fill layer has nothing to give them: magnify samples the backdrop beneath the node, motionBlur and trails are derived from the node's own velocity, and outline draws outside the silhouette a fill is clipped to. Use them as node effects.

A filter takes the matching effect's options minus mode — a filter is on the fill's own pixels by definition, so there is no backdrop to point it at.

Chaining and order

Filters apply in the order they are chained:

const look = ImageFilters
  .grayscale(0.6)
  .colorAdjustment({ contrast: 1.2, highlights: -0.1 });

<Image src="./photo.jpg" fit="fill" width={600} height={400} filters={look} />

One ordering caveat. Filters take one of two render paths: colour transforms compose into a single image filter, while the ones that resample pixel positions (oilPaint, dither, halftone, twirl, …) wrap the fill's shader. A resampler always runs before the composed colour pass, because a shader's output is what the image filter is applied to. So oilPaint(4).grayscale(1) is honoured exactly, while grayscale(1).oilPaint(4) also paints brushwork over grey — the difference being whether the brush windows saw colour. Put resamplers first when the distinction matters.

Raw objects

A chain is sugar. Any filter can be written as a plain object, and arrays mix freely:

filters={[
  { type: 'grayscale', amount: 0.6 },
  { type: 'oilPaint', radius: 4 },
]}