Library / SDK
spite/ccapture.js avatar
spite/ccapture.js

CCapture.js 2.0: Fixed-Framerate Canvas Recording with a Virtual Clock

A library to capture canvas-based animations at a fixed framerate

3,768 stars402 forksJavaScriptMIT

At a glance

What is it?
CCapture.js records canvas, WebGL and WebGPU animations at an exact framerate by lying to your render loop about the time. Version 2.0.0 is an ES-module rewrite built on WebCodecs, and it is not the same tool as the 1.x script you may already have in a page.
Who is it for?
Adopt CCapture.js 2.0.0 if you already have a canvas, WebGL or WebGPU animation and need a deterministic video file at an exact framerate, and if your audience is on current Chrome or Edge, where WebCodecs is available. Do not adopt it if you must support older browsers, or if you only need a screenshot, because the whole library exists to solve the timing problem, not the image problem.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 66 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem CCapture.js solves: wall-clock time ruins exported animations

A canvas animation drawn in a browser runs on wall-clock time. If a frame takes 40 ms to draw instead of 16, the next frame arrives late, and a recorder that grabs frames as they appear produces a video with uneven motion. The usual workaround is to slow the animation down and hope the machine keeps up, which is unreliable on heavy scenes and impossible to guarantee on someone else's laptop.

CCapture.js takes the other route. It replaces the browser's notion of time with a virtual clock that advances exactly one fixed step per captured frame. Your animation still renders as fast or as slowly as it needs to, but anything that derives motion from elapsed time sees a clock that only moves in 1/60th of a second increments. The exported file is then smooth at exactly the framerate you asked for, whether that is 30, 60 or 240.

The README frames this as a rewrite of the original CCapture.js, citing ryg's kkapture as the idea's origin. The audience is narrow and specific: people who already have a canvas animation and want a video file out of it, not people looking for a general screen recorder.

TimeWarp, FrameWrap and the composition that makes CCapture

The library is three pieces, and the split is the most interesting design decision in it.

TimeWarp owns time. According to the README it hooks performance.now, requestAnimationFrame, setTimeout and setInterval, Date, and media currentTime, then advances a virtual clock on demand. It knows nothing about canvases. FrameWrap owns capture: it takes already-drawn canvas frames, applies motion blur, enforces frame and time limits, and feeds a pluggable encoder. It knows nothing about time. CCapture composes the two and drives them.

That composition explains a behaviour that would otherwise look strange. Because requestAnimationFrame is hooked during capture, your render loop only advances when CCapture.capture() flushes it. The recorder paces the animation rather than the other way around, running as fast as the machine can draw and encode, with no wall-clock throttle.

The README is explicit that the halves are usable alone: TimeWarp for timing without capture, FrameWrap for encoding frames you produce yourself without touching the browser clock. That is a real API surface and not a marketing split, since package.json exposes ./TimeWarp and ./FrameWrap as separate export paths with their own type declarations.

Installing CCapture.js from npm or a CDN

The package is published as ccapture.js. The README gives a single install command, and the package is type: module with src/index.js as both main and module entry, so an ES-module import is the expected path.

bash
npm install ccapture.js

After that, the default export is the CCapture class, and the named exports give you the two halves.

js
import CCapture from "ccapture.js";
// or: import { CCapture, FrameWrap, TimeWarp } from "ccapture.js";

The package ships TypeScript declarations at src/index.d.ts, referenced from the types field, so autocomplete and type checking work without a separate @types package. The README also notes you can use it straight from source as native ES modules with no build step by pointing at src/index.js and serving over HTTP.

If you are not using a bundler, a UMD/IIFE build is included and the README points at jsDelivr for it. The whole API hangs off window.CCapture, including CCapture.FrameWrap, CCapture.TimeWarp and CCapture.createMotionBlur.

html
<script src="https://cdn.jsdelivr.net/npm/ccapture.js@2/build/ccapture.umd.min.js"></script>
<script>
  const capturer = new CCapture({ format: "mp4", framerate: 60, timeLimit: 5 });
</script>

You can build that file yourself with npm run build, which esbuild writes to build/ccapture.umd.js and build/ccapture.umd.min.js.

Your first capture: one line inside an existing render loop

The README calls this the least-intrusive API, and the claim holds up: the only line you add inside your loop is the capture call. The constructor takes the format, the target framerate and a time limit after which the recorder stops and saves itself.

js
const capturer = new CCapture({ format: "mp4", framerate: 60, timeLimit: 5 });
capturer.start();

function render() {
  requestAnimationFrame(render);
  // draw using performance.now() (warped) or the rAF timestamp
  capturer.capture(canvas);
}
render();

Three details matter here. The frame you pass to capture() should already be drawn, and capture() is async but you do not need to await it. You must kick your loop once right after start(), or make sure it is already running, because capture() drives it from then on. And if you do not set a timeLimit or frameLimit, nothing stops on its own: you call capturer.stop() and then capturer.save() yourself.

For three.js the README passes the renderer's canvas directly, calling capturer.capture(renderer.domElement) after renderer.render(scene, camera). For p5.js, p5 owns the loop, so the example calls noLoop() and drives drawing with redraw() inside its own tick function, resuming the live preview on the stop event with capturer.on("stop", () => loop()).

The hookDate trade-off and what breaks when the clock is a lie

Replacing the browser's clock is invasive, and the README devotes its own section to the hookDate trade-off rather than burying it. The hooks cover performance.now, requestAnimationFrame, setTimeout and setInterval, Date, and media currentTime. Anything in your page that reads those during capture sees warped values, not just your animation.

That is the central limitation. If a third-party library in the same page measures elapsed time for its own purposes, a network retry timer, a physics engine with its own accumulator, an analytics heartbeat, it will be reading the virtual clock while capture runs. The README's guidance is to derive motion from performance.now(), the rAF timestamp or p5's millis(), all of which are warped, rather than fixed per-frame increments. Code that already uses fixed increments is the case where the virtual clock buys you nothing, because there was never any wall-clock dependence to remove.

The browser support section is the other constraint. The encoders are built on WebCodecs, which is a modern-browser API, and the README notes that GPU motion blur currently needs WebGL2. A WebGPU source canvas captures fine because WebCodecs reads it directly, but it falls back to the CPU blur backend. So the combination of WebGPU rendering and GPU motion blur is not available, and older browsers are out of scope entirely. If you need a recorder that runs anywhere, this is the wrong tool.

Formats, codecs and the encoders you can swap in

The format option accepts mp4, webm, webm-legacy, png, jpg and webp, with mp4 as the default. The codec option takes avc, vp9, av1 or vp8, and the default depends on the container: avc for mp4, vp9 for webm. Quality is a 0 to 100 value defaulting to 90, and the README states it drives the video bitrate as well as JPEG and WebP quality, while PNG ignores it. The bitrate option defaults to 0, meaning the bitrate is derived from resolution and quality.

Frame and time limits are the two ways to bound a recording. GIF output has its own gifColors option for palette size, defaulting to 256 and accepting 2 to 256. The README also documents a custom encoder path, which is the natural extension point for a format the built-in set does not cover.

A real alternative is the original CCapture.js 1.x. Both lie to the animation about time, so the timing idea is not what separates them. The difference is the encoding layer and the module system: 1.x predates WebCodecs and ships as a classic script, while 2.0.0 is an ES-module rewrite with TypeScript declarations, a UMD build for script tags, and encoder output through WebCodecs. The README includes a migration section from 1.x, which is a signal that the API changed enough to need one. If your page already loads the 1.x global and works, moving to 2.0.0 buys you modern encoders and types at the cost of a browser-support floor and an API migration.

Maintenance, licensing and what an upgrade actually costs

The repository is not archived, and the last push was on 2026-07-27. The v2.0.0 release is dated the same day, so the current line is recent, but that is a single data point about a rewrite rather than a long release history. Judge the project on the fact that 2.0.0 landed in July 2026 and plan accordingly.

The licence is MIT. That is permissive and places few obligations on how you redistribute the library, but it is a copyright licence only: it says nothing about the patent questions that can attach to video codecs such as avc, vp9 and av1. Those questions depend on your jurisdiction and your distribution model, and the repository does not address them. That is a matter for your own counsel, not something this article can settle.

Upgrade cost is concentrated in the 1.x to 2.0.0 move. The README provides a migration section, the package is now type: module, and the UMD global is a separate export path rather than the default artifact. The build step is esbuild, invoked by npm run build, and prepublishOnly runs it before publishing. The test script chains six Node test files plus separate browser and type-check commands, so the project's own verification story is scriptable rather than manual.

Editorial conclusion

Adopt CCapture.js 2.0.0 if you already have a canvas, WebGL or WebGPU animation and need a deterministic video file at an exact framerate, and if your audience is on current Chrome or Edge, where WebCodecs is available. Do not adopt it if you must support older browsers, or if you only need a screenshot, because the whole library exists to solve the timing problem, not the image problem. Before committing, verify three things on your own hardware: that the default avc codec and your target container behave as you expect, that your animation derives motion from performance.now() or the rAF timestamp rather than a fixed per-frame increment, and that the size of your frame buffers fits the memory budget of the machine doing the recording.

Frequently asked questions

How do I install CCapture.js?

Install it from npm with npm install ccapture.js, then import the default export or the named CCapture, FrameWrap and TimeWarp exports. If you are not using a bundler, the README points at a UMD build on jsDelivr that exposes the API on window.CCapture.

Can I load CCapture.js from a CDN instead of npm?

Yes. The README gives a script tag pointing at the UMD build on jsDelivr, and the whole API hangs off window.CCapture, including CCapture.FrameWrap, CCapture.TimeWarp and CCapture.createMotionBlur. You can also build that file yourself with npm run build.

Does CCapture.js need WebCodecs?

The encoders are built on WebCodecs, which is a modern-browser API, and the README puts older browsers out of scope. GPU motion blur additionally needs WebGL2, while a WebGPU source canvas captures through WebCodecs but falls back to the CPU blur backend.

How do I stop a CCapture.js recording?

Set a timeLimit or frameLimit and the recorder stops and saves itself. Otherwise call capturer.stop() and then capturer.save() when you are done, because without one of those limits nothing stops on its own.

What is the difference between CCapture.js 1.x and 2.0.0?

Both use a virtual clock to decouple the animation from wall-clock time. Version 2.0.0 is an ES-module rewrite with TypeScript declarations, a UMD build for script tags and encoder output through WebCodecs, and the README includes a migration section from 1.x.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. spite/ccapture.js on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/spite-ccapture-js.svg)](https://hysenlabs.com/projects/spite-ccapture-js)