Dynamic Image Reflection
Posted on February 8th, 2009 in JavaScript, jQuery, Programming, SVG, Web Development | No Comments »
Todays post is about creating a mirrored effect on images appearing on a website. Looking around, there are already a few people who have attempted to do this, however, I would like to tackle the problem myself and provide an in depth post about how to actually do it. At the end of this post we should have a fully functioning jQuery plugin which allows us to mirror images. We can approach this two ways, using Canvas or SVG. I have opted to use Canvas, however I did create a neat SVG file which is able to mirror arbitrary images, passed in by a query parameter. The first part if this post will address creating a reflection of an image. Once we have the ability to do this, we can roll it into a jQuery plugin to provide reflections for images which we specify.
Creating a Reflection
The approach I have taken for reflection uses the HTML canvas element. The Canvas element was originally introduced by Apple into their WebKit component and allows for a drawable region in HTML. The Canvas element exposes a full set of drawing functions to the developer to draw inside of it. We can utilise these functions to mirror an image, complete with a 'fading' effect. Assuming we already have a canvas DOM element, we can retrieve the 2d drawing context with a call to getContext('2d') on the DOM element. This can then be used to draw to the canvas via a published API. If you wish to know more the Mozilla Developer Center has published some tutorials on using Canvas.
In order to achieve our mirror effect, we must 'flip' the vertical of the image. We can achieve this using the scale function exposed to us in the canvas API, context.scale(1, -1). This will effectively 'flip' what we draw, or, more technically add a 'flip' transform to the translation matrix of our canvas element. The Mozilla Developer Center again provides some useful resources regarding the use of scale. However, this documentation states that scale() may only take positive arguments.
With a little experimentation however, I revealed that it actually works as expected for negative augments. Quite naturally I followed this up with some source code digging. For WebKit, in CanvasRenderingContext2D.cpp line 326 we see the definition for the scale function, as provided to us by the 2d drawing context in WebKit. All this does is add a scale transformation to the current transformation matrix of the canvas. If this is negative, it will cause a 'flip' of the image.
In Firefox we find in nsCanvasRenderingContext2D.cpp on line 877 the definition for scale. This simply calls cairo_scale, with the supplied parameters. Cairo is the Firefox graphics backend, so it makes quite a bit of sense that this actually happens and cairo_scale accepts negative parameters as well.
But I digress. We may achieve a reflected image with the following code (assuming we have the 2d context from the canvas (canvas) and an image we want to mirror (image)):
Creating a Reflection
The approach I have taken for reflection uses the HTML canvas element. The Canvas element was originally introduced by Apple into their WebKit component and allows for a drawable region in HTML. The Canvas element exposes a full set of drawing functions to the developer to draw inside of it. We can utilise these functions to mirror an image, complete with a 'fading' effect. Assuming we already have a canvas DOM element, we can retrieve the 2d drawing context with a call to getContext('2d') on the DOM element. This can then be used to draw to the canvas via a published API. If you wish to know more the Mozilla Developer Center has published some tutorials on using Canvas.
In order to achieve our mirror effect, we must 'flip' the vertical of the image. We can achieve this using the scale function exposed to us in the canvas API, context.scale(1, -1). This will effectively 'flip' what we draw, or, more technically add a 'flip' transform to the translation matrix of our canvas element. The Mozilla Developer Center again provides some useful resources regarding the use of scale. However, this documentation states that scale() may only take positive arguments.
With a little experimentation however, I revealed that it actually works as expected for negative augments. Quite naturally I followed this up with some source code digging. For WebKit, in CanvasRenderingContext2D.cpp line 326 we see the definition for the scale function, as provided to us by the 2d drawing context in WebKit. All this does is add a scale transformation to the current transformation matrix of the canvas. If this is negative, it will cause a 'flip' of the image.
In Firefox we find in nsCanvasRenderingContext2D.cpp on line 877 the definition for scale. This simply calls cairo_scale, with the supplied parameters. Cairo is the Firefox graphics backend, so it makes quite a bit of sense that this actually happens and cairo_scale accepts negative parameters as well.
But I digress. We may achieve a reflected image with the following code (assuming we have the 2d context from the canvas (canvas) and an image we want to mirror (image)):
canvas.translate(0, image_height);canvas.scale(1, -1); canvas.drawImage(image, 0, 0, image_width, image_height); canvas.restore();This will render a mirrored form of the image, flipped on the horizontal axis. However, many mirror effects also feature a gradual fading of the mirrored image. In order to add this we add a mask on top of our flipped image, using the 'destination-in' globalCompositeOperation:
canvas.globalCompositeOperation = "destination-in"; var gradient = canvas.createLinearGradient(0, 0, 0, height); gradient.addColorStop(1, "rgba(255, 255, 255, 1.0)"); gradient.addColorStop(0, "rgba(255, 255, 255, 0.0)"); canvas.fillStyle = gradient; canvas.rect(0, 0, width, height); canvas.fill();This now completes our mirror image effect, fading out to black as the distance from the horizontal flip axis increases. In order to make this functionality, cleaner, more modular & reusable and thus more useful to us, we make this functionality available as a jQuery plugin. The goal should be that one only needs to call $('.mirror').mirror() to mirror all images selected by the selector (in this case, all images with a class of 'mirror'). In order to achieve this, we turn our approach into a jQuery extension, and add the function mirror(). As I have explained the general method, I will provide a simple example for a jQuery plugin:
jQuery.fn.mirror = function() {
/*
* jQuery functions should return 'this', to conform to the fluent interface.
* We also iterate through all img elements.
*/
return this.find("img").each(function(i, e) {
/* Our jQuery Element */
var je = jQuery(e);
/* For a number of calculations, we require the width and the height of the image */
var width = je.attr('width');
var height = je.attr('height');
/*
* Create a div to contain our original image
* and its reflection.
*/
var div = jQuery("<div></div>")
.css('position', 'relative')
.css('width', width)
.css('height', height * 2);
/* create a canvas for our relection */
var canvas = jQuery("<canvas></canvas>")
.attr('width', width)
.attr('height', height)
.css('width', width)
.css('height', height)
.css('position', 'absolute')
.css('left', 0)
.css('top', height);
/*
* Replace the image with our div.
* This line is magic for taking the image out
* putting the image &amp; canvas in the div,
* then putting the div in the images place.
*/
div.append(je.replaceWith(div)).append(canvas);
/*
* Now we must do our voodo with canvas to make a
* reflection of the image
*/
canvas = canvas.get(0).getContext('2d');
je.load(function() {
canvas.translate(0, height);
canvas.scale(1, -1);
canvas.drawImage(e, 0, 0, width, height);
canvas.restore();
canvas.globalCompositeOperation = "destination-in";
var gradient = canvas.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(1, "rgba(255, 255, 255, 1.0)");
gradient.addColorStop(0, "rgba(255, 255, 255, 0.0)");
canvas.fillStyle = gradient;
canvas.rect(0, 0, width, height);
canvas.fill();
});
});
}