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:
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 );
https://gist.github.com/838785
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.
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 :)
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?
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.
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!
@Will - that jitter in Chrome is totally annoying. It defeats the whole purpose. How do we fix that?
Maybe one day I'll try implementing it in WebGL. I think that would guarantee high quality across browsers.
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
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é
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)
I've noticed this on safari, but on opera it looks much worse…
thanks,
Thanking you
This looks really nice when it's slowed down.
Thanks 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!
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.
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:
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…!
Cheers
Dave
Any advice on how to lower the crash rate here?
Thanks Will.
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
http://labor.99grad.de/flash/agentur/wiesbaden/jquery/firefox-ruckelt-bei-skalierung-eines-bildes/
(in German)
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!
<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!
<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+…
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 !
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!
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!
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
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.
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.
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
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!
I hope everything go to you perfect because you deserve it. A lot of thanks!
I hope everything go to you perfect because you deserve it. A lot of thanks!
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 :)
Hi,
How do you rewrite the render_image function to make it responsive ?
We cannot make your hack work.
Hello, thanks for the code, do you know why i get this error please ? Uncaught TypeError: Cannot read property 'initialized' of undefined
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?
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...