Skip to content

Instantly share code, notes, and snippets.

@uwemneku
Created September 22, 2025 20:11
Show Gist options
  • Select an option

  • Save uwemneku/53da519d8f602098c9fb7dacba53a672 to your computer and use it in GitHub Desktop.

Select an option

Save uwemneku/53da519d8f602098c9fb7dacba53a672 to your computer and use it in GitHub Desktop.
Konva js, video export with web worker and media-bunny
import gsap from "gsap";
import Konva from "konva";
import {
BufferTarget,
CanvasSource,
getFirstEncodableVideoCodec,
Mp4OutputFormat,
Output,
QUALITY_VERY_HIGH,
} from "mediabunny";
self.onmessage = async function () {
console.log("worker started");
//
const output = new Output({
target: new BufferTarget(), // Stored in memory
format: new Mp4OutputFormat({}),
});
//
const videoCodec = await getFirstEncodableVideoCodec(
output.format.getSupportedVideoCodecs(),
{
width: 800,
height: 400,
},
);
Konva.Util.createCanvasElement = () => {
const canvas = new OffscreenCanvas(1, 1);
canvas.style = {};
return canvas as unknown as HTMLCanvasElement;
};
const stage = new Konva.Stage({
width: 800,
height: 400,
});
const layer = new Konva.Layer();
const rec = new Konva.Rect({ width: 800, height: 400, fill: "white" });
layer.add(rec);
//
const circles = new Array(200).fill(0).map(() => {
const c = new Konva.Circle({
x: 40 * Math.random() * 10,
y: 40 * Math.random() * 10,
radius: 20,
// random fill
fill: `hsl(${360 * Math.random()}, 100%, 50%)`,
stroke: "black",
strokeWidth: 1,
});
const x = c.x();
const y = c.y();
const newX = 800 * Math.random();
const newY = 400 * Math.random();
const distanceX = newX - x;
const distanceY = newY - y;
layer.add(c);
return { c, x, y, newX, newY, distanceX, distanceY };
});
//
stage.add(layer);
//
if (!videoCodec) {
throw new Error("Your browser doesn't support video encoding.");
}
//
const frameRate = 60;
const offScreenCanvas = stage.getLayers()[0].getCanvas()._canvas;
const canvasSource = new CanvasSource(offScreenCanvas, {
codec: videoCodec,
bitrate: QUALITY_VERY_HIGH,
});
output.addVideoTrack(canvasSource, { frameRate });
await output.start();
//
console.time("export");
const TOTAL_DURATION = 10; // seconds
const totalFrames = frameRate * TOTAL_DURATION;
for (let i = 1; i < totalFrames; i++) {
const progress = gsap.parseEase("none")(i / totalFrames);
circles.forEach(({ c, x, y, distanceX, distanceY }) => {
c.x(x + distanceX * progress);
c.y(y + distanceY * progress);
});
await canvasSource.add((i / totalFrames) * TOTAL_DURATION, 1 / frameRate);
}
//
canvasSource.close();
await output.finalize();
const videoBlob = new Blob([output.target.buffer!], {
type: output.format.mimeType,
});
const resultVideo = URL.createObjectURL(videoBlob);
console.timeEnd("export");
self.postMessage(resultVideo);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment