kinetograph.

Changelog

What changed in each Kinetograph release.

Kinetograph is in beta, so APIs can still change between versions. The same list is in CHANGELOG.md at the root of the repo.

0.3.0 (2026-11-03)

Added

  • kinetograph init makes a project with src/, public/ and out/ and a starter scene; --html makes a GSAP page.
  • kinetograph add <name> installs components, templates and tastes from kinetograph.dev/r/; add --list lists them.
  • All 89 @remotion/effects effects draw, also on <Img>, <Video>, <Gif> and the @remotion/shapes components.
  • --preferred-effect-backend webgpu|webgl2|2d; doctor reports the installed @remotion/effects version.
  • Effects can read their picture's pixels (getImageData), so tile(), outline(), brightness() and contrast() match Remotion.
  • WebGL 2: vertex-shader textures, 3D textures and float textures.
  • --x264-preset (ultrafast to placebo, default veryfast).
  • Update notices: once a day the CLI says when the CLI or the agent skills are out of date. Off in CI and with KINETOGRAPH_NO_UPDATE_CHECK=1.
  • scaleX and scaleY on every layer (#93).
  • makeWebFont(cssUrl) loads fonts from any @font-face stylesheet.
  • Importing pictures (import tex from './tex.jpg') gives the file's path.
  • CSS filter functions on elements inside an <svg>.
  • parseVtt() in @remotion/captions.
  • --props takes a JSON file path.
  • Plugins: packages with a kinetograph field are found, listed by kinetograph plugins, and can add MCP tools and Studio panels.
  • CompositionInfo.propsSchema; Studio draws prop fields from the zod schema, with presets.
  • Studio: agent panels (brief, script, storyboard, styleframes, review, custom), markup mode, an Up next queue, image attachments, readable tool rows, sound and waveforms, range selection, a command menu, export of a range, and a plugin list from the site.
  • server.json and mcpName for the official MCP Registry.

Changed

  • Layers blend in sRGB by default, as Chrome does. blendSpace: "linear" keeps the old blending.
  • H.264 and H.265 use libx264 and libx265 (CRF 18) everywhere; VideoToolbox is opt-in with --encoder h264_videotoolbox.
  • Hardware encoders write a keyframe every 2 seconds.
  • libx264 is also found in the ffmpeg on your PATH.
  • still, strip, onion and render write under out/ when -o is not given.
  • npm install kinetograph installs Studio too.
  • Studio redesign: floating panels, a slim timeline, markdown replies.
  • DOM islands are no longer rebuilt every frame because of Tailwind's transition rules.
  • A picture drawn many times in one canvas is stored once (FoldOut: 204 to 41 ms a frame).

Fixed

  • A blur over mesh gradients, shaders or 3D no longer fades at the frame edges (#91).
  • Stroke types include cap, join, miterLimit, dashArray and dashOffset (#92).
  • A render whose media is all muted has no audio track.
  • WebGL and WebGPU canvases are antialiased (4x MSAA) as in a browser.
  • Variable fonts follow their weight range in the DOM and on canvas (#80).
  • Inline opacity, hard-stop gradients and <bdi>/bidi isolation (#72, #73, #81).
  • Windows drive paths and root-relative pictures load (#71).
  • Matches Remotion 4.0.539 and 4.0.540: spring() clamping, hue units, interpolate() wrap, translatePath(), multichannel audioBufferToDataUrl(), transform helpers, getAudioData() sample rate.
  • A clearer error for an entry without registerRoot.

0.2.5 (2026-10-28)

Added

  • HyperFrames audio effects: data-fx-chain is now played on an <audio> or audible <video>, on an <hf-audio-group> and on the root composition (the master bus), with all sixteen effects, the presets' blends and the fx.* and fx.preset.* lanes of data-automation. Order follows HyperFrames' mixer: data-volume ahead of the chain, the volume lane and fades after it, effect tails up to 5 s, group buses, the master chain over the mix. HyperFrames' effect graph and AudioWorklet processors (Apache-2.0) are ported to @kinetographdev/audio and run on node-web-audio-api, with no browser. Against HyperFrames' effects runtime in Chrome, 38 cases match in level to 0.000 dB, and a mixed page rendered by both CLIs is within 0.01 dB. Before, the chain was ignored. See /docs/compat/#hyperframes-projects.
  • HyperFrames pages: html and body are the document's own elements. Their rules for html, body and :root apply with their own specificity, rem follows the html font size, and scripts see document.documentElement and document.body (getComputedStyle(document.body), document.documentElement.style, document.body.appendChild, querySelector("body"), :root). Before, they were two wrapper elements and the page's selectors were rewritten to match. A test page that styles both went from 16.3 to 0.06 mean levels off HyperFrames' still.
  • Intel and AMD GPU encoders on Linux: --encoder h264_vaapi, hevc_vaapi, h264_qsv and hevc_qsv run in a system ffmpeg (KINETOGRAPH_FFMPEG=$(which ffmpeg)). They get the device and upload filter they need, --crf mapped to VAAPI's -qp and Quick Sync's -global_quality, yuv420p (yuv420p10le for HEVC), and a test encode before they are accepted, as for NVENC. KINETOGRAPH_VAAPI_DEVICE picks the render node. Never the default. Checked by unit tests and a stand-in ffmpeg, not on real hardware yet.
  • kinetograph doctor (and --json, as rendererReason) and the one-time CPU-fallback warning say why Skia's Graphite could not use a Vulkan GPU: the device and driver, and the Vulkan version, function or image-format usage that is missing. The addon method is Renderer.backendReason().
  • kinetograph track <video> --point x,y [--point ...] follows points through a video with libmv, as Blender 4.5's "Track Markers" does (its six motion models, pattern and search sizes, keyframe or previous-frame matching, backwards tracking, the margin check and where a track is lost), and writes <video>.track.json. useTrack(file, trackName?) in @kinetographdev/core returns { x, y, scale, rotation, corners } for the current frame. Tracking runs before a render, never during one. Against Blender on the same clips, the same frames are tracked and lost and markers agree to within 0.0002 px. The addon now includes libmv (MIT), Ceres Solver (BSD-3-Clause, with Apache-2.0 parts from Abseil), miniglog (BSD-3-Clause) and Eigen (MPL-2.0); their licences are in the platform packages.
  • kinetograph track --detect and --plane, as Blender's Detect Features and Create Plane Track. --detect places points with libmv's Harris detector (defaults: threshold 0.5, minimum distance 120 px, margin 16 px; --detect-inside/--detect-outside polygons for annotation placement) and tracks them. --plane p1,p2,p3,p4 (or all) makes a plane track from four or more point tracks, with corners from the patterns' bounding box or --plane-corners, carried through the clip by libmv's homography. The track file gains planes. usePlaneTrack(file, planeName?) in @kinetographdev/core gives { corners, pin } for the current frame, and cornerPin(pin) pins a layer to the plane. Against Blender on the same clips, detected features are the same and in the same order, and plane corners are bit-equal on every frame. The motion-tracking guide shows how to steady a shot with useTrack.
  • onVideoFrame on <Video>, <OffthreadVideo> and <Html5Video>, as Remotion has had it since 4.0.190. Your code gets the frame the video shows at each composition frame, as a canvas image source decoded by the engine's own decoder (a pure function of the frame number, whichever frames were rendered before). ctx.drawImage(frame, ...) (all three forms), createPattern, WebGL's texImage2D and WebGPU's copyExternalImageToTexture accept it, and getImageData of the canvas returns its pixels, alpha included.
  • Canvas 2D drawImage of a <video> element draws the frame at its currentTime (HyperFrames pages set it from the timeline). A canvas layer takes the source { "kind": "video" } (SCENE.md, "canvas").
  • getImageData and texImage2D of a canvas that holds one plain drawImage of a picture file or a video frame are answered on the CPU from the decoder's cache (addon readCanvasPicture), with no GPU: 0.7 ms against 3.3 ms for a held 1080p frame, 39 ms against 101 ms for the first call. Any other canvas is drawn by the renderer as before.
  • Web Animations API on the frame clock: element.animate(), KeyframeEffect, Animation (currentTime, startTime, playbackRate, play, pause, finish, cancel, reverse, updatePlaybackRate, commitStyles, ready, finished, onfinish, oncancel), getAnimations() and document.timeline, with cubic-bezier(), steps() and linear() easing and the add and accumulate composite modes. Values mix as Chrome mixes them (legacy colours in sRGB with premultiplied alpha, others in Oklab; transform lists by function or by matrix), and 27 cases match Chrome for Testing 154. Motion now runs opacity, transforms, filter, clipPath and colours on it instead of its JavaScript fallback, and its values match Chrome's to 0.0005, springs and oklch() colours included. A frame alone still equals the frame in sequence and at any concurrency.
  • render, still and doctor warn, once per render, when a GSAP tween plays on GSAP's root clock instead of a paused timeline seeked to the frame. Kinetograph renders it the same alone and in sequence, but Remotion's and HyperFrames' own renderers do not.
  • 3D glTF occlusion and emissive textures are drawn as glTF 2.0 specifies: occlusion at its strength darkens the ambient light and the environment (not directional lights), and the emissive texture multiplies the emissive factor. Before, they were reported and left out.
  • 3D bevel on text and extrude objects (<Text3D bevel>, <Extrude bevel>): { thickness, size, offset, segments }, as three.js's ExtrudeGeometry bevel options.
  • 3D morph targets and skins are deformed on the GPU, in a compute pass each frame, instead of on the CPU. A 204,800-vertex skinned, morphing model draws a 1080p frame in 3 to 5 ms on the M4 against 10 to 12 ms (four of them: 9 to 15 ms against about 30). A part with more data than one storage buffer holds (a big morph rig) is still deformed on the CPU. A vertex whose skin weights are all 0 follows its first joint, as in three.js, instead of collapsing to the origin.
  • 3D shadows: every light may cast them. The maps are layers of one depth texture array (up to 48, a point light taking 6) within 1 GiB, and the old limits of 4 shadow lights and 8 maps are gone.
  • WebGPU rgba16float canvases are drawn at half-float precision into the compositor, and a display-p3 colorSpace is converted to sRGB primaries. Before, both were reported and drawn as 8-bit sRGB. Output stays SDR, so values outside 0 to 1 are clipped; toneMapping: 'extended' is drawn as standard and reported.
  • WebGPU frame texture budget is 4 GiB (three of the biggest textures the device's limits allow), up from 512 MiB, which refused a single rgba32float 8192 x 8192 texture with its levels. Chrome on the M4 made 4 GiB of textures without an error.
  • WebGL framebuffers do what Three.js's renderer needs, drawn natively and checked against Chrome for Testing: the stencil test (stencilFunc, stencilOp, stencilMask and their Separate forms, clearStencil, the canvas's stencil attribute, stencil renderbuffers); several colour outputs (COLOR_ATTACHMENT1 to 7, drawBuffers, clearBuffer*, layout(location = n) outputs); depth and depth-and-stencil textures as attachments, sampled as Chrome samples them; colour, depth and stencil renderbuffers, with one depth buffer shared by the framebuffers that attach it; a texture level above 0 as a target, with TEXTURE_MAX_LEVEL; blitFramebuffer of depth and stencil; and texSubImage2D from an image or a layer, into a texture drawn in or one made from a file.
  • Canvas 2D ctx.filter = 'url(#id)' draws the SVG <filter> of the page it names, in its place in the filter list, as Chrome does.
  • studio downloads what an edit newly needs without a restart. When any project file changes (the page, the compositions it mounts, the local modules it imports), the step before frame 0 runs again after a short wait for a save that touches several files, and fetches only what is not cached. A dependency added in a sub-composition or local module is there on the next rebuild.
  • HyperFrames modules: import() of a specifier built at run time (import(`./locale/${lang}.js`), a bare name held in a variable) works. The pre-download pass runs the page's first frames (frame 0, the last and up to six between) with a hook on import(), resolves each requested specifier through the import map, downloads and locks it, and the render bundles it from the cache. A specifier first seen mid-render is fetched once, locked and noted; with --offline it is an error that names the specifier.
  • HyperFrames modules: a module whose download was redirected resolves its relative imports and import.meta.url against the final response URL, as a browser does. The cache keeps the final URL beside the file and the lock records it as final. A redirect to a host that is not allowed is still refused.
  • A plain HTML page whose root element lacks data-composition-id, data-width, data-height or data-duration gets a warning from doctor, render and still that names what is missing and shows the one-line fix. --strict fails on it, as on other unsupported content.
  • doctor, render and still report the errors a page's own scripts raise and survive: console.error calls, uncaught exceptions and unhandled promise rejections, once per message per frame, with the frames they were seen on. A library that catches its own failure (Mermaid) no longer passes as clean. --strict fails on them.
  • Constructable CSSStyleSheet (insertRule, replaceSync, cssRules, rule.style), for scripts that build CSS text.
  • DOM for sanitizers: DOMParser (text/html and the XML types, including image/svg+xml), document.implementation.createHTMLDocument and createDocument, document.createNodeIterator and createTreeWalker (the iterator moves on when the node it stands on is removed, as sanitizers rely on), NodeFilter, DocumentFragment (also document.createDocumentFragment), <template> with .content, Element.attributes (NamedNodeMap, Attr, getAttributeNode, removeAttributeNode), Node with nodeType, nodeName and ownerDocument on text nodes too, HTMLTemplateElement and HTMLFormElement. DOMPurify finds its document through them. Mermaid 12 renders flowcharts, sequence diagrams, class diagrams and pie charts when its labels are SVG text (mermaid.initialize({ htmlLabels: false })); see /docs/known-issues/ for HTML labels.
  • WebGL depth test is drawn: enable(gl.DEPTH_TEST), depthFunc, depthMask, depthRange, clearDepth, clear(DEPTH_BUFFER_BIT) and polygonOffset, on the canvas (the depth context attribute) and on a framebuffer with a depth renderbuffer (renderbufferStorage, framebufferRenderbuffer). The HyperFrames composition code-slice-hero now renders all 240 frames in doctor. A colour or stencil renderbuffer, a depth texture and the stencil test were still reported at this point. See /docs/known-issues/#webgl.
  • The GPU device allows up to 16 vertex buffers (was 8), so a glsl draw with more than 8 attributes draws. WebGPU pages still see maxVertexBuffers 8 on every machine.
  • Script writes to an element's classes and data attributes reach the page: svgElement.className.baseVal = 'on', classList.replace(), classList.value and element.dataset.key = 'x' (and delete element.dataset.key) were lost, so selectors that read them never matched. Tests now also cover a class added, removed or toggled on an SVG shape between frames, setAttribute, the style attribute, a sibling's class, :nth-child after a node is inserted, and a rule inside @media; a class or attribute added to an SVG shape between frames already restyled it.
  • HyperFrames effects in data-color-grading are drawn: blur, bloom, kuwahara, chromaBleed, pixelate, halftone, twoInkPrint, ascii, dither, monoScreen, scanlines, chromaticAberration, crtCurvature, digitalGlitch, tapeDamage, filmArtifacts, engraving and crosshatch, with all 64 controls, the palette, the presets' own effect values and the tweened --hf-color-grading-blur|bloom|kuwahara|pixelate|ascii|dither. They are no longer reported as not drawn. See /docs/compat/#hyperframes-projects.
  • colorGrade is a scene effect of its own, with the helper colorGrade({ effects: { halftone: 0.9 } }) in @kinetographdev/scene, so your components can use the grade and the look effects. The catalog lists 23 effects now.

Changed

  • Blur radius limit: blur, backdropBlur, glow and a shadow's blur clamp at 16384, up from 512 (blur) and 128 (the others). Each image of a blur's pyramid holds only what the blur draws in it, so a 1000 px blur needs about the memory of a 128 px one (22 MB on 1080p, down from 579 MB). No blur texture is larger than the device allows (8192 px a side): a mirrored backdrop blur whose canvas (the box plus three radii each way) would be larger is drawn shrunk and enlarged after. The canvas margin still makes room for a radius of at most 128.
  • Scene backdropBlur has edgeMode: canvas (default) or mirror, which reads only the layer's box, mirrored.
  • Scene gradient paint has tile (paint it in one tile and repeat it, with gaps), and image paint has spaceX and spaceY.

Fixed

  • Motion-blur sub-frames sample GSAP tweens that play by themselves at the sub-frame's time, with their onUpdate. Before, every sub-frame showed the frame's own state, so those tweens were not blurred. A blurred render's frames equal an unblurred render's, and a frame alone equals the frame in sequence.
  • SVG getBoundingClientRect matches Chrome for Testing: the <svg>'s preserveAspectRatio (none, slice, the alignments), a CSS transform with its transform-origin, and nested <svg> viewports are now included. Strokes, markers and filters stay out, as in Chrome.
  • A canvas pattern of another canvas covers any number of tiles (it stopped at 10,000), and a pattern of a canvas pass's child is drawn (it was left out).
  • Canvas ctx.filter = 'drop-shadow(...)' uses its blur length as the standard deviation, as Chrome does; it was halved.
  • Canvas isPointInStroke takes the line width, caps, joins, miter limit and dashes into account, in the current transform, as Chrome does.
  • WebGL: explicit layout(location = n) on a shader's inputs and outputs is honoured; the translator dropped it.
  • WebGL: with the default antialias, blitFramebuffer into a canvas, or out of it into a rectangle of another size, sets INVALID_OPERATION, as Chrome's multisampled canvas does.
  • GPU-compositor layers (a preserve-3d context, an effect only the compositor draws) that blur their backdrop see what Skia drew before them, as backdrop-filter sees everything painted before the element. A frosted panel in a preserve-3d group over a box outside the group was not frosted at all. In a scene that blends in sRGB (every DOM and Remotion scene) the compositor now blurs a backdrop on sRGB-encoded values, as Skia and Chrome do; it blurred in linear light, which made a frost over a colour edge lighter. Against Chrome for Testing the repro went from 1.63 to 0.02 mean levels (max 98 to 1), and a frosted panel with the box inside the group from 0.83 to 0.02. A frame without a backdrop blur draws as before; one with such a layer costs about 0.8 ms more on the GPU at 1080p.
  • transform-style: preserve-3d planes that cross each other intersect, as in Chrome. The layers of a 3D context are ordered by a BSP tree of their planes, and a plane another crosses is cut where they meet, each piece in its own place in the order (two cards pushed through each other, a card flip, a cube; 0.65 mean levels from Chrome for Testing). Parallel planes are ordered by their planes rather than their centres, so a tilted stack of cards no longer puts a farther card in front.
  • A display: inline-block element that is also a stacking context (a transform, opacity, position: relative, ...) is drawn once. It came out as two identical layers ($0.0.1 and $0.0.1~2), which doubled its anti-aliased edges and translucent fills.
  • background-repeat: space and round work with any number of tiles. Past 150 tiles of a gradient or a picture, the tile is drawn once and the paint repeats it at its exact period, gaps included, as Chrome's pattern shader does. Before, space with thousands of gradient tiles warned and drew nothing, and round drifted by up to a tile.
  • backdrop-filter: blur() reads only the backdrop inside the element's border box, mirrored past its edges, as Chrome does; before, it blurred in what lay around the box.
  • CSS filter: drop-shadow() uses its blur length as the standard deviation, as Chrome does (it used half, as for box-shadow). blur(), drop-shadow() and backdrop-filter past 532 px are drawn 532 px wide, Chrome's own limit.
  • @remotion/skia and @shopify/react-native-skia draw what they used to leave out, each frame-pure: Reanimated shared values (react-native-reanimated maps to the core: useSharedValue, useDerivedValue, interpolate, Easing; a value lives for one frame, and withTiming and the other clock animations jump to their end and are reported) with RN-Skia's useClock, usePathInterpolation and the other value hooks; FitBox, fitbox and rect2rect; Skia.Surface.Make/MakeOffscreen with makeImageSnapshot, drawAsImage and drawAsPicture (rasterised on the CPU, so the same on every machine); Skia.Data, Skia.Image.MakeImageFromEncoded/MakeImage, SkImage.readPixels and encodeToBytes; Skia.Typeface.MakeFreeTypeFaceFromData, Skia.FontMgr.System() (the registered fonts) and Skia.TypefaceFontProvider; useAnimatedImageValue and Skia.AnimatedImage (the GIF or WebP frame for the frame's time, looping); paragraph measurement after layout(width) (getHeight, getLineMetrics, getRectsForRange, ...); shaders, colour filters, image filters, mask filters and path effects set on a Skia.Paint() (Skia.Shader.*, effect.makeShader, ...); and useVideo, whose currentFrame is the picture the file shows at the frame's time (decoded on the CPU like an onVideoFrame frame; its sound is not played).
  • Text inside an ImageSVG is drawn with the registered fonts: the first family of each font-family list that is registered, else the built-in fallback font. A named family that is not registered is reported.
  • @remotion/rive: stateMachineInputs sets a state machine's inputs (an object, or a function of the frame; each change is met at its frame's time as the machine is replayed, so a frame stays a pure function of the props). Referenced pictures and fonts are read from beside the .riv or given by assetLoader (a CDN asset is reported). onLoad gets the file's artboards, animations and state machines, effects and effectsOutputSize run over the canvas, and the ref's getCanvas, getArtboard and getAnimationInstance answer (getRenderer stays null).
  • Studio chat survives a reload: the server keeps the conversation and replays it to the window that comes back within 30 s, and the agent keeps running meanwhile.
  • Studio timeline clips show a thumbnail of their first frame. Thumbnails are drawn one at a time, only while no preview frame is being asked for, and kept. On examples/compat, playback took 10.2 to 10.8 ms a frame with 40 thumbnails waiting and 10.8 to 11.4 ms without; each thumbnail took about 5 ms in the idle time after.
  • Studio numbers siblings that share a name ("Card", "Card 2", "Card 3") in the timeline, the scene tree, the preview and the chat's context.
  • @remotion/skia stays on Skia's CPU rasteriser after measuring the main renderer's GPU path. No prebuilt Skia for skia-safe 0.153.3 has the GPU backend (Graphite on Metal) together with the text-layout and SVG modules this addon uses, so the GPU path would mean building Skia from source on every platform, and GPU pixels differ from the CPU's and between machines. A busy 1920 x 1080 canvas (gradient, 40 circles, a blurred rounded rect, text, a blurred circle) costs 9.9 ms a frame to draw, read back and hash, plus 8.0 ms to encode and write its PNG (0.39 MB) when the picture is new.
  • Studio's agent no longer reports a turn as done after the conversation was stopped.
  • img.src = url set from a script draws. src had a getter and no setter, so nothing drew and doctor called the page clean. Now src, srcset, sizes, alt, poster, href and currentSrc reflect their attribute as in a browser. An SVG element's href is an SVGAnimatedString (image.href.baseVal = url). src, href, poster and currentSrc read back resolved against the document's URL, so new URL(link.href) parses. An <img srcset sizes> is parsed as HTML parses it and drawn with the candidate Chrome takes at device pixel ratio 1. In a HyperFrames page, a remote picture set at run time is read from the pre-download cache: fetch-deps finds its URL in the page's scripts, or runs the page's first frames when a script builds it, and a render that misses it reports it with the fetch-deps command.
  • window.visualViewport exists (the composition's size at scale 1, an EventTarget). @paper-design/shaders 0.0.82 threw "visualViewport is not defined" at new ShaderMount; eight of its shaders now match Chrome for Testing to the pixel.
  • A scene-file key the format does not read (bogusTop, a misspelt opacty, radius on a rect) is warned about by render, still, lint and doctor with its JSON path, on every frame, and --strict fails on it. Before, it was ignored without a word.
  • KaTeX draws its glyphs. The fonts of a page's own <style> @font-face rules are in the island's font list on the frame the <style> first appears, so its text is no longer left out ("font not one of the project's font files") until the next frame. A width: min-content box with white-space: nowrap that starts with an inline-block now counts the text after it, so KaTeX's pieces no longer overlap, and a KaTeX formula matches Chrome's layout.
  • doctor no longer calls a page with an SVG <image> "refused"; it looked for the data: picture the painter writes for it as a file.
  • HyperFrames colour grading: crtCurvature on an element bigger than the canvas pulls in the part off the canvas, as Chrome does; it drew black there (a 1600 x 900 image on a 1280 x 720 page: 27.2 to 0.31 mean levels off HyperFrames' still). The colorGrade effect counts the warp's reach in the scene margin.
  • HyperFrames colour grading: bloom runs at half size, as HyperFrames' does, about twice as fast (composite 2.75 to 1.29 ms on a 1280 x 720 page) and closer to HyperFrames (0.39 to 0.04 mean levels; 0.91 to 0.11 with a wide radius).
  • An import map with a src is noted as ignored, because Chrome ignores it too (browsers do not load external import maps). The known issue that called it a gap is corrected.
  • The error for an encoder no ffmpeg has (this ffmpeg has no encoder called "h264_vaapi") names the fix: set KINETOGRAPH_FFMPEG to an ffmpeg program that has it. KINETOGRAPH_FFMPEG no longer hides the bundled libx264 when the program it names lacks it; each encoder is looked up in every program in turn.
  • An <svg> with a <foreignObject> holding a block element (<div>, <p>) is drawn; before, the whole <svg> went missing with nothing for doctor to say. The HTML in a <foreignObject> is laid out as a block box of the object's width and height (the attributes, or CSS), painted by the HTML painter (text, backgrounds, borders, flex, nested SVG), cut to the box by its overflow, and carried through the SVG's viewBox, transforms, opacity and clip. doctor reports per frame what cannot be drawn (a percentage size, HTML in a clip path), and the shapes beside it are drawn. As a result, Mermaid 12's default HTML labels draw in flowchart, class, state and ER diagrams.
  • An inline element that holds a block (<span><p>text</p></span>) paints its text, in HTML and in a <foreignObject>; the painter left it out.
  • display: table-cell on an element that is no table part, and display: table on one with no table parts, are a block as wide as its content (or the width given), as the anonymous table boxes of CSS 2.2 section 17.2.1 make them. Before, they filled the line or held nothing. Libraries centre labels with them.
  • Type selectors for camel-case SVG names (foreignObject, clipPath) match; they never did, so the UA rule that clips a foreignObject did not apply.
  • monospace and ui-monospace text uses a bundled JetBrains Mono (Regular, SIL OFL 1.1, 92 KB) when the project registers no fixed-pitch font, so code blocks (Shiki, Code Hike) line up instead of falling back to a proportional face. A registered monospace font still wins.
  • SVG <linearGradient> and <radialGradient> blend their stops without premultiplying alpha, as Chrome does. A stop going from opaque red to rgba(0,0,255,0) passes through purple and fades out, where it used to fade to clear red. CSS gradients still blend premultiplied.
  • A React-path <canvas> that a frame draws on starts that frame empty, as the docs say. A canvas nobody draws on in a frame keeps its pixels, so one drawn once in useEffect(..., []) still shows. Before, it kept the previous frame's pixels, so still 45 (or a render worker that starts mid-way) drew a different picture from frame 45 of a full render. A plain HTML page still keeps its canvas between frames.
  • Mermaid's calculateTextDimensions no longer throws svg element not in render tree. SVG text is measured with the built-in fallback font when its family is not registered, and getBoundingClientRect of an SVG shape in an <svg> with no layout box answers in the SVG's own units.
  • outerHTML and innerHTML write SVG attributes such as viewBox and markerUnits in their proper case below the <svg> element; they wrote lower case. A style given as text was written as nothing, and an element inside <foreignObject> reported the SVG namespace (and <br> parsed as an unclosed element).
  • Nodes made by innerHTML or insertAdjacentHTML below the first level have a parentNode; it was null.
  • document.querySelector('body'), querySelectorAll('html') and getElementsByTagName('body') find html, head and body; they returned nothing, which broke d3.select('body').
  • lut effect: interpolation: 'trilinear' | 'tetrahedral' (default 'trilinear'). Tetrahedral blends four table points chosen from the six-tetrahedra split of the cell, as DaVinci Resolve, OpenColorIO and ffmpeg's lut3d=interp=tetrahedral do.
  • render, still, doctor, studio and the other commands that open a project download what it loads from the web before frame 0, then draw offline. HyperFrames pages: CDN scripts, data, media, pictures and fonts, as fetch-deps did. Remotion projects: the https pictures, videos and audio that <Img>, <Video> and CSS url() name in the bundle. --offline never downloads. fetch-deps stays as the explicit command, works on Remotion entries, and gains --update.
  • kinetograph.lock.json beside the entry pins every downloaded file (URL to SHA-256 and size), written on first download. Later runs check the cache against it and stop with the URL on a mismatch; fetch-deps --update accepts new bytes. A Remotion URL first requested in the middle of a render is downloaded once, added to the lock and noted. Cache writes are atomic (hash first, file last), so parallel workers on a first render never read half a file.
  • HyperFrames: <script type="importmap"> (imports, scopes, trailing-slash prefixes) and <script type="module"> that import now work. The graph (bare names through the map, URLs, relative paths, export * from, esm.sh /v135/... paths, dynamic import("...")) is downloaded in the pre-pass and locked, and each module script is bundled by esbuild from the cache into a classic script that runs after the classic scripts, in document order. import.meta.url is the module's URL. cdn.skypack.dev joins the default hosts.
  • CSS Motion Path moves and turns the element: offset-path (path(), circle(), ellipse(), inset(), polygon() and ray()), offset-distance (lengths and percentages of the path; a closed path wraps, an open one stops at its end), offset-rotate (auto, reverse, angles), offset-anchor and offset-position. GSAP tweens of offsetDistance animate. The offset transform sits between translate/rotate/scale and transform. Not followed, and reported: url() paths, shape() and a lone <coord-box>. Rounded inset() corners and ray(... contain) are approximated.
  • white-space: nowrap keeps inline-blocks on one line when no text comes before them; a row of inline-block tiles in an absolutely positioned box without a width used to wrap. Parley took the wrap mode from the last text only; it is now vendored (vendor/parley) and starts from the container's.

0.2.0 (2026-10-27)

Added

  • kinetograph render and still print which renderer draws (renderer: Skia GPU (Metal), Skia CPU (no GPU found) or Skia CPU (KINETOGRAPH_SKIA_CPU=1)), and doctor with no entry lists it too. The addon's Renderer has a backend() method for it.
  • libx264 and libx265 ship in the npm packages. The platform packages (@kinetographdev/native-<platform>) carry a separate GPL ffmpeg program in bin/ (x264, x265 and libvpx; the addon itself still links only an LGPL ffmpeg), which the addon runs as a child process for the encoders it lacks. H.264 defaults to libx264 where there is no hardware encoder, ahead of libopenh264. KINETOGRAPH_FFMPEG points at another ffmpeg. The addon's own ffmpeg also gained libvpx (VP8, VP9) on macOS and Linux. See /docs/ffmpeg-license/.
  • <Video datamosh> smears one shot into the next using the video's own motion vectors. See /docs/guides/datamosh/. (#5)
  • GSAP support, including the free plugins (SplitText, MotionPath, DrawSVG, TextPlugin, Flip): build a paused timeline and seek it to each frame. See /docs/compat/#gsap. (#7)
  • HTML text outlines: -webkit-text-stroke and paint-order: stroke fill, drawn with Chrome's miter joins and working with text-shadow, transforms, opacity and background-clip: text. (#8)
  • WebGL points (gl_PointSize, up to 511 px), instancing (divisor 1), blitFramebuffer, texSubImage2D into framebuffer textures and gl_VertexID. p5.brush's standalone build draws as it does in Chrome, and all 14 HyperFrames WebGL shader transitions draw. (#9)
  • Studio has a "Copy for agent" button that copies a change request for a coding agent, and its timeline shows a loading state instead of staying empty. (#6)
  • Studio layer names are readable (component, text, and the file and line each came from). The timeline has one row per Sequence, folds repeated components and has a "Show details" toggle. A new Agent tab chats with your coding agent over ACP about a layer or area selected in the preview. The studio now needs the per-launch ?token= link it prints, and the agent is off on non-loopback hosts unless --allow-remote-agent is passed.
  • HyperFrames projects render as they are with kinetograph render|still|doctor|studio <index.html|dir>: nested compositions, GSAP timelines seeked as HyperFrames seeks them, data-hidden, data-color-grading, speed ramps, volume lanes and HyperFrames' 18 embedded fonts. kinetograph fetch-deps caches the CDN scripts, data and media a page uses, so a render never touches the network. See /docs/compat/#hyperframes-projects. (#13)
  • kinetograph fetch-deps downloads the remote pictures of a HyperFrames page (<img>, srcset, <video poster>, SVG <image>, CSS url()) into the cache, under the same rules as scripts and media: https only, the default CDNs plus --allow-host, no redirect to another host, a hash checked on every read, 50 MB each. A render reads them from the cache, and a picture that is not cached is reported with the exact fetch-deps command. See /docs/compat/#hyperframes-projects.
  • GSAP tweens that play by themselves (gsap.to in a useEffect, with no paused timeline) follow the frame: GSAP's root clock tracks it, a frame is the same alone and in sequence, and a jump to a later frame renders the frames it skips. Flip works on a paused timeline. See /docs/compat/#gsap.
  • async effects and <HtmlInCanvas> programs: an async createEffect apply, onInit or onPaint holds the frame until its promise settles, and a rejection is reported for that frame.
  • WebGPU on a canvas: buffers, WGSL shaders, render pipelines and draws, replayed on the engine's own GPU device, so HyperFrames' shader backgrounds (aurora, blob, ripples, swirl) draw. Textures, samplers and compute were reported, not drawn, at this point. (#15)
  • WebGPU compute: compute pipelines (explicit layouts and layout: 'auto'), compute passes and dispatchWorkgroups, storage buffers and storage textures (rgba32float, rgba16float, rgba8unorm, r32float and the rest of WebGPU's table), 32-bit float textures sampled as unfilterable-float, and clearBuffer, replayed in submit order with the render passes. device.limits and adapter.limits are the engine device's own. Every dispatch, binding and format is checked on the page side and again before the frame is drawn, and the work a frame holds is capped. Steps that made state a page carries from frame to frame are replayed from their history, and a frame that reads state an earlier frame made is reported. The 13 shaders.iife.js blocks that use textures and compute (chrome-orb, frost, goo, heatmap, hologram, holographic, light-edge, liquid-metal, nebula, obsidian, plastic, thin-film, water) draw with nothing reported.
  • Packages @kinetographdev/gsap, @kinetographdev/p5-brush, @kinetographdev/anime and @kinetographdev/motion adapt libraries that run their own clock or randomness. Each is a thin wrapper that seeks the library to the frame; the libraries are peer dependencies, unchanged. See /docs/libraries/.
  • Recharts, d3-selection, anime.js, Motion's animate(), Paper.js and Bklit UI render, and Rapier draws its first frame. The DOM gained NodeList/HTMLCollection, compareDocumentPosition, document.defaultView, customElements and the *AttributeNS methods. (#12)

Fixed

  • Canvas 2D gradients blend their stops without premultiplying alpha, as Chrome does: a gradient from transparent green to opaque red keeps its green in the middle. CSS gradients still blend premultiplied. HyperFrames' transitions-destruction now draws as in Chrome.
  • kinetograph doctor counted frames twice in its summary ("nothing unsupported in 0 of 10 frames; with unsupported content: 10"). It now says "10 of 10 frames (100.0%) have unsupported content; 0 are clean", or "all N frames are clean".
  • SVG files draw as pictures in <Img src={staticFile('shape.svg')}>, <img src>, background-image: url(x.svg) and image layers. The picture is sized from the file's width, height and viewBox (300 x 150 with none) and painted at the size it is drawn at, so it is sharp at any scale. Text inside such a picture is left out and warned about, as a browser sets it in none of the page's fonts. doctor reports a file that cannot be read. (#30)
  • Scene colours take any CSS colour, not only hex: the 148 names (white), rgb(), hsl(), hwb(), lab(), oklch() and color(), with commas or spaces. background: 'white' was rejected with colour "white" must start with '#'. (#30)
  • An element that starts at width: 0 (or any zero length) and is animated wider by a script, such as anime.js width: [0, 100], draws. element.style gave 0 where a browser gives 0px, so anime.js wrote a unitless 55.5 that CSS drops. Zero lengths now read back as 0px.
  • <TransitionSeries> and <Series> accept children wrapped in a React.Fragment, nested ones too, so a .map() can emit a Transition and Sequence pair as Remotion allows. (#31)
  • Machines with a Vulkan 1.4 driver no longer fall back to the CPU. (#1, #2)
  • A frame renders the same when the document was edited between frames: an inline box sits on its baseline in an edited document as it does in a fresh one. (#3)
  • SVG <text> inherits font family, size, weight and style from the HTML around the <svg>. (#4)
  • Text with a glow, shadow, opacity or rotation keeps its descenders and the edge of its glow instead of being cut at the line box. (#8)
  • getComputedStyle and getBoundingClientRect on an element inside an AbsoluteFill that has a background no longer fail with "no element at path". (#7)
  • Page CSS styles SVG content: class, descendant and attribute selectors reach fill, stroke, fonts, text-anchor, paint-order and the other presentation properties. (#14)
  • SVG fill, stroke and stop-color of var(), oklch(), oklab(), color-mix() or rgb(r g b / a) draw in their colour; they were drawn black. A colour that cannot be read is reported. (#14)
  • Page CSS gives SVG shapes transform (with rotate, scale, translate and transform-origin), filter: url(#id), clip-path: url(#id) and mask: url(#id), including shapes a script adds and rules inside @media, through the same cascade. HyperFrames no longer re-applies such rules in TypeScript.
  • An MP4 whose moov atom is at the end decodes its audio. (#14)
  • document.getElementById and querySelector find nodes appended to <body> outside the React root, and SVG <text> measures its real size in getBBox and getComputedTextLength. (#12)
  • transform-style: preserve-3d on an element with opacity, a filter, clip-path, mask, mix-blend-mode, isolation: isolate, contain: paint, backdrop-filter or overflow: hidden failed the render with "a preserve3d group cannot have effects". Such an element is now flattened, as CSS Transforms 2 and Chrome do, and drawn as a flat group with its effect.
  • kinetograph doctor runs the scene validation a render runs on every frame, and lists a scene the render would refuse (with its frames, exit code 1). It used to report only what is unsupported, so a project that cannot render looked fine.
  • A character the font lacks (→, ✓, ≈, an emoji) is no longer drawn as a box. Text falls back, one character at a time, to the rest of the font-family list and then to Noto Sans Symbols 2, Noto Sans Math and Noto Emoji (monochrome), which ship inside the native addon (about 1.1 MB, SIL OFL). This covers HTML text, SVG <text>, text layers, Text3D and canvas fillText. A character no font has (CJK, for one) is still drawn as a box, but doctor and --strict report it with its code point and the font list (font-family, "no glyph for U+2192 ..."), and inspect lists it as missingChars. (#32)

Changed in behaviour

  • A DOM measurement the engine cannot answer (a getComputedStyle or getBoundingClientRect lookup) is a warning, and the script gets the element's own style. The render goes on. --strict still makes it an error.
  • The AbsoluteFill background is written as a hex colour (#rrggbb) in scene JSON and inspect output. A named colour no longer fails the render.
  • Text with a glow, shadow, opacity or rotation renders slightly differently at its edges (the descender and glow fix above). Re-approve snapshot tests that cover such text. (#8)
  • A WebGL canvas starts each frame empty. preserveDrawingBuffer no longer carries one frame's drawing into the next, since every frame must render on its own. (#9)
  • requestAnimationFrame callbacks run after the frame renders, and a jump to a later frame runs them through the frames it skips, so libraries with their own frame loop (Motion) give the same frame alone and in sequence. (#12)
  • The bundler reads a package's browser map for imports that would not resolve otherwise. (#12)

0.1.0 (2026-10-23)

First public release, in beta.

  • Renderer: a Rust GPU compositor (wgpu, Skia) loaded into Node as an N-API addon, with effects, a 2.5D camera, motion blur and a layer cache.
  • Remotion compatibility: remotion and @remotion/* imports resolve to Kinetograph's own implementation, drawn by a native DOM engine for CSS, SVG, Canvas 2D and WebGL fragment shaders.
  • 3D layer (glTF, PBR) and Lottie, both Kinetograph's own.
  • Audio through Web Audio, mixed into the render.
  • CLI kinetograph (still, strip, render, inspect), an MCP server, the studio and an agent skill.

On this page