Fill it even more! Creating a video fill pattern in canvas
A recent comment left in the createPattern reference topic noted that while we talked about using a video for a fill pattern, we didn't show an example. That sounded like a pretty good idea, so here's what I came up with: Video pattern sample.
The video at the top is the original, and the canvas with the fill pattern is the big block below. The small video pattern is repeated in both the vertical and horizontal direction. If you run the video, you'll see that it all moves simultaneously. You can substitute an video file of your own, since this one isn't that action packed. Note: You need to use a hosted .MP4 file, not a random file from YouTube. For the most part, the URL you get in YouTube is a link to a page with an embedded video, not the video itself.
You can set up fills to repeat vertically:
or horizontally:
Or not at all:
Picking how to repeat the pattern is up to you.
If you're familiar with using fills in Canvas, you're only a few steps away from filling with a video. A video fill is essentially a series of still frames swapped out as fast as the computer can do it. The result is a fill with full motion video. Here's how it works.
Fill'r up, getting a shape filled
For a quick refresher, this example shows how to fill a rectangle on a canvas.
<!DOCTYPE html>
<html>
< head>
<title>Simple rectangle fill</title>
< /head>
< body>
< canvas width="1080" height="800" id="myCanvas">
Sorry, canvas isn't supported in this browser or mode
</canvas>
<script>
var canvas = document.getElementById("myCanvas");
if(canvas.getContext){
var ctx = canvas.getContext("2d");
ctx.fillStyle='red';
ctx.fillRect(100, 100, 900, 600);
}
</script>
< /body>
< /html>
Picture a fill
Now to do the same thing with an image. This code example adds a pattern object using an image and the repetition direction. The original <img> element is hidden through a CSS style (display: none;).
<!DOCTYPE html>
<html>
< head>
<title>Simple rectangle pattern fill</title>
<style>
img {
display:none;
}
</style>
< /head>
< body>
<canvas width="1080" height="800" id="myCanvas">
Sorry, canvas isn't supported in this browser or mode
</canvas>
< img id="pix" src="https://go.microsoft.com/fwlink/?LinkID=199028" />
<script>
var canvas = document.getElementById("myCanvas");
if (canvas.getContext) {
var image = document.getElementById("pix");
image.addEventListener("load", function () {
var ctx = canvas.getContext("2d");
var pattern = ctx.createPattern(image, "repeat");
ctx.fillStyle = pattern;
ctx.fillRect(100, 100, 900, 600);
}, false);
}
</script>
< /body>
< /html>
We added a couple of statements, the pattern object variable, an <img> tag with a NASA image, and a little CSS. The CSS simply turns off the display for the <img> tag. In the JavaScript code, we get the image object of the hidden image. Using createPattern, we create a pattern with a repetition of both horizontal and vertical.
We also captured the load event for the image because the image is coming across time and space of the Internet, and might not be fully loaded into the <img> tag when we need it. Once load fires, we then get the canvas context, and do the fill.
Video, one frame at a time
To create a video pattern, we swap out the <img> tag with a <video> tag. Using the same code as the image pattern but with the video element. It works, but only we just get a single video frame each time fill is called. In this example, try clicking the Show video frame button as the video changes.
<!DOCTYPE html>
< html>
< head>
<title>Simple rectangle video frame fill</title>
<style>
video {
/*display:none;*/
}
</style>
< /head>
< body>
<video id="myVideo" autoplay ="autoplay" controls="controls" width="200" height="160" src="https://ie.microsoft.com/testdrive/ieblog/2011/nov/pp4_blog_demo.mp4">
Sorry, video isn't supported in this browser or mode
</video>
<button id="doVideo">Do video capture</button><br />
<canvas width="1080" height="800" id="myCanvas">
Sorry, canvas isn't supported in this browser or mode
</canvas>
<script>
document.getElementById("doVideo").addEventListener("click", fillVideo, false);
fillVideo();
function fillVideo() {
var canvas = document.getElementById("myCanvas");
if (canvas.getContext) {
var video = document.getElementById("myVideo");
var ctx = canvas.getContext("2d");
var pattern = ctx.createPattern(video, "repeat");
ctx.fillStyle = pattern;
ctx.fillRect(100, 100, 900, 600);
}
}
</script>
< /body>
< /html>
Not very dynamic, as it only grabs a single video frame.
Motion potion
To create the magic of full motion video, we'll add an animation loop. We'll also add a button to start and stop the video, since the original video element is hidden in this demo. This example puts it all together.
<!DOCTYPE html>
< html>
< head>
<title>Simple rectangle video frame fill</title>
<style>
video {
/* Turn off the original video element*/
display:none;
}
</style>
< /head>
< body>
<!-- Set up and play video -->
<video id="myVideo" autoplay ="autoplay" src="https://ie.microsoft.com/testdrive/ieblog/2011/nov/pp4_blog_demo.mp4">
Sorry, video isn't supported in this browser or mode
</video>
<button id="stop">Stop video</button><br />
<canvas width="1280" height="800" id="myCanvas">
Sorry, canvas isn't supported in this browser or mode
</canvas>
<script>
var canvas = document.getElementById("myCanvas");
if (canvas.getContext) {
// Canvas setup
var ctx = canvas.getContext("2d");
var video = document.getElementById("myVideo");
var timerID;
var w = canvas.width;
var h = canvas.height;
function animateVideo() {
// Stop loop when video ends
if (video.paused || video.ended) {
return false;
}
// Create a repeat video pattern
pattern = ctx.createPattern(video, "repeat"); // Get the video frame
ctx.fillStyle = pattern; // Assign pattern as a fill style.
ctx.fillRect(0, 0, w, h); // Fill the canvas.
// Restart the animation
timerID = window.requestAnimationFrame(animateVideo);
}
var button = document.getElementById("stop");
// Start or stop video based on video state
button.addEventListener("click", function () {
if (video.paused) {
video.play();
} else {
video.pause();
}
}, false);
// Change button text based on the video state
video.addEventListener("pause", function () {
button.innerHTML = "Play";
}, false);
// Change button text based on the video state, and start animation
video.addEventListener("play", function () {
button.innerHTML = "Pause";
animateVideo();
}, false);
}
</script>
< /body>
< /html>
We've added a couple of elements for ease of use. The button handler for video control has two independent parts. In the first part, the "click" event listener watches for a button press, and then checks to see if the video is paused or not (the only two states we're interested in). If it's paused, it tells the video to play. If not (else), it tells the video to pause.
The second part has two event listeners that watch for the pause and the play events. If the pause event fires, it changes the button text to "Play." If the play event fires, it changes the button text to "Pause." We could have put the button changing code into the click event for the button, but run the risk of getting out of sync with the video. When the video starts (with autoplay), the button might not have the correct label. When we capture the play and pause events, the button text change is independent of the video controls,. So whether we're pausing with the button or the video element (if displayed), it's still in sync.
The secret sauce
For full motion video display, we need to refresh the fill pattern more than just pressing a button like we did in the last example. This is accomplished using the requestAnimationFrame method.
The requestAnimationFrame method is the recommended way of getting accurate animation timing. Previously JavaScript coders used the setTimeout method, which uses a fixed amount of time between frames. This works, but there are two problems. First, the UI might not be ready to render, so it skips frames (choppy animation). Second, when you leave the page, the animation is still running which eats up resources.
The requestAnimationFrame method solves both these problems. RequestAnimationFrame asks the system to let us know when it's ok to draw an image. It never tries to do something when it can't, so there are no skipped frames. When you leave the page, the animation is suspended until you return to the page, so no resources are used.
In our example, we start the animation loop from within the play event. This ensures that the animation loop starts when the video is playing. If we put the call to animateVideo outside the loop, it might try to display the video before it's fully started, in which case it'll exit and never run again.
Let's Play!
One cool thing with patterns, video or otherwise, is that you don't need to have a rectangle for your patterns. This example is a slight modification of the full motion video example. Rather than projecting into a rectangle, we're using a shape from an earlier blog post.
<!DOCTYPE html>
< html>
< head>
<title>Simple rectangle video frame fill</title>
<style>
video {
/* Turn off the original video element*/
display:none;
}
</style>
< /head>
< body>
<!-- Set up and play video -->
<video id="myVideo" autoplay ="autoplay" src="https://ie.microsoft.com/testdrive/ieblog/2011/nov/pp4_blog_demo.mp4">
Sorry, video isn't supported in this browser or mode
</video>
<button id="stop">Play</button> <button id="change">Change fill rule</button> <br />
<canvas width="1280" height="800" id="myCanvas">
Sorry, canvas isn't supported in this browser or mode
</canvas>
<script>
var canvas = document.getElementById("myCanvas");
if (canvas.getContext) {
// Canvas setup
var ctx = canvas.getContext("2d");
var video = document.getElementById("myVideo");
var timerID;
var w = canvas.width;
var h = canvas.height;
function animateVideo() {
// Stop loop when video ends
if (video.paused || video.ended) {
return false;
}
// Create a repeat video pattern
pattern = ctx.createPattern(video, "repeat"); // Get the video frame
ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear area for repeated use
ctx.beginPath();
ctx.strokeStyle = 'blue';
ctx.lineWidth = "5"
ctx.moveTo(200, 110);
ctx.lineTo(50, 110);
ctx.lineTo(300, 310);
ctx.lineTo(200, 10);
ctx.lineTo(100, 310);
ctx.lineTo(350, 110);
ctx.lineTo(200, 110);
ctx.stroke();
ctx.fillStyle = pattern; // Assign pattern as a fill style.
ctx.fill();
// Restart the animation
timerID = window.requestAnimationFrame(animateVideo);
}
var change = document.getElementById("change");
change.addEventListener("click", function () {
if (ctx.msFillRule == "nonzero") {
ctx.msFillRule = "evenodd";
change.innerHTML = "Change to non-zero";
} else {
ctx.msFillRule = "nonzero";
change.innerHTML = "Change to even-odd";
}
},false);
var button = document.getElementById("stop");
// Start or stop video based on video state
button.addEventListener("click", function () {
if (video.paused) {
video.play();
} else {
video.pause();
}
}, false);
// Change button text based on the video state
video.addEventListener("pause", function () {
button.innerHTML = "Play";
}, false);
// Change button text based on the video state, and start animation
video.addEventListener("play", function () {
button.innerHTML = "Pause";
animateVideo();
}, false);
}
</script>
< /body>
< /html>
Fun, right? Now with a little imagination, you can fill all kinds of things with video, circles, triangles, or even text.
Give it a try.
-Jay