February 26, 2011 will

Ken Burns effect with Javascript and Canvas

I recently decided to look into working with the Canvas element to prototype a game idea I had. Since the easiest way to learn a technology is to use it, I set myself the goal of implementing the Ken Burns Effect.

There are a few JS slideshow scripts that do the Ken Burns effect, but I haven't seen any implemented in Canvas.

Without further ado, here is my implementation of the effect:

Your browser doesn't support canvas! Try Chrome, Firefox or Opera

If you see the effect, above, you are probably viewing this in one of the good browsers. I think it can be made to run on IE with excanvas, although I have yet to test that.

If there is enough interest, I may open source the code and add some new features / docs. In the meantime, feel free to download the code (a JQuery plugin) or run it on your site:

Ken Burns Effect in Javascript and Canvas

There's actually not that much to it. Here is the code to kenburns.js:

(function($){

    $.fn.kenburns = function(options) {

        var $canvas = $(this);
        var ctx = this[0].getContext('2d');
        var start_time = null;
        var width = $canvas.width();
        var height = $canvas.height();

        var image_paths = options.images;
        var display_time = options.display_time || 7000;
        var fade_time = Math.min(display_time / 2, options.fade_time || 1000);
        var solid_time = display_time - (fade_time * 2);
        var fade_ratio = fade_time - display_time
        var frames_per_second = options.frames_per_second || 30;
        var frame_time = (1 / frames_per_second) * 1000;
        var zoom_level = 1 / (options.zoom || 2);
        var clear_color = options.background_color || '#000000';

        var images = [];
        $(image_paths).each(function(i, image_path){
            images.push({path:image_path,
                         initialized:false,
                         loaded:false});
        });
        function get_time() {
            var d = new Date();
            return d.getTime() - start_time;
        }

        function interpolate_point(x1, y1, x2, y2, i) {
            // Finds a point between two other points
            return  {x: x1 + (x2 - x1) * i,
                     y: y1 + (y2 - y1) * i}
        }

        function interpolate_rect(r1, r2, i) {
            // Blend one rect in to another
            var p1 = interpolate_point(r1[0], r1[1], r2[0], r2[1], i);
            var p2 = interpolate_point(r1[2], r1[3], r2[2], r2[3], i);
            return [p1.x, p1.y, p2.x, p2.y];
        }

        function scale_rect(r, scale) {
            // Scale a rect around its center
            var w = r[2] - r[0];
            var h = r[3] - r[1];
            var cx = (r[2] + r[0]) / 2;
            var cy = (r[3] + r[1]) / 2;
            var scalew = w * scale;
            var scaleh = h * scale;
            return [cx - scalew/2,
                    cy - scaleh/2,
                    cx + scalew/2,
                    cy + scaleh/2];
        }

        function fit(src_w, src_h, dst_w, dst_h) {
            // Finds the best-fit rect so that the destination can be covered
            var src_a = src_w / src_h;
            var dst_a = dst_w / dst_h;
            var w = src_h * dst_a;
            var h = src_h;
            if (w > src_w)
            {
                var w = src_w;
                var h = src_w / dst_a;
            }
            var x = (src_w - w) / 2;
            var y = (src_h - h) / 2;
            return [x, y, x+w, y+h];
        }

        function get_image_info(image_index, load_callback) {
            // Gets information structure for a given index
            // Also loads the image asynchronously, if required
            var image_info = images[image_index];
            if (!image_info.initialized) {
                var image = new Image();
                image_info.image = image;
                image_info.loaded = false;
                image.onload = function(){
                    image_info.loaded = true;
                    var iw = image.width;
                    var ih = image.height;

                    var r1 = fit(iw, ih, width, height);;
                    var r2 = scale_rect(r1, zoom_level);

                    var align_x = Math.floor(Math.random() * 3) - 1;
                    var align_y = Math.floor(Math.random() * 3) - 1;
                    align_x /= 2;
                    align_y /= 2;

                    var x = r2[0];
                    r2[0] += x * align_x;
                    r2[2] += x * align_x;

                    var y = r2[1];
                    r2[1] += y * align_y;
                    r2[3] += y * align_y;

                    if (image_index % 2) {
                        image_info.r1 = r1;
                        image_info.r2 = r2;
                    }
                    else {
                        image_info.r1 = r2;
                        image_info.r2 = r1;
                    }

                    if(load_callback) {
                        load_callback();
                    }

                }
                image_info.initialized = true;
                image.src = image_info.path;
            }
            return image_info;
        }

        function render_image(image_index, anim, fade) {
            // Renders a frame of the effect
            if (anim > 1) {
                return;
            }
            var image_info = get_image_info(image_index);
            if (image_info.loaded) {
                var r = interpolate_rect(image_info.r1, image_info.r2, anim);
                var transparency = Math.min(1, fade);

                if (transparency > 0) {
                    ctx.save();
                    ctx.globalAlpha = Math.min(1, transparency);
                    ctx.drawImage(image_info.image, r[0], r[1], r[2] - r[0], r[3] - r[1], 0, 0, width, height);
                    ctx.restore();
                }
            }
        }

        function clear() {
            // Clear the canvas
            ctx.save();
            ctx.globalAlpha = 1;
            ctx.fillStyle = clear_color;
            ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
            ctx.restore();
        }

        function update() {
            // Render the next frame
            var update_time = get_time();

            var top_frame = Math.floor(update_time / (display_time - fade_time));
            var frame_start_time = top_frame * (display_time - fade_time);
            var time_passed = update_time - frame_start_time;

            function wrap_index(i) {
                return (i + images.length) % images.length;
            }

            if (time_passed < fade_time)
            {
                var bottom_frame = top_frame - 1;
                var bottom_frame_start_time = frame_start_time - display_time + fade_time;
                var bottom_time_passed = update_time - bottom_frame_start_time;
                if (update_time < fade_time) {
                    clear();
                } else {
                    render_image(wrap_index(bottom_frame), bottom_time_passed / display_time, 1);
                }
            }

            render_image(wrap_index(top_frame), time_passed / display_time, time_passed / fade_time);

            if (options.post_render_callback) {
                options.post_render_callback($canvas, ctx);
            }

            // Pre-load the next image in the sequence, so it has loaded
            // by the time we get to it
            var preload_image = wrap_index(top_frame + 1);
            get_image_info(preload_image);
        }

        // Pre-load the first two images then start a timer
        get_image_info(0, function(){
            get_image_info(1, function(){
                start_time = get_time();
                setInterval(update, frame_time);
            })
        });

    };

})( jQuery );
Use Markdown for formatting
*Italic* **Bold** `inline code` Links to [Google](http://www.google.com) > This is a quote > ```python import this ```
your comment will be previewed here
gravatar
Rob
Nicely done. Is there a benefit to doing this with canvas? I would guess that the fastest/smoothest way might be using CSS transitions or keyframes.
gravatar
Brad Heller
@Rob I would assume that it's for compatibility sake. CSS transitions are still buggy at best!
gravatar
Will McGugan
In think the zooming should be better quality with Canvas, if it support sub-pixel rendering. In Firefox it is very smooth, so I'm guessing that the image rendering is sub-pixel. But on Chrome, there is a slight jitter.
gravatar
samuelgilman
Looks great on safari.
gravatar
Juho Vepsäläinen
You should probably use requestAnimationFrame where available instead of the current interval trick for animating. Looks cool otherwise.

https://gist.github.com/838785
gravatar
Marco Gallen
Excelent example! What about perfomrmance? Does incrising the number of images decrease the performance of the browser?
gravatar
Will McGugan
Juho, I wasn't aware of that. Thanks.

Marco, Increasing the number of images won't have any effect on performance. A maximum of two images are rendered at any one time (2 for the cross-fade). The images are also loaded on the fly, so it doesn't have to wait till it has loaded all the images before starting the slideshow.
gravatar
Tom B
Looks good in IE9
gravatar
Njunik
Hi Will,
on my site I obtain an error "TypeError: Result of expression 'this.getContext' is not a function.“ on ”var ctx = this.getContext('2d');"
Any suggestion?

PS: tnx for your work :)
gravatar
Will McGugan
Njunik, I'm guessing that either your running it on <IE9 (use http://excanvas.sourceforge.net/ if that's the case), or you called kenburns with a selector for something other than a canvas tag.
gravatar
njunik
@Will Ok. It was my error. With canvas tag it's all ok. Tnx
gravatar
Chris Harding
How do you use it to fit 100% on all monitors as if you use a percent rather than a number for the width and height it zooms the image right in?
gravatar
Will McGugan
Chris, not sure if I follow you, but it does have to do some cropping if the aspect ratio of the image doesn't match the aspect ratio of the window.
gravatar
James
Hi Will,

I as well am having an issue with line “var ctx = this.getContext('2d');” stating “Object does not support this property or method.” This is for IE8 running excanvas, and I called kenburns with a selector for a canvas tag.

I have tried to tweak the code but no change. Any suggestions?
gravatar
Konrad
Hi Will,

I just try to implement your fancy script. I’d like to run the show at least only one time - but its looping. How can I stop the show t the end of the images?

yours, K.
gravatar
Sebas
Will,

I used your script in a website, but I want to get it working in IE8 and lower. But it won't work. With ExCanvas it will show one still image and nothing else.

Maybe you already have an solution?

Thanks in advance!
gravatar
Ionel Roiban
@Sebas - there is no solution for IE7-8, this is just a proof of concept for Ken Burns effect using HTML5 canvas. If you need KB effect in older IE browsers use one of the many javascript or CSS implementations.

@Will - that jitter in Chrome is totally annoying. It defeats the whole purpose. How do we fix that?
gravatar
Will McGugan
Ionel, I don't think it is fixable unfortunately. I suspect that Chrome is using a faster algorithm for scaling images, which comes at the expense of quality. The loss in quality is probably not noticeable in the vast majority of Canvas applications.

Maybe one day I'll try implementing it in WebGL. I think that would guarantee high quality across browsers.
gravatar
corsaro
Hi'
how I can add your choice of images?
I have modified
onclick: impost –> image_index = 1;
image_info = get_image_info(image_index);
does not work!

Thanks
gravatar
René Mérou
Hi Will,

Is it possible to change the zoom rectables every time the cicle restart?

It seems the images one time they are rendered there will do the same for ever.

I can automaticaly reload the page but that will reload the images too and thats not a good use resources.

Is it a way to renderize everything with the new ramdom rectangles but without the need to reload the images?

regards

René
gravatar
Stafford
Merci beaucoup, depuis le temps que je chercher ce module
gravatar
Francis
Merci beaucoup !
Thank you very much, Will, your code is simple and works very well ! It will be very usefull for me !

I made a version with a fallback, that use “xfade2” to replace “Ken Burns effect”, for the browsers which don't know canvas tag.
You can find it here.

(I remove the first image, because it has a different size ; the others have the same size, so it's better for xfade2)
gravatar
jitter
Is there a way to remove that jitter effects?
I've noticed this on safari, but on opera it looks much worse…

thanks,
gravatar
Will McGugan
I'm afraid not. It depends on the accuracy of how the browser scales images.
gravatar
Raghavendra
Very nice and easy to makeout. Many thanks for the same.I want to know is it possible to overlay texts on the images, if possible please guide me how to do this.
Thanking you
gravatar
Urbanaut
Regarding the jitter, I've found that higher resolution images do not jitter as there's enough scope to do the scaling calculation optimally.

This looks really nice when it's slowed down.

Thanks Will.
gravatar
Jo
How do I make the gallery resize automatically to fit all windows irrespective of the resolution of the machine…
gravatar
Ian
Hi Will,

Is there a parameter to limit the left>right zoom AND ensure it zooms to the top of a photo?

For example, say you had a few photos of people wearing hats (perhaps you run a Hatters Shop! Strange but go with it!) The target of each photo would then be the wearers face and hat. It would be undesirable to have the zoom effect end on the wearers chest, for example.

So… How would you “guarantee” a bottom>top zoom, everytime?

Great script, btw!
gravatar
eAnka
Absolutely great! That's what I was looking for! Thank you!
gravatar
Gabi
Hi, can you please tell me how can I give the images from code behind? in some kind of collection. Thanks
gravatar
Jordi Alhambra
Hi Will,
First of all thanks so much for your nice code.
Just to report that everything is fine until I change from XHTML to <!DOCTYPE html> (HTML5) when any image nor slideshow signs are shown.
Is there any workaround or anything I'm missing there?
Thanks in advance for your reply.

gravatar
Jordi Alhambra
Solved, please delete prev question.
gravatar
Tushar
thanks
gravatar
Dave
Brilliant, thanks for this! I used it at dcporter.net/me/instagram to ken-burns my instagram feed.

I noticed and fixed a bug with canvases whose bitmap sizes are different than their onscreen sizes. For example:

<div style=“width: 200px”>
<canvas height=“500” width=“500” style=“width: 100%” />
</div>

Here, $canvas.width() will return 200, and you end up painting in 200 pixels of a 500-px bitmap. Here's my fix:

 var width = $canvas.attr('width') || $canvas.width();
var height = $canvas.attr('height') || $canvas.height();

This will probably still fail if the canvas's bitmap size isn't reflected in the attributes (e.g. if it's the default size), but it handles more cases with the same fallback.

Otherwise, I'd love to have a way to pause and restart the animation, preferably right where it left off, for example if the canvas goes offscreen. Currently I'm deleting and recreating the element each time, which keeps the CPU fan off but is a huge memory leak. I'd also love to see this up on github or somewhere, for pull request fun…!
gravatar
Dave
I've hacked in the pause functionality (though when you Play again it jumps ahead to where the slideshow would have been if it had kept playing - that's fine in my situation). I'd like to add the ability to start the next item's fade-in on demand (e.g. on click), but that disrupts the neatly deterministic flow that you've got. Any advice on how to approach this problem? If it doesn't require a complete rewrite I may be able to tackle it myself.

Cheers
Dave
gravatar
Will McGugan
@Dave, nothing trivial springs to mind. At the moment, I think the entire sequence is determined by the current time. If you want to make changes mid-sequence you'll need to separate the render logic from the logic that picks the frame. Won't require a complete re-write, but it's not a small tweak either…
gravatar
Serdar Camlica
Excellent job. is there any chance to random image sort ?
gravatar
Urbanaut
I've not noticed this until tonight, but this crashes Chrome & Safari on iPad running: iOS 5.1.1 and of course no update for first gen iPads.

Any advice on how to lower the crash rate here?

Thanks Will.
gravatar
Will McGugan
Not sure there is much that can be done about that I'm afraid!
gravatar
Marc
At first. Great stuff!

One question. In Firefox at slow transitions the whole thing gots laggy and the smoothness oof the transition is no longer present. I've read that a “image rotate” function could stop firefox from lagging. sth like “ $('.bild').rotate( 0.1 * f ); ” or similar. is it possible to get some code in there which provides a rotate?
sorry for the strange asking. i'm not a coder in the first place.
regards
marc
gravatar
Marc
Here's the link to the article where i've read this.
http://labor.99grad.de/flash/agentur/wiesbaden/jquery/firefox-ruckelt-bei-skalierung-eines-bildes/
(in German)
gravatar
Gene
Thank you for this fantastic code. I've only tested this in FF and Safari…

I've created a “viewport” image and layered it (via z-index), but I can't keep the images proportionally-scaled when I add px to the width and height. The correct proportional-scaling of the images (i.e., where the image resolution looks correct) can be seen at http://rgweber.com/testsite/slideshowtest2.php (i.e., without using px) but it doesn't fill my “viewport,” and the images that fill the viewport (but appear to be improperly scaled, using px) can be seen on the http://rgweber.com/testsite/slideshowtest.php page. Thoughts? Thank you!
gravatar
Gene
I haven't had a chance to test this yet in anything but FF and Safari, but I was able to resolve the issue by applying your coding method of sizing the canvas:
<canvas id=“kenburns” width=“640” height=“480”>)
as opposed to the CSS styling for size that I was using:
<canvas id=“kenburns” style=“width:640px; height:480px;”></canvas>

Thanks again for an excellent script!
gravatar
Gene
Shame on me for the typos; it should have been:

<canvas id=“kenburns” width=“640” height=“480”></canvas>
as opposed to the CSS styling for size that I was using:
<canvas id=“kenburns” style=“width:640px; height:480px;”></canvas>

Keeping my fingers crossed that my “viewport” code will also work in at least IE9+…
gravatar
perter sen
Hi,

Normal, it's slide from image 1 -> image 5
But if i want select image 2 by function :

render_image(image2, time_passed / display_time, time_passed / fade_time);

and it's show image 2 for me, but next image not have to image 3, it's image 5

I don't know why ?

You can help me ? Thank you so much !
gravatar
mrlonganh
Hi, I have a slideshow from image1->image5, and I want start slideshow from image 2. How can i do it ?
gravatar
Oscar Chinellato
Hi Will, nice code!
I see here some great suggestions to make your code even better. What about putting it all on github? That would really be awesome.
Thank you anyway!
gravatar
Jay
Very good Will !

But I met the INDEX_SIZE_ERR when set ZOOM: 0.8 or any other under 1.0.
In Safari and Opera (do not know about the IE).
But Chrome doing fine.
What could be wrong?

Thank you in advance!
gravatar
PJ Evans
Awesome! ONE thing I'd love to see, any way to make the pic order random?
gravatar
Eric
Hello,
First of all thank you for the detailed explanation.
I'm trying to get your script running but I can't. Even when I load this very page in chrome (Version 33.0.1750.117 m) or firefox (27.0.1), I can't get your sample included in this page to work.
Is there something I'm doing wrong and I should activate in FF or Chrome for this to work?
Thanks in advance
Eric
gravatar
Brian Reynolds
Would be great to get this on GitHub. I could see using it in multiple places and you should get credit for the good code.

We used it for a demo. I made a change to it to handle the case where there's only one image. I also made another change to make the looping optional. Would be happy to contribute pull-requests if you set up a github repo.
gravatar
Brian
Would be great to get this on GitHub. I could see using it in multiple places and you should get credit for the good code.

We used it for a demo. I made a change to it to handle the case where there's only one image. I also made another change to make the looping optional. Would be happy to contribute pull-requests if you set up a github repo.
gravatar
beasty
Great! All other image slideshow with kenburn effect use my processor(i5-2400) in firefox(and only in ff) on 100%. But with this code run it on 20%.
But, I have a question. Why convert this code the big pictures to small size? This is my big problem. How can I prevent this conversion?
Thanks a lot in advance
gravatar
John
Great work, is it possible to jump to a specified picture?
gravatar
JG
Trying to implement this code. What are the requirements? Does it need a certain version of PHP, etc? Do I need to make certain directories writable?

I've tried to get it working, but just end up with a grey image that when I right-click shows data:image/png;base64, and a long string of random characters.

Please help! Thanks!
gravatar
Andrés
Men, you are awesome. You are so great. It works so fucking good and it was so easy to install.

I hope everything go to you perfect because you deserve it. A lot of thanks!
gravatar
Andrés
Men, you are awesome. You are so great. It works so fucking good and it was so easy to install.

I hope everything go to you perfect because you deserve it. A lot of thanks!
gravatar
Darren Pula
Nice one. How can change link and text for every image.
gravatar
Freelancer

Hi! Really great code, good work! But trying to use it on a responsive site, it didn't resize images. So, I rewrote vars width and height, inside function render_image() like this:

function render_image(){
width = $canvas.width();
height = $canvas.height();

}

So, it worked fine :)
gravatar
Nicolas BANS

Hi,

How do you rewrite the render_image function to make it responsive ?

We cannot make your hack work.

gravatar
ikala ngita

Hello, thanks for the code, do you know why i get this error please ? Uncaught TypeError: Cannot read property 'initialized' of undefined

gravatar
Rick

My apologies if this is an ignorant question, but I'm wondering if it's possible (and how) to change the effect where all of the images zoom OUT, instead of alternating between zooming in and out? Any suggestions how this could be achieved?

gravatar
Will McGugan

Hi Rick. That's definitely possible, with a few tweaks of the code. Sorry, no time to give you guidance at the moment, but it shouldn't be all that difficult...