Creating An Image Zoom And Clip Effect With jQuery

Posted January 21, 2010 at 1:52 PM

Tags: Javascript / DHTML

A long time ago, I saw a really cool image effect in which I could select a region of an image and the image would automatically scale up to show the selected region. In that effect, the grainy, scaled-up image was then replaced with a hi-resolution version of the selected region. Years later, as I am getting more comfortable with jQuery, I wanted to see if I could recreate the effect. As an initial blog post, I'm just going to deal with the client-side image zoom; I'll save the hi-resolution image creation and client-side swap for a follow-up post.

 
 
 
 
 
 
 
 
 
 

To accomplish this image zooming effect with jQuery, I figured I would need some sort of "view" container for the image. If the image were positioned relative to this parent container, the image could then be moved around and resized within it. And, if the view remained a static size and clipped any overflow content, I could show only the selected portion of the image.

 
 
 
 
 
 
Creating A Zoom And Clip Effect With jQuery Requires A View Container And A Relative-Positioned Image. 
 
 
 

As I got started, the solution unfolded a lot quicker than I thought it would. The hardest part of it was figuring out how to adjust the image position for the zoom. In the following code, you can really see that I thought in a top-down manor, simply writing code as I went. In fact, if you start at the top and just read down, you probably won't have to jump around at all.

  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>Image Zoom With jQuery</title>
  • <style type="text/css">
  •  
  • #view {
  • border: 1px solid #333333 ;
  • overflow: hidden ;
  • position: relative ;
  • width: 400px ;
  • }
  •  
  • #image {
  • display: block ;
  • left: 0px ;
  • top: 0px ;
  • }
  •  
  • #zoom {
  • background-image: url( "./white_fade.png" ) ;
  • border: 1px solid #000000 ;
  • cursor: pointer ;
  • display: none ;
  • height: 200px ;
  • position: absolute ;
  • width: 200px ;
  • z-index: 100 ;
  • }
  •  
  • </style>
  • <script type="text/javascript" src="../jquery-1.4.js"></script>
  • <script type="text/javascript">
  •  
  • // When the WINDOW is ready, initialize. We are going with
  • // the window load rather than the document so that we
  • // know our image will be ready as well (complete with
  • // gettable dimentions).
  • $( window ).load(function(){
  •  
  • // First, let's get refernces to the elements we will
  • // be using.
  • var view = $( "#view" );
  • var image = $( "#image" );
  •  
  • // Create the ZOOM element - this will be added with
  • // Javascript since it's more of an "effect".
  • var zoom = $( "<a id='zoom'><span><br /></span></a>" );
  •  
  • // Before we start messing with the scripts, let's
  • // update the display to allow for the absolute
  • // positioning of the image and zoomer.
  •  
  • // Set an explicit height / width on the view based
  • // on the initial size of the image.
  • view.width( image.width() );
  • view.height( image.height() );
  •  
  • // Now that the view has an explicit width and height,
  • // we can change the displays for positioning.
  • image.css( "position", "absolute" );
  •  
  • // Set an exlicit height on the image (to make sure
  • // that some of the later calcualtions don't get
  • // messed up - I saw some irradic caculated-height
  • // behavior).
  • image.height( image.height() );
  •  
  • // Before we add the zoom square, we need it to match
  • // the aspect ratio of the image.
  • zoom.width( Math.floor( image.width() / 2 ) );
  • zoom.height( Math.floor( image.height() / 2 ) );
  •  
  • // Now, add the zoom square to the view.
  • view.append( zoom );
  •  
  •  
  • // ---------------------------------------------- //
  • // ---------------------------------------------- //
  •  
  •  
  • // Now that we have our UI set up physically, we need
  • // to bind the event handlers.
  •  
  • // We want to show and hide the zoom only when the
  • // user hovers over the view.
  • view.hover(
  • function( event ){
  • // Show the soom.
  • zoom.show();
  • },
  • function( event ){
  • // Hide the zoom.
  • zoom.hide();
  • }
  • );
  •  
  •  
  • // As the user mouses over the view, we can get the
  • // mouse coordinates in terms of the page; we need
  • // to be able to translate those into VIEW-based
  • // X and Y cooridates. As such, let's get the offset
  • // of the view as our base 0x0 coordinate.
  • //
  • // NOTE: We are doing this here so that we do it once,
  • // rather than every time the mouse moves.
  • viewOffset = view.offset();
  •  
  • // Get the jQuery-ed version of the window as we will
  • // need to access it's scroll offsets every time the
  • // mouse moves over the div.
  • //
  • // NOTE: This will change the change the refernce to
  • // "window" for all of the code in this closure.
  • var window = $( window );
  •  
  •  
  • // As the user moves across the view, we want to move
  • // the zoom square with them.
  • view.mousemove(
  • function( event ){
  • // Get the window scroll top; the mouse
  • // position is relative to the window, NOT
  • // the document.
  • var windowScrollTop = window.scrollTop();
  • var windowScrollLeft = window.scrollLeft();
  •  
  • // Translate the mouse X / Y into view-local
  • // coordinates that can be used to position
  • // the zoom box.
  • setZoomPosition(
  • Math.floor(
  • event.clientX - viewOffset.left + windowScrollLeft
  • ),
  • Math.floor(
  • event.clientY - viewOffset.top + windowScrollTop
  • )
  • );
  • }
  • );
  •  
  •  
  • // I position the zoom box within the view based on
  • // the given view-local mouse coordinates.
  • var setZoomPosition = function( mouseLeft, mouseTop ){
  • // Ideally, we want to keep the zoom box centered
  • // on the mouse. As such, we want the given mouse
  • // left and mouse top coordiantes to be in the
  • // middle of the zoom box.
  • var zoomLeft = (mouseLeft - (zoom.width() / 2));
  • var zoomTop = (mouseTop - (zoom.height() / 2))
  •  
  • // As we move the zoom box around, however, we
  • // never want it to go out of bounds of the view.
  •  
  • // Protect the top-left bounds.
  • zoomLeft = Math.max( zoomLeft, 0 );
  • zoomTop = Math.max( zoomTop, 0 );
  •  
  • // Protect the bottom-right bounds. Because the
  • // bottom and right need to take the dimensions
  • // of the zoom box into account, be sure to use
  • // the outer width to include the border.
  • zoomLeft = Math.min(
  • zoomLeft,
  • (view.width() - zoom.outerWidth())
  • );
  • zoomTop = Math.min(
  • zoomTop,
  • (view.height() - zoom.outerHeight())
  • );
  •  
  • // Position the zoom box in the bounds of the
  • // image view box.
  • zoom.css({
  • left: (zoomLeft + "px"),
  • top: (zoomTop + "px")
  • });
  • };
  •  
  •  
  • // Now that we have the mouse movements being tracked
  • // properly, we need to track the click on the zoom to
  • // zoom in the image on demand. To do that, however,
  • // we need to start storing some information with the
  • // image so we can manipulate it as needed.
  • image.data({
  • zoomFactor: (view.width() / zoom.width()),
  • zoom: 1,
  • top: 0,
  • left: 0,
  • width: image.width(),
  • height: image.height(),
  • originalWidth: image.width(),
  • originalHeight: image.height()
  • });
  •  
  •  
  • // Now, let's attach the click event handler to the
  • // zoom box.
  • zoom.click(
  • function( event ){
  • // First, prevent the default since this is
  • // not a navigational link.
  • event.preventDefault();
  •  
  • // Let's pass the position of the zoom box
  • // off to the function that is responsible
  • // for zooming the image.
  • zoomImage(
  • zoom.position().left,
  • zoom.position().top
  • );
  • }
  • );
  •  
  •  
  • // I take the zoom box coordinates and translate them
  • // into an actual image zoom based on the existing
  • // zoom and offset of the image.
  • //
  • // NOTE: We don't care about the dimensions of the
  • // zoom box itself as those should have already been
  • // properly translated into the zoom *factor*.
  • var zoomImage = function( zoomLeft, zoomTop ){
  • // Get a reference to the image data object so we
  • // don't need to keep retreiving it.
  • var imageData = image.data();
  •  
  • // Check to see if we have reached the max zoom
  • // or if the image is currently animating.
  • // If so, just return out.
  • if (
  • (imageData.zoom == 5) ||
  • (image.is( ":animated" ))
  • ){
  •  
  • // Zooming in beyond this is pointless (and
  • // can cause the browser to mis-render the
  • // image).
  • return;
  •  
  • }
  •  
  • // Scale the image up based on the zoom factor.
  • imageData.width =
  • (image.width() * imageData.zoomFactor);
  •  
  • imageData.height =
  • (image.height() * imageData.zoomFactor);
  •  
  • // Change the offset set data to re-position the
  • // 0,0 coordinate back up in the top left.
  • imageData.left =
  • ((imageData.left - zoomLeft) * imageData.zoomFactor);
  •  
  • imageData.top =
  • ((imageData.top - zoomTop) * imageData.zoomFactor);
  •  
  • // Increase the zoom.
  • imageData.zoom++;
  •  
  • // Animate the zoom.
  • image.animate(
  • {
  • width: imageData.width,
  • height: imageData.height,
  • left: imageData.left,
  • top: imageData.top
  • },
  • 500
  • );
  • };
  •  
  •  
  • // I reset the image zoom.
  • var resetZoom = function(){
  • // Get a reference to the image data object so we
  • // don't need to keep retreiving it.
  • var imageData = image.data();
  •  
  • // Reset the image data.
  • imageData.zoom = 1;
  • imageData.top = 0;
  • imageData.left = 0;
  • imageData.width = imageData.originalWidth;
  • imageData.height = imageData.originalHeight;
  •  
  • // Animate the zoom.
  • image.animate(
  • {
  • width: imageData.width,
  • height: imageData.height,
  • left: imageData.left,
  • top: imageData.top
  • },
  • 300
  • );
  • };
  •  
  •  
  • // As a final step, to make sure that the image can
  • // be zoomed out, bind the mousedown to the document.
  • $( document ).mousedown(
  • function( event ){
  • // Check to see if the view is in the event
  • // bubble chain for the mouse down. If it is,
  • // then this click was in the view or its
  • // child elements.
  • var closestView = $( event.target ).closest( "#view" );
  •  
  • // Check to see if this mouse down was in the
  • // image view.
  • if (!closestView.size()){
  •  
  • // The view was not found in the chain.
  • // This was clicked outside of the view.
  • // Reset the image zoom.
  • resetZoom();
  •  
  • }
  • }
  • );
  •  
  • });
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <h1>
  • Image Zoom And Clip With jQuery
  • </h1>
  •  
  • <div id="view">
  • <img id="image" src="./girl.jpg" width="400" />
  • </div>
  •  
  • </body>
  • </html>

Right now, I am manually controlling the position of the "zoomer" (the white image selection box). I am sure that I could have included the jQuery UI library and made use of the "draggable" functionality; but I wanted to keep this as straightforward as possible. The code would have been shorter with the draggable library; but, to be honest, writing out the code for it really got me to think about the dynamics of the UI interaction and ultimately, helped me think through the solution more effectively.

Every time that I try do something complicated with jQuery, I am always blow away by how much less complex this Javascript library makes things. In this exploration, the data storage, positional, and dimensional methods really just came through in a huge way! jQuery is so badass that it made the Math the most complex aspect of the problem. Isn't that wild? In a follow-up post, I hope to have the client interacting with the server to pull down a pre-clipped, hi-resolution image of the selected region.




Reader Comments

Jan 22, 2010 at 3:39 AM // reply »
14 Comments

Nice job dude :)

You could also implement a very nice crop effect on top of this (I've personally used jCrop which I found was awesome when implemented alongside CF's image tags) to create an "uber" image manipulation package.

http://deepliquid.com/content/Jcrop.html

In fact I might just do that myself :P


Jan 22, 2010 at 11:21 AM // reply »
8,824 Comments

Here's a quick follow up on that post - this time, adding a server-side component to create and serve up hi-resolution images on demand:

http://www.bennadel.com/blog/1824-Creating-An-Image-Zoom-And-Clip-Effect-With-jQuery-And-ColdFusion.htm


Jan 25, 2010 at 3:30 AM // reply »
1 Comments

Is this possible with regular CSS or just with Java?

Thanks in advance.


Jan 25, 2010 at 9:21 PM // reply »
8,824 Comments

@Sammy,

Not that I know of - there's so much interactivity. Is there a particular reason you would want to have CSS do this?


Apr 30, 2010 at 1:07 PM // reply »
1 Comments

Hello Ben,

Nice job, i've put this script on my website but there's a problem, when the div#view is put at the bottom of any pages. After scrolling on my browser to see the div#view that contain the image. The div#zoom is not working correctly anymore and is blocked at the top of the div#view.

Hope it helps to improve your script. Sorry for my bad english ;-)


May 18, 2010 at 12:48 AM // reply »
1 Comments

Hi ben thanks for all this nice articals and it is realy helpful. i have one problem with using (Creating An Image Zoom And Clip Effect With jQuery) topic when i run the project zoom selector is stuck on the tope of the image it is moving horizentaly on the heading of the image not on other part of image and while using is along with the image it is working fine and Link has also post his comment on this topic he has the same problem so is there any solution for this porblem.

Thanks


May 18, 2010 at 8:46 PM // reply »
8,824 Comments

@Link, @Amin,

Hmmm, not exactly sure. Looking at the code, I appear to be making use of clientX, clientY. After posting this, I found out that jQuery actually standardized on pageX and pageY:

http://www.bennadel.com/blog/1869-jQuery-Mouse-Events-PageX-Y-vs-ClientX-Y.htm

It's possible that this difference between these two sets of variables is what is causing the problem on pages that have a scroll. In fact, that kind of makes sense - clientX/Y is relative to the view frame and pageX/Y is relative to the top/left of the full document.

It's all a learning experience :)


FS
Aug 26, 2010 at 8:39 PM // reply »
1 Comments

This king of effect has been implemented by ajax-zoom with PHP and jQuery. It uses image tiles and can be viewed here: http://www.ajax-zoom.com


Sep 4, 2010 at 11:16 AM // reply »
8,824 Comments

@FS,

Dude, that one looks really awesome!


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Formatting: <strong>bold</strong> <em>italic<em>







  • Help Wanted - Find Your Next ColdFusion Job
Recent Blog Comments
Sep 5, 2010 at 6:35 PM
Muscle: Confessions Of An Unlikely Bodybuilder By Samuel Wilson Fussell
@Ben, Certainly will/ Thanks Sean ... read »
Sep 5, 2010 at 6:26 PM
Experimenting With HTML5's Cache Manifest For Offline Web Applications
@Ben, Yes, I am using Firefox Portable. At the moment I run a portable web server on the stick which holds and serves all files. The good thing is, I can run PHP pages on the stick to do requests to ... read »
Sep 5, 2010 at 5:05 PM
Ask Ben: Finding XML Nodes That Have Children With The Given Case-Insensitive Phrase
@Murray, Good point on the clarification. ... read »
Sep 5, 2010 at 4:40 PM
Ask Ben: Finding XML Nodes That Have Children With The Given Case-Insensitive Phrase
Actually, for the benefit of anyone reading this who might want to make sense of the question post, the first <td> had a bold tag surrounding the numeral 6. So, the problem was that the xmlSear ... read »
Sep 5, 2010 at 4:35 PM
Ask Ben: Finding XML Nodes That Have Children With The Given Case-Insensitive Phrase
Thanks Ben. Much appreciated. ... read »
Sep 5, 2010 at 3:39 PM
jQuery forEach() Experiment For Branch-Wise Implicit Iteration
@Sereal, Wow - what a super flattering thing to say :) I really appreciate that! I'm so happy that this stuff is providing value for you. ... read »
Sep 5, 2010 at 3:32 PM
Escaping Form Values - Understanding The ColdFusion htmlEditFormat() Life Cycle
@Ben, There's also a performance benefit to escaping on database insert since it only needs to be done ONCE - when inserting. When you escape on output, this needs to be done every time you output ... read »
Sep 5, 2010 at 3:30 PM
XML Building / Parsing / Traversing Speed In ColdFusion
@Don, I've played around with a couple of approaches to dealing with XML documents that are too large to be parsed in one shot. In one, approach, I use Regular Expression to try and parse one tag a ... read »