Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Last active April 29, 2022 07:10
Show Gist options
  • Save mattdesl/3b05793e779f47f852725c7712264f54 to your computer and use it in GitHub Desktop.
Save mattdesl/3b05793e779f47f852725c7712264f54 to your computer and use it in GitHub Desktop.
h264-mp4-encoder + canvas-sketch = in-browser MP4 generation
// npm canvas-sketch-cli -g
// canvas-sketch sketch.js --open
const canvasSketch = require('canvas-sketch');
const H264 = require('h264-mp4-encoder');
const { lerp } = require("canvas-sketch-util/math");
const settings = {
duration: 4,
dimensions: [1024, 1024],
animate: true,
fps: 60,
};
// Start the sketch
function sketch ({ update }) {
return ({ context, frame, width, height, playhead }) => {
context.clearRect(0, 0, width, height);
context.fillStyle = "white";
context.fillRect(0, 0, width, height);
const gridSize = 7;
const padding = width * 0.2;
const tileSize = (width - padding * 2) / gridSize;
for (let x = 0; x < gridSize; x++) {
for (let y = 0; y < gridSize; y++) {
// get a 0..1 UV coordinate
const u = gridSize <= 1 ? 0.5 : x / (gridSize - 1);
const v = gridSize <= 1 ? 0.5 : y / (gridSize - 1);
// scale to dimensions with a border padding
const tx = lerp(padding, width - padding, u);
const ty = lerp(padding, height - padding, v);
// here we get a 't' value between 0..1 that
// shifts subtly across the UV coordinates
const offset = u * 0.2 + v * 0.1;
const t = (playhead + offset) % 1;
// now we get a value that varies from 0..1 and back
let mod = Math.sin(t * Math.PI);
// we make it 'ease' a bit more dramatically with exponential
mod = Math.pow(mod, 3);
// now choose a length, thickness and initial rotation
const length = tileSize * 0.65;
const thickness = tileSize * 0.1;
const initialRotation = Math.PI / 2;
// And rotate each line a bit by our modifier
const rotation = initialRotation + mod * Math.PI;
// Now render...
draw(context, tx, ty, length, thickness, rotation);
}
}
};
function draw(context, x, y, length, thickness, rotation) {
context.save();
context.fillStyle = "black";
// Rotate in place
context.translate(x, y);
context.rotate(rotation);
context.translate(-x, -y);
// Draw the line
context.fillRect(x - length / 2, y - thickness / 2, length, thickness);
context.restore();
}
}
(async () => {
const manager = await canvasSketch(sketch, {
duration: 5,
...settings,
animate: true,
playing: false
});
const fps = manager.props.fps;
const encoder = await H264.createH264MP4Encoder();
// Must be a multiple of 2.
encoder.width = settings.dimensions[0];
encoder.height = settings.dimensions[1];
encoder.quantizationParameter = 10;
encoder.speed = 0;
encoder.frameRate = fps;
encoder.groupOfPictures = fps; // only tested with 60 ...
encoder.debug = true;
encoder.initialize();
let fpsInterval = 1 / fps;
let frame = 0;
let loop = setInterval(() => {
if (frame >= manager.props.totalFrames) {
clearInterval(loop);
finalize();
return;
}
manager.sketch({
...manager.props,
deltaTime: frame === 0 ? 0 : fpsInterval,
playhead: frame / manager.props.totalFrames,
frame,
time: frame * fpsInterval,
});
console.log(frame, manager.props.totalFrames);
const rgba = manager.props.context.getImageData(0, 0, encoder.width, encoder.height);
encoder.addFrameRgba(rgba.data)
frame++
}, 0);
const download = (url, filename) => {
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename || "download";
anchor.click();
};
function finalize () {
encoder.finalize();
const uint8Array = encoder.FS.readFile(encoder.outputFilename);
download(URL.createObjectURL(new Blob([uint8Array], { type: "video/mp4" })))
encoder.delete();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment