Fills

Every shape node accepts a fill prop. Pass a CSS color string as a shorthand, or a fill object for gradients, images, and layering.

Solid color

// CSS shorthand
<Rect fill="royalblue" />

// Explicit object
<Rect fill={{ type: 'color', color: '#4f80ff' }} />

// With opacity and blend mode
<Rect fill={{ type: 'color', color: 'white', opacity: 0.6, blend: 'screen' }} />

Colors accept any CSS color string ('red', '#ff0000', 'rgb(255,0,0)', 'hsl(0,100%,50%)') or an RGBA tuple ([1, 0, 0, 1]).


Gradients

Linear gradient

<Rect fill={{
  type: 'linear-gradient',
  colors: ['#4f80ff', '#e84393'],
  stops: [0, 1],
}} />

// Custom direction — start and end are { x, y } in node-local space
<Rect fill={{
  type: 'linear-gradient',
  colors: ['#4f80ff', 'transparent', '#e84393'],
  stops: [0, 0.5, 1],
  start: { x: -0.5, y: 0.5 },
  end:   { x:  0.5, y: -0.5 },
}} />

Radial gradient

<Rect fill={{
  type: 'radial-gradient',
  colors: ['white', 'transparent'],
  radius: 200,
}} />

// Off-center focal point
<Rect fill={{
  type: 'radial-gradient',
  colors: ['#e84393', 'transparent'],
  radius: 300,
  center: { x: -0.3, y: 0.2 },
}} />

Conic gradient

<Rect fill={{
  type: 'conic-gradient',
  colors: ['red', 'orange', 'yellow', 'green', 'blue', 'red'],
  stops: [0, 0.2, 0.4, 0.6, 0.8, 1],
}} />

Image fill

<Rect fill={{
  type: 'image',
  src: './texture.png',
  fit: 'fill',   // 'fill' (cover+crop, default) | 'fit' | 'tile' | 'stretch'
}} />

Video fill

A playing clip, painted through whatever shape carries it — so it is clipped to an ellipse, a path, or a run of text just like any other fill.

<Ellipse fill={Fills.video('./clip.mp4', { fit: 'fill' })} width={400} height={400} />

The frame it shows is worked out as it paints, from how long the node carrying the fill has existed — so the clip runs on its own wherever paint is accepted: a fill, an overlay, inside a stroke or shadow, or in a custom node's raw Graphics. Nothing has to advance it, and a frame looks the same whether you scrubbed to it, exported it, or played into it.

PropTypeDefaultDescription
fit'fill' | 'fit' | 'tile' | 'stretch''fill'How the frame fills the shape
playingbooleantrueWhether the clip advances with the node's clock
timestampnumber | nullExplicit source time in seconds. Set it to drive the playhead yourself; omit it (or pass null) to let the clip play
playStartnumber0Seconds after the node appears before playback begins
trimStart / trimEndnumberRegion of the source to play
speednumber1Playback-rate multiplier
loop'forward' | 'reverse' | 'none''none'Loop behaviour at trimEnd
durationnumbertrimmed lengthLength of one loop cycle

timestamp is the time-remap knob: tween it to scrub the clip, or set it with playing: false to hold one frame. A paused clip with no timestamp sits on its first frame.

Cross-fading a video in over a node that already existed opens the clip partway through — the node's age is the playback clock. Give it playStart to line the start back up:

yield* rect().fillTo(Fills.video('./clip.mp4', { playStart: 4 }), 0.5); // 4s into the node's life

Multiple fills

Pass an array to layer fills bottom to top:

<Rect fill={[
  { type: 'color', color: '#0f1117' },
  { type: 'linear-gradient', colors: ['#4f80ff33', 'transparent'], stops: [0, 1] },
]} />

Procedural fills

Three fills generate their own pixels rather than taking a colour or an image. The first two expose a fixed set of knobs; the third hands you the shader.

Noise — speckle

Fills.noise paints sparse per-pixel speckle: dust, static, stipple.

<Rect fill={['#0D0F15', Fills.noise({ density: 0.4, color: 'white', opacity: 0.15 })]} />

Tween seed in whole steps to make the static crawl.

Fractal noise — fields

Fills.fractalNoise is a continuous field rather than speckle — several octaves of smooth noise summed at rising frequency and falling amplitude, which is what produces cloud, smoke, marble, wood, terrain and plasma.

// Drifting cloud
<Rect fill={Fills.fractalNoise({ basis: 'value', colors: ['#10131b', '#6990DD'] })} />

// Marble veining
<Rect fill={Fills.fractalNoise({ basis: 'ridged', octaves: 5, colors: ['#1a1206', '#F5C26B'] })} />
PropTypeDefaultDescription
basis'value' | 'simplex' | 'ridged' | 'worley''simplex'Which noise function the octaves come from
octavesnumber4How many octaves are summed, 18. Each one doubles the cost
frequencynumber | { x, y }3Cycles of the first octave across the shape
lacunaritynumber2Frequency multiplier per octave
gainnumber0.5Amplitude multiplier per octave
seednumber0Field offset — random-access into the noise
offset{ x, y }{ x: 0, y: 0 }Pan through the field, in cycles
anglenumber0Rotation of the field in degrees
colorsColor[]['black', 'white']Colours the 0–1 field maps onto, low to high
stopsnumber[]evenly spacedPositions of each colour in 01
contrastnumber1Steepen (>1) or flatten (<1) the field before the ramp

Choosing a basis:

  • 'value' — the cheapest and softest: clouds, haze, paper fibre, subtle mottling.
  • 'simplex' — more even, fewer axis-aligned artefacts. The right default for anything organic seen up close.
  • 'ridged' — sharp branching crests: mountains, lightning, veins, marble.
  • 'worley' — cellular distance, so it gives edges rather than blur: scales, cracked earth, caustics, stained glass.

Animating: tween offset to travel through the field (it flows); tween seed to shift to a different field entirely (it churns). basis and octaves snap rather than interpolating, because both are baked into the compiled shader.

It composes with the effect layer rather than duplicating it — duotone over it is marble, threshold over it is an organic matte, posterize over it is topographic banding, and displace is water.

Custom shader — your own SkSL

Fills.shader is the escape hatch past the built-in procedural fills: you write the fragment function.

const GLOW = `
uniform float u_amount;

vec4 main(vec2 uv) {
    float d = 1.0 - length(uv - 0.5) * 2.0;
    float a = smoothstep(0.0, 1.0, d) * u_amount;
    return vec4(vec3(0.41, 0.56, 0.87) * a, a);   // premultiplied
}`;

<Rect width={400} height={400} fill={Fills.shader(GLOW, { uniforms: { u_amount: 1 } })} />

It is a fill, so it paints inside the shape's own path and stacks like any other layer — the same shader clips to an ellipse, a path or a run of text, and takes opacity, blend and space for free:

<Rect fill={['#0b0d12', Fills.shader(GLOW, { blend: 'screen', opacity: 0.8 })]} />
<Ellipse fill={Fills.shader(GLOW)} />
<Text text="DEPTH" fontSize={320} fill={Fills.shader(GLOW)} />
<Rect stroke={{ fill: Fills.shader(GLOW), weight: 12 }} />

The shader contract

The source must declare vec4 main(vec2 fragCoord), and it is SkSL — GLSL-like, with float2/float4 and their vec2/vec4 aliases, and the usual mix/smoothstep/clamp/length/fract/sin built-ins.

Two rules:

  • Return premultiplied alphareturn vec4(rgb * a, a).
  • Don't apply the layer's opacity yourself. It is already on the paint as an alpha your output is modulated by, so folding it in again renders at the square of what you asked for. Same for a node's own opacity.

Note

Keep the source constant. It is the shader compile cache's key, so building it per frame — interpolating a changing value into a template literal — compiles a new GPU program every frame. Hoist it to module scope and put everything that moves in a uniform, which is an in-place write and costs nothing.

Uniforms

Uniforms are supplied by name, in any order — the renderer reflects the compiled program to find where each one goes. A name the shader doesn't declare, or one it declares that you didn't supply, is reported in the console instead of silently rendering wrong.

Fills.shader(SRC, {
    uniforms: {
        u_amount: 0.6,             // float
        u_centre: [0.5, 0.5],      // vec2
        u_tint: [0.9, 0.3, 0.4, 1],// vec4
        u_seed: 7,                 // int
    },
})

Declare any of these and the renderer fills it in. An entry in uniforms with the same name wins.

UniformTypeValue
u_sizevec2The fill's resolved bounds size, in local px
u_resolutionvec2The same thing, under the name web shaders usually use
u_originvec2The bounds' top-left corner, in local px
u_aspectfloatwidth / height
u_timefloatSeconds the node carrying the fill has existed
u_scalefloatDevice px per local px

Two caveats. u_time is node age, not scene time — it starts when the node appears, which is what makes it identical whether you played to a frame or seeked to it. And u_scale folds in the pixel ratio and the camera zoom, so a 4× export sees a different value from the player: use it for a hairline that should stay one pixel wide, not for anything that decides the look.

Coordinates

coords chooses what fragCoord means. It is applied as a matrix on the shader, so one compiled program serves every size and every mode.

coordsfragCoord is
'normalized' (default)01 across the bounds on both axes. Stretches with the shape
'centered'(0,0) at the centre, y spanning -0.50.5, square pixels. x spans ±u_aspect/2. What anything radial wants
'local'Raw shape-local px. For a pattern whose pitch is an authored size, the way Fills.stripe's gap is

A circle is round under 'centered' with no correction, and an ellipse under 'normalized' on any non-square shape.

Sampling images

Bind an image to a uniform shader declaration by name. The fill doesn't paint until the image has decoded, the same as an image fill.

const RIPPLE = `
uniform shader u_photo;
uniform vec2  u_size;
uniform float u_amount;

vec4 main(vec2 uv) {
    float wave = sin(uv.y * 24.0) * 0.02 * u_amount;
    return u_photo.eval((uv + vec2(wave, 0.0)) * u_size);
}`;

<Rect width={640} height={400} fill={Fills.shader(RIPPLE, {
    textures: [{ name: 'u_photo', src: 'kingfisher.jpg' }],
    uniforms: { u_amount: 1 },
})} />

The sampler is evaluated in your own coordinate space, so you do the mapping — multiplying by u_size above turns the normalised coordinate back into the image's pixel space. Up to four textures; they tile (repeat) past their edges.

Animating: uniform values interpolate, component-wise for vectors, and so does opacity. source, coords and textures snap at the midpoint of a tween rather than cross-fading — the source is the compile-cache key, and there is no halfway value between two programs. To dissolve between two shaders, stack them as two layers and tween their opacities in opposite directions.

Shader fills also paint a node's stroke and its shadow, so a shader-filled shape casts a shader-coloured shadow rather than a flat silhouette.


Fill types reference

TypeRequired propsOptional props
'solid'coloropacity, blend
'linear-gradient'colors, stopsstart, end, blend
'radial-gradient'colors, stops, radiuscenter, blend
'conic-gradient'colors, stopscenter, startAngle, blend
'image'srcfit, crop, zoom, anchor, matrix, filters, opacity, blend
'video'srcfit, crop, zoom, anchor, matrix, filters, playing, timestamp, playStart, trimStart, trimEnd, speed, loop, duration, opacity, blend
'noise'size, density, color, seed, opacity, blend
'fractalNoise'basis, octaves, frequency, lacunarity, gain, seed, offset, angle, colors, stops, contrast, opacity, blend
'stripe'gap, strokeWidth, angle, color, opacity, blend
'shader'sourceuniforms, coords, textures, opacity, blend
'view3D'graphics3DmaxPixelRatio, antialias, opacity, blend

Every fill also takes space ('local' | 'parent' | 'global'), which chooses the rect it resolves against, and a plain CSS colour string is shorthand for 'solid'.


Animating fills

Fill colors and gradient stops are all animatable with .to():

import { createScene, Rect, createRef, easeInOut } from 'motion-script';

export default createScene(function* (stage) {
  const box = createRef<Rect>();

  stage.add(<Rect ref={box} width={300} height={200} fill="royalblue" />);

  // Animate to a new color
  yield* box().to({ fill: '#e84393' }, 1, easeInOut);

  // Animate opacity of a fill object
  yield* box().to({ fill: { type: 'color', color: 'white', opacity: 0 } }, 0.5);
});

Blend modes

The blend key on any fill object controls how it composites with fills below it. Available modes:

ModeDescription
'normal'Standard alpha composite
'multiply'Darkens
'screen'Lightens
'overlay'Contrast boost
'darken'Keeps darker pixels
'lighten'Keeps lighter pixels
'color-dodge'Brightens based on top layer
'color-burn'Darkens based on top layer
'hard-light'High-contrast overlay
'soft-light'Subtle contrast
'difference'Subtracts colors
'exclusion'Lower-contrast difference
'hue'Hue of top, saturation/luminosity of bottom
'saturation'Saturation of top, hue/luminosity of bottom
'color'Hue and saturation of top, luminosity of bottom
'luminosity'Luminosity of top, hue/saturation of bottom