What works
What Kinetograph draws from a Remotion or HyperFrames project, and what it approximates or skips.
Kinetograph renders Remotion and HyperFrames projects without a browser. Your remotion and @remotion/* imports run on Kinetograph's own versions of those packages. To move a project, see Migrate from Remotion.
Each section lists what works, then the gaps. "Reported" means it shows in the render warnings and in doctor.
Layers blend in sRGB, like a browser. Set blendSpace: 'linear' for linear-light blending.
When the engine can't draw something
The engine never switches to a browser. It skips the item or draws something close, and warns.
render,stillandstripprint one line per kind of problem (element, property, reason, frames), at most 12 lines. Only frames where the element is on screen count.--strictstops the command at the first frame with anything unsupported: error codeUNSUPPORTED_CONTENT, exit code 1, no output file. Use it in CI and final renders.kinetograph doctor <entry>goes through every frame without the GPU and lists what can't be drawn, on which frames, and any scene a render would refuse. Run it before a long render. It takes a HyperFramesindex.htmltoo.--jsonputs the full list undercompat.leftOut.doctoris also an MCP tool.
npx kinetograph doctor src/index.ts
npx kinetograph render src/index.ts --strictText and fonts
- Fonts load from files,
new FontFace(...)withdocument.fonts.add(face), or@font-face. One file is kept per family, weight and style; aunicodeRangesplit keeps the basic Latin part. -webkit-text-stroke,paint-order: stroke fill,text-shadow, transforms, opacity andbackground-clip: textwork.- Kinetograph reads no system fonts, so a render looks the same everywhere. With no font loaded, text uses the bundled DM Sans. Variable fonts follow the requested weight.
- A missing character comes from Noto Sans Symbols 2, Noto Sans Math or Noto Emoji. Emoji are monochrome.
- CJK, Arabic, Devanagari and similar scripts have no bundled font. They are drawn as boxes and reported. Load a font that has them.
monospacefalls back to a proportional face, so code blocks (Shiki, Code Hike) look wider than in Chrome. Load a monospace font.exuses the first font's x-height. With only a generic family such asserif, a MathJax formula comes out about 10% larger.exin SVG shape attributes is half an em.
Video and audio
<Audio>and<Video>from@remotion/mediawork.toneFrequency(0.01 to 2) changes pitch, not length.@remotion/media-utilsworks.visualizeAudiofollows Remotion's documented behaviour, but its numbers are not identical.onVideoFrameon<Video>,<OffthreadVideo>and<Html5Video>hands you the frame as a canvas image source:ctx.drawImage(frame, ...)andgetImageData. The frame is not a WebCodecsVideoFrame: it haswidth,height,timestampandclose()only. A canvas you draw on starts each frame empty. A video with no named colour space converts differently from Chrome.<Video datamosh>is Kinetograph's own prop: see Datamosh.@remotion/media-parserand@remotion/webcodecswork in part: the@remotion/webcodecsencode and decode functions, and the box tree and sample callbacks of@remotion/media-parser, throwUnsupportedApiError. Normal<Video>and<Audio>are not affected.- On Linux the addon has no VAAPI or Quick Sync encoders. Use your system's with
KINETOGRAPH_FFMPEG=$(which ffmpeg)and--encoder h264_vaapi,hevc_vaapi,h264_qsvorhevc_qsv(yuv420p;yuv420p10leforhevc_*). They are never the default (libopenh264is).KINETOGRAPH_VAAPI_DEVICEpicks another/dev/dri/renderD*.--crf Nmaps to constant quality, which is a different picture quality. Untried on hardware.
CSS and SVG
import './index.css', local@imports, Tailwind (built with your project's setup) and*.module.csswork.staticFilereadspublic/next to the entry, else next to the nearestpackage.json.filter: blur(),drop-shadow()andbackdrop-filtertake any radius; past 532 px they are drawn 532 px wide, as in Chrome.backdrop-filter: blur()reads only the backdrop inside the element's border box. The same filter functions work inside<svg>, next tofilter: url(#id).transform-style: preserve-3dplanes that cross intersect, and are flattened where a browser flattens them (opacitybelow 1,filter,clip-path,mask-image,mix-blend-mode, non-visibleoverflow).- A
backdrop-filterin apreserve-3dcontext inside an element drawn as one group blurs only what its own 3D context drew, not the whole group as Chrome does. Reported as approximated. - Crossing
preserve-3dplanes are cut using each border box, so a shadow past the box doesn't count. The crossing line isn't anti-aliased, and a layer cut more than 16 times can show a faint seam. Abackdrop-filteron a turned element mirrors the backdrop at its bounding box. - A basic shape such as
inset()asclip-pathinside an<svg>is not drawn. - A rule that sets a property to its initial value loses to a presentation attribute:
.bar { fill: black }overfill="red"stays red. - HTML is laid out as a browser would. A group of HTML elements is a box the size of the composition, as Remotion's root
divis. A Kinetograph element inside HTML takes the size of its own box, so a layer with no box is 0 × 0. CSS@keyframesfollow the frame's time.
Canvas 2D, effects and shaders
- A
url()filter on Canvas 2D works when it names an SVG<filter>of the page. On a<Canvas>component it is reported. HtmlInCanvaseffects (2d,webgl2,webgpu),onPaintandonInitrun. State fromsetupdoesn't carry to the next frame, since each frame runssetup,applyandcleanupagain.getImageDataof the source and nested<HtmlInCanvas>are not supported. Anasynchook holds the frame until its promise settles;setTimeoutwaits don't.- WGSL
shader()sees only its own layer, runs one pass, has no other texture, and takesf32uniforms only.
WebGL
<canvas> with a webgl or webgl2 context runs on the GPU. p5.brush's standalone build draws as in Chrome.
- Works: fragment shaders, framebuffers,
gl_PointSize, instancing,blitFramebuffer, depth and stencil tests,drawBuffers, depth textures. - Every frame starts with an empty canvas, also with
preserveDrawingBuffer, because a frame is a function of its own number. - Not drawn: multisampled renderbuffers,
readPixels, lines, instanced points and avertexAttribDivisorabove 1, cube textures, float render targets (float textures fromtexImage2Dand 3D textures work),sampler2DShadow, non-RGBA render targets, mixed-size framebuffer attachments,generateMipmapon a drawn-into texture, and aTEXTURE_BASE_LEVELother than 0. - Differs from a browser: the depth buffer holds 32-bit floats,
depth: falsemakes the depth test always pass, a depth texture is sampledNEAREST, points are squares of at most 511 px, andgl_VertexIDwithdrawElementsis the position in the index list.
WebGPU
A canvas's getContext('webgpu') records its calls, and the passes it submits are drawn on the engine's own device.
- Works: buffers, WGSL modules, render and compute pipelines and passes, 2D textures in the colour formats that need no optional feature, storage buffers and textures, uploads, copies and multi-pass shaders.
copyExternalImageToTexturetakes images,ImageBitmap, canvases,<video>and effect layers, and writes the four 8-bit RGBA formats only. - Not drawn, reported every frame: query sets, render bundles, indirect draws, depth and stencil, multisampling, texture arrays, 1D and 3D textures, comparison samplers,
viewFormats,importExternalTexture(usecopyExternalImageToTexture), readback (mapAsync,copyTextureToBuffer), the canvas's own texture as a source, overridable constants, and depth, compressed and optional-feature formats. - An
rgba16floatordisplay-p3canvas is clipped to SDR sRGB, andtoneMapping: { mode: 'extended' }is drawn as standard (reported). - A refused WGSL module, or a frame over 4 GiB of textures, 4096 steps or 4096 dispatches, gives a warning and a transparent canvas.
- State from earlier frames (a simulation with
loadOp: 'load') is replayed, so it is right in a sequence. A frame drawn alone, or by another worker, can't have it; it is reported anddoctorlists it. To avoid that, clear or rewrite what a compute pass writes before using it, and bind only what a pass uses.atomicoperations that depend on invocation order are not deterministic on a GPU.
3D
- A
<Model>file that uses something the 3D layer can't read is reported on every frame that draws it, naming the feature (for exampleKHR_materials_clearcoat). A file the CLI can't read at all fails the render. See the scene format. - A second set of texture coordinates,
alphaMode: MASK(drawn opaque) and glTF extensions are not read, and are reported. - Shadow maps may hold at most 1 GiB, counted at the largest
mapSize(48 maps of 2048). A point light takes 6 maps. A scene over the limit is refused on load.
Remotion packages
- Works:
@remotion/transitions(all 19 presentations),light-leaks,starburst,sfx,zod-types,timeline-utils,canvas,motion-blur,layout-utilsandcaptions. @remotion/skiaand@shopify/react-native-skia: real Skia in an optional add-on, on the CPU. Drive Reanimated values fromuseCurrentFrame();withTiming,withSpringand other clock animations jump to their end value and are reported.useVideoshows frames but no sound. Not supported:makeImageFromView,Skia.SVG.MakeFromDataand touch. If Graphite can't start on a Vulkan device (Linux, Windows), drawing falls back to the CPU; the warning andrendererReasonindoctor --jsonsay why.KINETOGRAPH_SKIA_CPU=1chooses the CPU. See AUTHORING.md, section 7.7.@remotion/rive: Rive's own runtime, withstateMachineInputs,assetLoaderand referenced assets. An asset on Rive's CDN isn't fetched and is reported.getRenderer()returnsnull. See AUTHORING.md, section 7.6.
GSAP
- GSAP (core, free plugins and
Flip) works on the page's elements and SVG. Build a paused timeline in a layout effect and seek it toframe / fps, as Remotion documents. A tween that plays by itself, such asgsap.to(...)in auseEffect, also works, because Kinetograph moves GSAP's root clock with the frame, with one warning. - A jump to a later frame renders the skipped frames without drawing them, so tweens made in effects run. This costs one React render per skipped frame in a project that imports
gsap. Motion-blur sub-frames runonUpdateonly, notonStart,onComplete,onRepeatorcall(). ScrollTriggerandDraggabledon't apply to a render.
Libraries
How to write code for GSAP, Motion, anime.js, d3, Chart.js, Recharts and the rest is in Libraries.
requestAnimationFrameand Web Animations (element.animate()) run on the frame's clock, so a frame alone equals the frame in sequence. A pseudo-element animation is not drawn and warns once.- Motion clamps each step to 40 ms, so below 25 fps it runs a little slow. Motion components that mount mid-video start at the frame they mount on, but in a still or a later worker at their own first frame. Mount them at frame 0 or use
@kinetographdev/motion. - Mermaid 12 draws flowchart, class, state, ER, sequence and pie diagrams. Register your font before
mermaid.render. A<foreignObject>with a percentage size isn't drawn;mermaid.initialize({ htmlLabels: false })gives plain SVG text. - A library that reads the wall clock is not frame-pure.
@paper-design/shaders0.0.82: eight shaders match Chrome; others untested.
HyperFrames projects
render, still, doctor and studio take a HyperFrames project as it is. Pass its index.html or its directory. Nothing is rewritten. For the steps, see Migrate from HyperFrames.
- Composition and timelines: every frame, each
window.__timelinesentry is paused and seeked. The rootdata-composition-idelement gives the size (data-width,data-height) and length (data-duration, else the longest timeline or clip), at 30 fps.data-starttakes seconds orclip-id + 2; its references stay inside one composition. Withoutdata-duration, a video or audio lasts its file and an<img>lasts 3 s.data-hidden,data-composition-src,data-variable-valuesandgetVariables()work. - Media:
<video>is decoded by the engine, frame-accurate, followingdata-media-startanddata-playback-rate(kept to 0.1 to 10).<audio>and unmuted<video>are mixed withdata-volume,data-fade-inanddata-fade-out.data-automationtakesratelanes andvolumelanes (capped at +12 dB). - Audio effects:
data-fx-chainplays all HyperFrames audio effects on an element, an<hf-audio-group>or the master bus, with presets andfx.*lanes. An unreadable chain stops the mix with an error naming the element. - Colour grading:
data-color-gradingon<video>,<img>and<canvas>runs HyperFrames' whole grading shader as one GPU pass:adjust,wheels,curves,hueCurves,secondaries,.cubeluts,intensity,details, all 18effectsand thepalette. A missing.cubedrops thelutand says so. - Fonts: the 18 families HyperFrames embeds come in every weight Google serves, from the cache
fetch-depsfills. A render never downloads a font. System names map as in HyperFrames:Arialto Inter,Consolasto JetBrains Mono,Georgiato EB Garamond. A page that callsmeasureTextbefore its@font-faceloads gets fallback widths in Chrome; Kinetograph measures with the registered font, so it can pick a larger size. - Scripts:
<script type="importmap">is read (one with asrcis ignored, as in Chrome), and each module is bundled and runs after the classic scripts. Top-levelawaitworks, but the runtime doesn't wait for it. CSS and JSON import attributes are not supported. - WebGPU: pages that use it, such as the
shaders.iife.jsblocks (aurora,blob,ripples,swirl), are drawn natively. See WebGPU above. - Three.js is not drawn. A page that loads it is reported on every frame, and
--strictfails.
Downloads and the lock file
Nothing is downloaded while a frame is drawn. Before frame 0, commands that open a project download what the page loads from the web.
kinetograph render my-video # downloads first, then renders offline
kinetograph render my-video --offline # never downloads; an uncached file is an error
kinetograph fetch-deps my-video # the download alone; --update refreshes it- Allowed hosts (https):
cdn.jsdelivr.net,unpkg.com,cdnjs.cloudflare.com,esm.shandcdn.skypack.dev.--allow-hostadds one, and--urladds a file a scan missed. - This covers scripts, modules, data files a script names, remote
<video>and<audio>(500 MB each,--max-media-mb) and pictures (50 MB each). - A file that isn't cached stops the command, naming the URL and
fetch-deps. A picture is drawn without and reported. - A module or picture URL built at run time is found by running the first frames in the pre-pass. One seen only in an unsampled frame is downloaded on first use and noted; with
--offlinea module is an error naming the specifier.doctorgives thefetch-depscommand for a picture it can't find. kinetograph.lock.jsonbeside the entry stores each file's SHA-256 and size; commit it. A changed file stops the command untilfetch-deps --update.
Not planned
IFrameis not drawn, because a real webpage needs a browser (reported).Internals,FolderContext,PathInternalsandAudioForPrevieware internal to Remotion and not exported.@remotion/threeand@remotion/playerare replaced by the engine's 3D layer and studio.renderer,bundler,cliandlambdaare replaced by the Kinetograph CLI.tailwindandtailwind-v4only configure webpack, so do nothing. Tailwind itself works.
Can differ by a pixel
Rarely, when renderers share the GPU, two renders at the same concurrency differ by 1 in one colour channel on a few pixels of a frame. The cause is unknown. For a bit-exact render, use --concurrency 1 or KINETOGRAPH_SKIA_CPU=1 (slower, slightly different pixels).