render()
render() writes an MP4 from the page, with an optional shutter for motion blur and a finishing pass of grain, vignette, bloom and a grade.
render() draws the page with drawElementImage, which Chrome has only with HTML-in-Canvas enabled until Chrome 157, and throws without it (KN-201).
Media from another origin needs crossorigin="anonymous" and CORS headers, or render() throws (KN-204).
A render begins with a run of readiness, and check() tests the same seeks without writing a file.
render(options)
render() writes an MP4 from the page, with no renderer installed.
Each frame is a seek, settled before it is captured, drawn into a canvas, encoded with WebCodecs and written by Kinetic's MP4 writer.
A composition with an audio clip gets a sound track, the clips mixed and encoded the same way (Sound in a render).
const { blob, frames, ms } = await wildflower.kinetic.render();
| Option | Default | Meaning |
|---|---|---|
fps | The clock's | Frames per second. |
from, to | 0 and the composition's duration | The range to render, in seconds. to has to be after from and no later than the composition's duration, or the call throws (KN-100). |
codec | 'avc1.640028' | An avc1 (H.264), vp09 (VP9) or av01 (AV1) codec string. |
bitrate | 8,000,000 | Bits per second. |
audio | true | Whether the file has the sound, when the composition has an audio clip. false writes the video alone. |
audioCodec | 'mp4a.40.2' | 'mp4a.40.2' (AAC-LC) or 'opus'. With no AAC encoder in the browser, the default falls back to Opus (KN-202). |
audioBitrate | 192,000 | The sound's bits per second. |
shutter | config({ shutter }), which is off by default | Motion blur: an angle in degrees, or { angle, samples }, and each frame is the average of that many seeks inside it (The shutter). 0, false or null turns it off. |
finish | config({ finish }), which is off by default | The finishing pass on every frame: grain, a vignette, bloom, chromatic aberration, a grade, a soften and dust, each a field of an object (The finish). 0, false or null turns it off. |
source | The composition root | The element to capture, which can be a canvas. |
width, height | The root's data-width and data-height, or a canvas's own | The pixel size. A DOM source's box is scaled to it, as a canvas source's frames are. |
name | The root's data-composition-id | The file name, without .mp4. |
download | true | false keeps the file in the result only. |
onProgress | None | Called as (frame, frames) after each frame. |
signal | None | An AbortSignal, as fetch() takes, that cancels the render (Canceling a render). |
The result is { blob, name, frames, width, height, fps, codec, audioCodec, ms }.
name includes .mp4, audioCodec is the sound's codec or null for a file with no sound, and ms is the wall-clock milliseconds the render took.
const { blob, name, audioCodec } = await wildflower.kinetic.render({
from: 2,
to: 6,
download: false,
onProgress: (frame, frames) => console.log(frame + ' / ' + frames)
});
// name is 'intro.mp4' when the root's data-composition-id is 'intro'
The encoder writes a keyframe every 2 seconds.
Each frame lasts a whole number of 90 kHz ticks, so the file and its sound last frames / fps to a tick at any rate, 59.94 and 23.976 included.
The clock returns to its earlier time when the render ends, and a clock that was playing plays on from there.
Canceling a render
signal takes an AbortSignal, as fetch() does.
Aborting it stops the render at the next frame, closes the video and audio encoders, and puts the page, the clock, the shutter and the finish back as a finished render does.
The promise then rejects with signal.reason, an AbortError DOMException unless abort() was given a reason of its own.
A signal that is already aborted rejects at once, before the page is touched, and anything other than an AbortSignal is KN-100.
const controller = new AbortController();
cancelButton.onclick = () => controller.abort();
try {
const { blob } = await wildflower.kinetic.render({ download: false, signal: controller.signal });
} catch (error) {
if (error.name !== 'AbortError') throw error;
}
While a render runs, the preview bar's Render button reads Cancel, and pressing it cancels the render the same way.
The shutter
shutter is the shutter angle in degrees, or an object with these fields.
| Field | Default | Meaning |
|---|---|---|
angle | 180 | How much of each frame the shutter is open, in degrees: 360 is the whole frame and 180 half of it. Above 0 and up to 360. |
samples | 8 | The seeks averaged for each frame, a whole number from 2 to 64. |
await wildflower.kinetic.render({ shutter: 180 });
await wildflower.kinetic.render({ shutter: { angle: 360, samples: 16 } });
// Every render, the preview bar's Render button included.
wildflower.kinetic.config({ shutter: 180 });
For frame i, at t = from + i / fps, the render seeks samples times, at t + (k / samples) · (angle / 360) / fps for k from 0 to samples - 1.
Each seek is settled and drawn as a frame without the shutter is, and the frame in the file is their average.
- The samples run forward from the frame's own time and stop short of the next frame, so a clip that starts or ends on a frame is cut cleanly, and two shots never blend at a cut.
- The samples are summed in whole numbers, so two renders of the same frame give the same pixels.
- Each sample's color is weighted by its alpha, so a part of the stage with no background blurs as over black, as the file shows it.
- A canvas source is drawn into a canvas of its own size for each sample and averaged the same way.
frame on the clock store and clipFrame in a scene keep the frame's own number through all its samples, so a decision made in frames is the same in every sample of the frame.
wildflower.kinetic.scene('flash', {
computed: {
lit() { return this.clipFrame >= 9; }
}
});
// lit is the same in every sample of frame 9
Footage and the rest of the page
Footage is seeked to each sample's time, 1 ms past it, as at any seek.
- At 180 degrees, and at 360 degrees with the default 8 samples up to 60 fps, footage at the composition's frame rate shows the same source frame in every sample and stays sharp, as a camera would record it.
- At 360 degrees with many samples the last ones reach the next source frame and leave a faint trace of it, from 34 samples at 30 fps and from 17 at 60 fps.
- Footage at a higher rate than the composition blends the source frames the shutter spans.
smear() returns one copy for the length of the render (Motion blur), and the clock store's shutter holds the angle until the render ends.
onProgress counts the frames of the file, and in development builds each sample's seek is measured against KN-170 as any seek is.
0, false and null turn the shutter off.
An angle that is not a number above 0 and up to 360, samples that are not a whole number from 2 to 64, a field other than angle and samples, or a shutter of any other type, an array included, throws before the page is touched (KN-100).
The finish
finish is an object of fields, each off unless it is given.
After the shutter's average, and before the frame is encoded, the frame is drawn through one WebGL 2 pass into a canvas of the pass's own, and the file is made from that.
A field given as a number is its amount; given as an object, its parts.
| Field | Parts and their defaults | What it does |
|---|---|---|
grain | amount, size 1, fps 0 | Grain over the frame, strongest in the midtones, size pixels across, a new pattern fps times a second, from 0 to 240. At 0 the grain stays still, a paper's tooth, and at the composition's frame rate it is film grain. |
vignette | amount, radius 0.5, softness 0.6 | Darkens toward the corners from radius of the half-diagonal, over softness more of it. |
bloom | amount, threshold 0.8, radius 24 | The color brighter than threshold, blurred over radius pixels and added back, times amount. On a light frame, keep the threshold high. |
aberration | amount | The red and blue channels sampled toward and away from the center, by 6 pixels at the corners for each 1, up to 8, growing with the square of the distance. |
grade | exposure 0, contrast 1, saturation 1, temperature 0, lift 0, gamma 1, gain 1 | A grade in that order: stops of exposure, contrast about mid-gray, saturation, warmth, then lift, gamma and gain. |
soften | amount | A blur that grows toward the edges, by 1.5 pixels at the edges for each 1, up to 16. |
dust | amount, size 1 | Sparse light specks, new on every frame, size times their usual width. |
await wildflower.kinetic.render({ finish: { grain: 0.3, vignette: 0.35 } });
await wildflower.kinetic.render({ finish: { bloom: { amount: 0.5, threshold: 0.8, radius: 40 }, grade: { lift: 0.05, saturation: 0.8 }, dust: 0.3 } });
// Every render, the preview bar's Render button included.
wildflower.kinetic.config({ finish: { grain: 0.2, vignette: 0.25 } });
See Which to use for the finish's grain beside the other grain routes, and for the soften beside the other ways to blur.
The pass is applied in the order the table lists, except that the aberration and the soften happen as the frame is sampled, before the bloom is added.
It reads the frame, the fields and the frame's number, never a time or a random number, so two renders give the same frames, the dust changes from frame to frame as film's does, and the grain changes at its fps.
config() reports the grain as [amount, size, fps].
A transparent pixel of the frame is sampled as the black the file shows.
check() compares seeks before any finishing, so the pass never enters its hashes.
The finish in the page
In the page, Kinetic draws the parts of the finish that CSS and a canvas can show, from the same config({ finish }).
The vignette is a gradient over the stage from the same radius and softness; the grain a noise tile blended with overlay and moved at its fps; the dust drawn on a canvas by the same hash as the file's; the grade's exposure, contrast and saturation, and the soften, a backdrop-filter.
The bloom, the aberration and the grade's temperature, lift, gamma and gain are the file's only.
Kinetic keeps these overlays as the root's last children, under the pointer, and hides them for a render.
An element in the root marked data-finish takes them in the root's place, so a lesson's controls beside it stay clear of the grade and the soften.
The element that holds them, when its position is static, is given position: relative while they are in it.
<div data-composition-id="lesson" data-duration="20">
<div class="picture" data-finish>…</div>
<div class="controls">…</div>
</div>
A finish given to one render() call finishes that file only; the page shows the config's.
0, false and null turn the finish off; a field at 0, or a grade of defaults, is off.
A field that is not one of the seven, a part that is not the field's, or a part outside its range throws (KN-100), naming the field and the part.
A browser with WebGL 2 turned off throws KN-100 naming it.
The pass opens a WebGL context of its own for the render. Chrome keeps 16 WebGL contexts on a page and drops the oldest when another is opened, so a page with 16 or more WebGL layers, counting effects, drawings and Trillium views, can lose one and render it blank. Keep a piece with a finish to 15 WebGL layers. Development builds warn (KN-192) when Kinetic's own effects and drawings hold 16.
What a render needs
The browser
- A canvas source renders in any Chrome.
- The composition's DOM is drawn with
drawElementImage, which Chrome has only with HTML-in-Canvas enabled until Chrome 157, scheduled for 3 November 2026, turns it on: pastechrome://flags/#canvas-draw-elementinto Chrome's address bar, set the HTML-in-Canvas entry to Enabled, and restart Chrome. Without it,render()throws (KN-201). For the render, Kinetic wraps the root in a<canvas layoutsubtree>and puts it back afterwards. - WebCodecs needs a secure context: a page served over https, from localhost, or opened from disk.
On plain http from another host
render()throws (KN-200). - A page opened from disk cannot fetch its own files, so an audio clip, a luma map, an animated image or a query beside it fails readiness, and an
<img>beside it is drawn as nothing, since Chrome treats each file as its own origin. KN-204 looks at http and https URLs only, so it does not warn there. Serve a page that has files beside it.
Codecs and sound
- A codec the browser cannot encode at the size throws (KN-100).
- An
audioCodecthe browser cannot encode throws (KN-100). So does a composition with an audio clip in a browser that encodes neither AAC nor Opus, unlessaudioisfalse. An audio clip whose file fails to load or decode rejects the render with KN-135, also withaudio: false. - An audio encoder whose delay Kinetic cannot measure throws (KN-203), since the sound could not be lined up with the picture: a measuring encoder or decoder that fails, a measuring impulse that does not come back, or an encoder that gives no codec description.
- A video encoder that reports an error, takes no frame from its queue for 10 seconds, or gives output the MP4 cannot hold throws (KN-207), with the codec in its message, once the page and the clock are back; the development build's message adds the encoder's own.
Readiness, footage and speed
- The render begins with a run of readiness before it touches the page, and rejects with its failure (Failure).
That covers footage that fails to load or decode or has no source (KN-138), a failed Lottie animation or animated image (KN-180, KN-181), a map (KN-117), and a bad input or failed
waitForin any scene (KN-130, KN-131, KN-133). - Each canvas in the composition is drawn into the file through a 2D copy laid over it (Canvases in a render).
- Footage with keyframes far apart renders correctly but more slowly, since each frame's seek decodes from the keyframe before it.
Re-encoding it with a keyframe every second fixes that:
ffmpeg -i in.mp4 -c:v libx264 -g 30 -crf 18 -c:a copy out.mp4. - A render with a shutter takes about
samplestimes as long as one without, since each sample is a seek settled and drawn as a frame is. - With the HyperFrames runtime on the page,
render()seeks through the runtime's player: the shutter is not applied (KN-205), and the render's frame rate has to divide the root'sdata-fpsorrender()throws (KN-206), as Rendering through HyperFrames describes. - One render runs at a time: a second call while one runs throws (KN-100), and the first goes on.
Media from another origin
drawElementImagedraws media from another origin black, with no error of its own, sorender()throws first (KN-204), and its message lists each element with its URL. Open shadow roots are looked through too.- An
<img>,<video>or SVG<image>from another origin is drawn once it hascrossorigin="anonymous"and its server sendsAccess-Control-Allow-Origin, and a<video>'sposterfollows the video'scrossorigin. - A CSS image from another origin (on the element, its
::beforeor::after, or incontent: url()) and an<input type="image">are drawn black even then, and a frame or an<object>from another origin is drawn empty. Serve each from the page's own origin or as adata:URL.
Canvases in a render
Each canvas in the composition (a Trillium view, an effect, lottie-web's canvas renderer, a canvas an animated image is drawn into) reaches the file through a 2D copy laid over it and updated after every seek.
That holds wherever it sits and whatever its drawing size, border, padding and object-fit.
The copies make up for what drawElementImage does with a canvas inside the render's canvas.
- It draws a canvas that was on screen before the render from an older image.
- It paints the drawing at its own size from its corner, whatever its CSS size, border or
object-fit. - It cuts off a canvas not laid out at the root's top left, and paints no background for it.
- A canvas that appears mid-render, in a newly mounted scene or as its
display: nonelifts, gets a copy too. - A copy shows what its canvas shows at the seek: its background color, transform, translate, rotate, scale, opacity, filter, clip-path and every property an animation on it moves (a keyframed background color or shadow too).
- A copy has no transition or animation of its own, so a transition on the canvas's class does not slide it, and a CSS animation the clock seeks moves it with the canvas.
visibilityis the one animated property not copied: the copy takes the canvas's inlinevisibilityonly, so a canvas a CSS animation shows or hides is drawn as its inline style says.- A drawing that goes to 0 × 0 shows the background alone, as the page does.
- Kinetic hides the canvas with an attribute and a style sheet of its own, so its style attribute is exactly as it was after the render, whether the render ended or threw.
- Page code that removes a copy, or sets
document.adoptedStyleSheetswithout Kinetic's sheet, during the render gets them back at the next frame. - A WebGL canvas needs
preserveDrawingBufferfor its pixels to be read, and development builds warn about a Trillium view without it (KN-190).