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 );
This blog post was posted to It's All Geek to Me on Saturday February 26th, 2011 at 5:16PM
 

58 Responses to "Ken Burns effect with Javascript and Canvas"

  • Rob
    February 26th, 2011, 7:10 p.m.

    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.

  • February 26th, 2011, 7:20 p.m.

    @Rob I would assume that it's for compatibility sake. CSS transitions are still buggy at best!

  • February 26th, 2011, 7:28 p.m.

    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.

  • February 26th, 2011, 10:16 p.m.

    Looks great on safari.

  • February 27th, 2011, 5:52 a.m.

    You should probably use requestAnimationFrame where available instead of the current interval trick for animating. Looks cool otherwise.

    https://gist.github.com/838785 [gist.github.com]

  • February 28th, 2011, 3:21 p.m.

    Excelent example! What about perfomrmance? Does incrising the number of images decrease the performance of the browser?

  • February 28th, 2011, 3:29 p.m.

    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.

  • Tom B
    February 28th, 2011, 3:51 p.m.

    Looks good in IE9

  • Njunik
    March 11th, 2011, 5:44 p.m.

    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 :)

  • March 11th, 2011, 5:52 p.m.

    Njunik, I'm guessing that either your running it on <IE9 (use http://excanvas.sourceforge.net/ [excanvas.sourceforge.net] if that's the case), or you called kenburns with a selector for something other than a canvas tag.

  • njunik
    March 11th, 2011, 6:14 p.m.

    @Will Ok. It was my error. With canvas tag it's all ok. Tnx

  • April 6th, 2011, 2:13 p.m.

    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?

  • April 6th, 2011, 6:11 p.m.

    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.

  • James
    August 11th, 2011, 5:05 p.m.

    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?

  • Konrad
    November 17th, 2011, 3:01 p.m.

    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.

  • Sebas
    December 5th, 2011, 10:36 a.m.

    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!

  • January 31st, 2012, 1:43 p.m.

    @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?

  • February 6th, 2012, 2:50 p.m.

    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.

  • corsaro
    February 27th, 2012, 12:24 a.m.

    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

  • March 8th, 2012, 4:27 a.m.

    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é

  • Stafford
    April 26th, 2012, 5:22 p.m.

    Merci beaucoup, depuis le temps que je chercher ce module

  • Francis
    June 30th, 2012, 10:42 p.m.

    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)

  • jitter
    October 2nd, 2012, 12:09 p.m.

    Is there a way to remove that jitter effects?
    I've noticed this on safari, but on opera it looks much worse…

    thanks,

  • October 2nd, 2012, 12:18 p.m.

    I'm afraid not. It depends on the accuracy of how the browser scales images.

  • Raghavendra
    October 4th, 2012, 6:37 p.m.

    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

  • Urbanaut
    October 12th, 2012, 1:42 p.m.

    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.

  • Jo
    October 17th, 2012, 11:36 a.m.

    How do I make the gallery resize automatically to fit all windows irrespective of the resolution of the machine…

  • Ian
    October 21st, 2012, 6:39 p.m.

    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!

  • eAnka
    November 28th, 2012, 8:32 a.m.

    Absolutely great! That's what I was looking for! Thank you!

  • Gabi
    December 4th, 2012, 10:25 p.m.

    Hi, can you please tell me how can I give the images from code behind? in some kind of collection. Thanks

  • December 9th, 2012, 11:26 a.m.

    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.

  • Jordi Alhambra
    December 9th, 2012, 11:28 a.m.

    Solved, please delete prev question.

  • Tushar
    January 11th, 2013, 5:15 a.m.

    thanks

  • January 11th, 2013, 8:47 p.m.

    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…!

  • January 17th, 2013, 11:02 p.m.

    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

  • January 17th, 2013, 11:11 p.m.

    @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…

  • January 25th, 2013, 12:41 p.m.

    Excellent job. is there any chance to random image sort ?

  • Urbanaut
    March 15th, 2013, 1:16 a.m.

    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.

  • March 15th, 2013, 11:04 a.m.

    Not sure there is much that can be done about that I'm afraid!

  • Marc
    March 18th, 2013, 3:45 p.m.

    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

  • Marc
    March 18th, 2013, 3:47 p.m.
  • Gene
    April 11th, 2013, 7:09 p.m.

    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!

  • Gene
    April 12th, 2013, 5:54 a.m.

    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!

  • Gene
    April 12th, 2013, 6:01 a.m.

    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+…

  • perter sen
    May 13th, 2013, 5:14 a.m.

    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 !

  • mrlonganh
    May 13th, 2013, 10:29 a.m.

    Hi, I have a slideshow from image1->image5, and I want start slideshow from image 2. How can i do it ?

  • May 20th, 2013, 3:23 p.m.

    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!

  • Jay
    October 6th, 2013, 12:43 p.m.

    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!

  • December 16th, 2013, 7:36 a.m.

    Awesome! ONE thing I'd love to see, any way to make the pic order random?

  • February 24th, 2014, 7:50 a.m.

    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

  • March 24th, 2014, 10:45 a.m.

    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.

  • March 24th, 2014, 10:47 a.m.

    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.

  • beasty
    March 28th, 2014, 9:34 a.m.

    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

  • John
    May 25th, 2014, 3:01 a.m.

    Great work, is it possible to jump to a specified picture?

  • JG
    July 14th, 2014, 5:31 p.m.

    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!

  • Andrés
    October 1st, 2014, 8:49 p.m.

    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!

  • Andrés
    October 1st, 2014, 9:30 p.m.

    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!

  • December 11th, 2014, 11:22 p.m.

    Nice one. How can change link and text for every image.

Leave a Comment

You can use bbcode in the comment: e.g. [b]This is bold[/b], [url]http://www.willmcgugan.com[/url], [code python]import this[/code]
Preview Posting...
Previewing comment, please wait a moment...
Will McGugan

My name is Will McGugan. I am an unabashed geek, an author, a hacker and a Python expert – amongst other things!

You are reading my tech blog. See the homepage for my other blogs.

Search for Posts
Possibly related posts
Tags
Popular Tags
 
Archives
2013
 
Recent Comments
Nice one. How can change link and text for every image.
Beautiful. Just beautiful. Thank you.
- Tyler Troy on Going sub-pixel with PyGame
Sorry for the double comment my browser is very slow.
Hi Will I get the following error when i try to run simpleopengl.py. Traceback (most recent call last): File firstopengl.py, ...
Hi Will I get the following error when i try to run simpleopengl.py. Traceback (most recent call last): File firstopengl.py, ...
 
© 2008 Will McGugan.

A technoblog blog, design by Will McGugan