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 […]

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.

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

(function($){$.fn.kenburns=function(options){var$canvas=$(this);varctx=this[0].getContext('2d');varstart_time=null;varwidth=$canvas.width();varheight=$canvas.height();varimage_paths=options.images;vardisplay_time=options.display_time||7000;varfade_time=Math.min(display_time/2,options.fade_time||1000);varsolid_time=display_time-(fade_time*2);varfade_ratio=fade_time-display_timevarframes_per_second=options.frames_per_second||30;varframe_time=(1/frames_per_second)*1000;varzoom_level=1/(options.zoom||2);varclear_color=options.background_color||'#000000';varimages=[];$(image_paths).each(function(i,image_path){images.push({path:image_path,initialized:false,loaded:false});});functionget_time(){vard=newDate();returnd.getTime()-start_time;}functioninterpolate_point(x1,y1,x2,y2,i){// Finds a point between two other pointsreturn{x:x1+(x2-x1)*i,y:y1+(y2-y1)*i}}functioninterpolate_rect(r1,r2,i){// Blend one rect in to anothervarp1=interpolate_point(r1[0],r1[1],r2[0],r2[1],i);varp2=interpolate_point(r1[2],r1[3],r2[2],r2[3],i);return[p1.x,p1.y,p2.x,p2.y];}functionscale_rect(r,scale){// Scale a rect around its centervarw=r[2]-r[0];varh=r[3]-r[1];varcx=(r[2]+r[0])/2;varcy=(r[3]+r[1])/2;varscalew=w*scale;varscaleh=h*scale;return[cx-scalew/2,cy-scaleh/2,cx+scalew/2,cy+scaleh/2];}functionfit(src_w,src_h,dst_w,dst_h){// Finds the best-fit rect so that the destination can be coveredvarsrc_a=src_w/src_h;vardst_a=dst_w/dst_h;varw=src_h*dst_a;varh=src_h;if(w>src_w){varw=src_w;varh=src_w/dst_a;}varx=(src_w-w)/2;vary=(src_h-h)/2;return[x,y,x+w,y+h];}functionget_image_info(image_index,load_callback){// Gets information structure for a given index// Also loads the image asynchronously, if requiredvarimage_info=images[image_index];if(!image_info.initialized){varimage=newImage();image_info.image=image;image_info.loaded=false;image.onload=function(){image_info.loaded=true;variw=image.width;varih=image.height;varr1=fit(iw,ih,width,height);;varr2=scale_rect(r1,zoom_level);varalign_x=Math.floor(Math.random()*3)-1;varalign_y=Math.floor(Math.random()*3)-1;align_x/=2;align_y/=2;varx=r2[0];r2[0]+=x*align_x;r2[2]+=x*align_x;vary=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;}returnimage_info;}functionrender_image(image_index,anim,fade){// Renders a frame of the effectif(anim>1){return;}varimage_info=get_image_info(image_index);if(image_info.loaded){varr=interpolate_rect(image_info.r1,image_info.r2,anim);vartransparency=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();}}}functionclear(){// Clear the canvasctx.save();ctx.globalAlpha=1;ctx.fillStyle=clear_color;ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height);ctx.restore();}functionupdate(){// Render the next framevarupdate_time=get_time();vartop_frame=Math.floor(update_time/(display_time-fade_time));varframe_start_time=top_frame*(display_time-fade_time);vartime_passed=update_time-frame_start_time;functionwrap_index(i){return(i+images.length)%images.length;}if(time_passed<fade_time){varbottom_frame=top_frame-1;varbottom_frame_start_time=frame_start_time-display_time+fade_time;varbottom_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 itvarpreload_image=wrap_index(top_frame+1);get_image_info(preload_image);}// Pre-load the first two images then start a timerget_image_info(0,function(){get_image_info(1,function(){start_time=get_time();setInterval(update,frame_time);})});};})(jQuery);

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.

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.

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.

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

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.

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.

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.

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

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.

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

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.regardsmarc

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!

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>

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 advanceEric

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.

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.

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

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?