Generate Video from JavaScript Canvas

Eugene Musebe

Introduction

The canvas element allows you to add more interactivity to your web pages because now you can control the graphics, images, and text dynamically with a scripting language. This article shows how drawn canvas can be manipulated to form a single video object.

Codesandbox

Check the sandbox demo on Codesandbox.

You can also get the project Github repo using Github.

Prerequisites

Entry-level javascript and React/Nextjs knowledge.

Setting Up the Sample Project

In your respective folder, create a new nextjs app using npx create-next-app canvasvideo using your terminal. Head to your project root directory cd canvasvideo

Nextjs contains a serverside rendering backend which we will use for our media files upload. Set up Cloudinary for our backend.

Create your Cloudinary account using this Link. Log in to a dashboard containing the environment variable keys necessary for the Cloudinary integration in our project.

Include Cloudinary in your project dependencies: npm install cloudinary.

Create a new file named .env.local and paste the following guide to fill your environment variables. You can locate your variables from the Cloudinary dashboard. Use the guide below to fill in your variables.

1CLOUDINARY_CLOUD_NAME =
2
3CLOUDINARY_API_KEY =
4
5CLOUDINARY_API_SECRET =

Restart your project: npm run dev.

In the pages/api directory, create a new file named upload.js. Configure the Cloudinary environment keys and libraries.

1var cloudinary = require("cloudinary").v2;
2
3cloudinary.config({
4 cloud_name: process.env.CLOUDINARY_NAME,
5 api_key: process.env.CLOUDINARY_API_KEY,
6 api_secret: process.env.CLOUDINARY_API_SECRET,
7});

Create a handler function to execute the POST request. The function will receive media file data and post it to the Cloudinary website. It then captures the media file's Cloudinary link and sends it back as a response.

1export default async function handler(req, res) {
2 if (req.method === "POST") {
3 let url = ""
4 try {
5 let fileStr = req.body.data;
6 const uploadedResponse = await cloudinary.uploader.upload_large(
7 fileStr,
8 {
9 resource_type: "video",
10 chunk_size: 6000000,
11 }
12 );
13 url = uploadedResponse.url
14 } catch (error) {
15 res.status(500).json({ error: "Something wrong" });
16 }
17
18 res.status(200).json({data: url});
19 }
20}

The code above concludes our backend.

In the front end, in order to create the video we need to create a canvas animation. Inside the index component use the following code in your return statement.

1"pages/index.js"
2
3
4return (
5 <div className='container'>
6 <h2>Generate video from javascript canvas</h2>
7 <div className='row'>
8 <div className='column'>
9 {link?
10 <a href={link}>check link</a>
11 :
12 "click 'RECORD' to record canvas video"
13 }
14 <br />
15 <canvas id="canvas" width="300" height="300" />
16 <br />
17 <button onClick={record}>RECORD</button>
18 </div>
19 </div>
20 </div>
21)

The code above, using the css files from the github repo should generate a UI like below:

Now to animate the canvas. Start by importing the following 3 statehooks. We will use them as we move on. Also we will be animating part of the solar system inside the canvas. the useState variable below will be used to save the final link from our backend.

1"pages/index.js"
2
3import { useRef, useState, useEffect } from "react";
4
5export default function Home() {
6 const [link, setLink] = useState('');
7
8 var sun, moon, earth, canvas;
9
10 return (
11 <div className='container'>
12 <h2>Generate video from javascript canvas</h2>
13 <div className='row'>
14 <div className='column'>
15 {link?
16 <a href={link}>check link</a>
17 :
18 "click 'RECORD' to record canvas video"
19 }
20 <br />
21 <canvas id="canvas" width="300" height="300" />
22 <br />
23 <button onClick={record}>RECORD</button>
24 </div>
25 </div>
26 </div>
27 )
28}

Assign the three variables; sun, moon and earth to new image elements and use the images provided for the image sources. We then use the setIntervall to call a draw function every 100 milliseconds.

1useEffect(() => {
2 canvas = document.getElementById('canvas');
3
4 sun = document.createElement('img');
5 moon = document.createElement('img');
6 earth = document.createElement('img');
7
8 sun.src = 'images/sun.png';
9 moon.src = 'images/moon.png';
10 earth.src = 'images/earth.png';
11 setInterval(draw, 100);
12 },[])

Let's create the draw function. Note that in the use effect hook we have a the canvas variable assigned to the DOM element. Therefore in the draw function, start by retreiving the canvas context. We use the lobalCompositeOperation property of the Canvas 2D API to the type of compositing operation to apply when drawing new shapes.

1"pages/index.js"
2
3 const draw = () => {
4 var ctx = canvas.getContext('2d');
5
6 ctx.globalCompositeOperation = 'destination-over';
7 ctx.clearRect(0, 0, 300, 300); // clear canvas
8
9 ctx.fillStyle = 'rgba(0,0,0,0.4)';
10 ctx.strokeStyle = 'rgba(0,153,255,0.4)';
11 ctx.save();
12 ctx.translate(150, 150);
13}

Use the following guide to draw the 3 images on the canvas. You can play with the code to make it behave to your preference. That should complete the animation procedure

1"pages/index.js"
2
3 const draw = () => {
4 var ctx = canvas.getContext('2d');
5
6 ctx.globalCompositeOperation = 'destination-over';
7 ctx.clearRect(0, 0, 300, 300); // clear canvas
8
9 ctx.fillStyle = 'rgba(0,0,0,0.4)';
10 ctx.strokeStyle = 'rgba(0,153,255,0.4)';
11 ctx.save();
12 ctx.translate(150, 150);
13
14 // Earth
15 var time = new Date();
16 ctx.rotate(((2 * Math.PI) / 60) * time.getSeconds() + ((2 * Math.PI) / 60000) * time.getMilliseconds());
17 ctx.translate(105, 0);
18 ctx.fillRect(0, -12, 50, 24); // Shadow
19 ctx.drawImage(earth, -12, -12);
20
21 // Moon
22 ctx.save();
23 ctx.rotate(((2 * Math.PI) / 6) * time.getSeconds() + ((2 * Math.PI) / 6000) * time.getMilliseconds());
24 ctx.translate(0, 28.5);
25 ctx.drawImage(moon, -3.5, -3.5);
26 ctx.restore();
27
28 ctx.restore();
29
30 ctx.beginPath();
31 ctx.arc(150, 150, 105, 0, Math.PI * 2, false); // Earth orbit
32 ctx.stroke();
33
34 ctx.drawImage(sun, 0, 0, 300, 300);
35}

Now that the canvas is animated, we want to record the canvas activity as video. First we create an array named chunks to store the recorded chunks as blobs. We grab a canvas media stream and initialize the media stream recorder. Once a recorder stops, we construct a complete Blob from all the chunks. We set the recording to stop in 3 seconds. You can set the time to your preference. The chunk blob will then be sent to the uploadHandler function for cloudinary upload.

1"pages/index.js"
2
3 const record = () => {
4 console.log('canvas', canvas);
5 const chunks = [];
6 const stream = canvas.captureStream(); // grab our canvas MediaStream
7 const rec = new MediaRecorder(stream); // init the recorder
8
9 rec.ondataavailable = e => chunks.push(e.data);
10 rec.onstop = e => uploadHandler(new Blob(chunks, { type: 'video/webm' }));
11
12 rec.start();
13 setTimeout(() => rec.stop(), 3000);
14}

In the uploadHandler function, we use a file reader to encode the blobs to bse64 format and upload them to the backend using a POST method. The response will be a url string of the media file Cloudinary url. We store it in the link state hook so we can view the final project from the UI.

The final project should look like below:

That completes it. You can use the article above to enjoy the experience. Enjoy your coding

Eugene Musebe

Software Developer

I’m a full-stack software developer, content creator, and tech community builder based in Nairobi, Kenya. I am addicted to learning new technologies and loves working with like-minded people.