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.
| Prop | Type | Default | Description |
|---|---|---|---|
fit | 'fill' | 'fit' | 'tile' | 'stretch' | 'fill' | How the frame fills the shape |
playing | boolean | true | Whether the clip advances with the node's clock |
timestamp | number | null | – | Explicit source time in seconds. Set it to drive the playhead yourself; omit it (or pass null) to let the clip play |
playStart | number | 0 | Seconds after the node appears before playback begins |
trimStart / trimEnd | number | – | Region of the source to play |
speed | number | 1 | Playback-rate multiplier |
loop | 'forward' | 'reverse' | 'none' | 'none' | Loop behaviour at trimEnd |
duration | number | trimmed length | Length 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'] })} />
| Prop | Type | Default | Description |
|---|---|---|---|
basis | 'value' | 'simplex' | 'ridged' | 'worley' | 'simplex' | Which noise function the octaves come from |
octaves | number | 4 | How many octaves are summed, 1–8. Each one doubles the cost |
frequency | number | { x, y } | 3 | Cycles of the first octave across the shape |
lacunarity | number | 2 | Frequency multiplier per octave |
gain | number | 0.5 | Amplitude multiplier per octave |
seed | number | 0 | Field offset — random-access into the noise |
offset | { x, y } | { x: 0, y: 0 } | Pan through the field, in cycles |
angle | number | 0 | Rotation of the field in degrees |
colors | Color[] | ['black', 'white'] | Colours the 0–1 field maps onto, low to high |
stops | number[] | evenly spaced | Positions of each colour in 0–1 |
contrast | number | 1 | Steepen (>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 alpha —
return vec4(rgb * a, a). - Don't apply the layer's
opacityyourself. 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.
| Uniform | Type | Value |
|---|---|---|
u_size | vec2 | The fill's resolved bounds size, in local px |
u_resolution | vec2 | The same thing, under the name web shaders usually use |
u_origin | vec2 | The bounds' top-left corner, in local px |
u_aspect | float | width / height |
u_time | float | Seconds the node carrying the fill has existed |
u_scale | float | Device 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.
coords | fragCoord is |
|---|---|
'normalized' (default) | 0–1 across the bounds on both axes. Stretches with the shape |
'centered' | (0,0) at the centre, y spanning -0.5–0.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
| Type | Required props | Optional props |
|---|---|---|
'solid' | color | opacity, blend |
'linear-gradient' | colors, stops | start, end, blend |
'radial-gradient' | colors, stops, radius | center, blend |
'conic-gradient' | colors, stops | center, startAngle, blend |
'image' | src | fit, crop, zoom, anchor, matrix, filters, opacity, blend |
'video' | src | fit, 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' | source | uniforms, coords, textures, opacity, blend |
'view3D' | graphics3D | maxPixelRatio, 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:
| Mode | Description |
|---|---|
'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 |