This website uses cookies to personalise ads and to analyse traffic ok
web design

jQuery image panning

Image panning with animation easing that works on mouse movement with css and jQuery.

Image panning with animation easing

The markup

Simple markup: an image inside a div

<div class="content">
  <img src="img.jpg" />
</div>

The CSS

.content{
  width: 800px;
  height: 600px;
  overflow: hidden;
}

.content img{
  opacity: 0;
  transition: opacity .6s linear .8s;
}

.content img.loaded{ opacity: 1; }

.img-pan-container, .img-pan-container img{ -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; }

.img-pan-container{
  position: relative;
  overflow: hidden;
  cursor: crosshair;
  height: 100%;
  width: 100%;
}

.img-pan-container img{
  -webkit-transform: translateZ(0); -ms-transform: translateZ(0); transform: translateZ(0);
  position: absolute;
  top: 0;
  left: 0;
}

The javascript

Can be placed inside the head tag or at the bottom of the document right before the closing body tag

More code, better animation/performance (60 fps)

<!-- jQuery -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>

<!-- JS -->
<script>
  (function($){
    
    $(document).ready(function(){
      //call imagePanning fn when DOM is ready
      $(".content img").imagePanning();
    });
    
    //imagePanning fn
    $.fn.imagePanning=function(){
      var init="center",
        speed=800, //animation/tween speed
        //custom js tween
        _tweenTo=function(el,prop,to,duration,easing,overwrite){
          if(!el._mTween){el._mTween={top:{},left:{}};}
          var startTime=_getTime(),_delay,progress=0,from=el.offsetTop,elStyle=el.style,_request,tobj=el._mTween[prop];
          if(prop==="left"){from=el.offsetLeft;}
          var diff=to-from;
          if(overwrite!=="none"){_cancelTween();}
          _startTween();
          function _step(){
            progress=_getTime()-startTime;
            _tween();
            if(progress>=tobj.time){
              tobj.time=(progress>tobj.time) ? progress+_delay-(progress-tobj.time) : progress+_delay-1;
              if(tobj.time<progress+1){tobj.time=progress+1;}
            }
            if(tobj.time<duration){tobj.id=_request(_step);}
          }
          function _tween(){
            if(duration>0){
              tobj.currVal=_ease(tobj.time,from,diff,duration,easing);
              elStyle[prop]=Math.round(tobj.currVal)+"px";
            }else{
              elStyle[prop]=to+"px";
            }
          }
          function _startTween(){
            _delay=1000/60;
            tobj.time=progress+_delay;
            _request=(!window.requestAnimationFrame) ? function(f){_tween(); return setTimeout(f,0.01);} : window.requestAnimationFrame;
            tobj.id=_request(_step);
          }
          function _cancelTween(){
            if(tobj.id==null){return;}
            if(!window.requestAnimationFrame){clearTimeout(tobj.id);
            }else{window.cancelAnimationFrame(tobj.id);}
            tobj.id=null;
          }
          function _ease(t,b,c,d,type){
            var ts=(t/=d)*t,tc=ts*t;
            return b+c*(0.499999999999997*tc*ts + -2.5*ts*ts + 5.5*tc + -6.5*ts + 4*t);
          }
          function _getTime(){
            if(window.performance && window.performance.now){
              return window.performance.now();
            }else{
              if(window.performance && window.performance.webkitNow){
                return window.performance.webkitNow();
              }else{
                if(Date.now){return Date.now();}else{return new Date().getTime();}
              }
            }
          }
        };
      return this.each(function(){
        var $this=$(this),timer,dest;
        if($this.data("imagePanning")) return;
        $this.data("imagePanning",1)
          //create markup
          .wrap("<div class='img-pan-container' />")
          .after("<div class='resize' style='position:absolute; width:auto; height:auto; top:0; right:0; bottom:0; left:0; margin:0; padding:0; overflow:hidden; visibility:hidden; z-index:-1'><iframe style='width:100%; height:0; border:0; visibility:visible; margin:0' /><iframe style='width:0; height:100%; border:0; visibility:visible; margin:0' /></div>")
          //image loaded fn
          .one("load",function(){
            setTimeout(function(){ $this.addClass("loaded").trigger("mousemove",1); },1);
          }).each(function(){ //run load fn even if cached
            if(this.complete) $(this).load();
          })
          //panning fn
          .parent().on("mousemove touchmove MSPointerMove pointermove",function(e,p){
            var cont=$(this);
            e.preventDefault();
            var contH=cont.height(),contW=cont.width(),
              isTouch=e.type.indexOf("touch")!==-1,isPointer=e.type.indexOf("pointer")!==-1,
              evt=isPointer ? e.originalEvent : isTouch ? e.originalEvent.touches[0] || e.originalEvent.changedTouches[0] : e,
              coords=[
                !p ? evt.pageY-cont.offset().top : init==="center" ? contH/2 : 0,
                !p ? evt.pageX-cont.offset().left : init==="center" ? contW/2 : 0
              ];
            dest=[Math.round(($this.outerHeight(true)-contH)*(coords[0]/contH)),Math.round(($this.outerWidth(true)-contW)*(coords[1]/contW))];
          })
          //resize fn
          .find(".resize iframe").each(function(){
            $(this.contentWindow || this).on("resize",function(){
              $this.trigger("mousemove",1);
            });
          });
        //panning animation 60FPS
        if(timer) clearInterval(timer);
        timer=setInterval(function(){
          _tweenTo($this[0],"top",-dest[0],speed);
          _tweenTo($this[0],"left",-dest[1],speed);
        },16.6);
      });
    }
    
  })(jQuery);
</script>

The js code above is more than few lines, simply because it includes a vanilla custom javascript tween for much better performance and smoother animations.

Less code, average animation/performance (30 fps)

  $.fn.imagePanning=function(){
    var init="center",
      speed=800; //animation/tween speed
    //add custom easing for jquery animation
    $.extend($.easing,{
      pan:function(x,t,b,c,d){
        return -c * ((t=t/d-1)*t*t*t - 1) + b;
      }
    });
    return this.each(function(){
      var $this=$(this),timer,dest;
      if($this.data("imagePanning")) return;
      $this.data("imagePanning",1)
        //create markup
        .wrap("<div class='img-pan-container' />")
        .after("<div class='resize' style='position:absolute; width:auto; height:auto; top:0; right:0; bottom:0; left:0; margin:0; padding:0; overflow:hidden; visibility:hidden; z-index:-1'><iframe style='width:100%; height:0; border:0; visibility:visible; margin:0' /><iframe style='width:0; height:100%; border:0; visibility:visible; margin:0' /></div>")
        //image loaded fn
        .one("load",function(){
          setTimeout(function(){ $this.addClass("loaded").trigger("mousemove",1); },1);
        }).each(function(){ //run load fn even if cached
          if(this.complete) $(this).load();
        })
        //panning fn
        .parent().on("mousemove touchmove MSPointerMove pointermove",function(e,p){
          var cont=$(this);
          e.preventDefault();
          var contH=cont.height(),contW=cont.width(),
            isTouch=e.type.indexOf("touch")!==-1,isPointer=e.type.indexOf("pointer")!==-1,
            evt=isPointer ? e.originalEvent : isTouch ? e.originalEvent.touches[0] || e.originalEvent.changedTouches[0] : e,
            coords=[
              !p ? evt.pageY-cont.offset().top : init==="center" ? contH/2 : 0,
              !p ? evt.pageX-cont.offset().left : init==="center" ? contW/2 : 0
            ];
          dest=[Math.round(($this.outerHeight(true)-contH)*(coords[0]/contH)),Math.round(($this.outerWidth(true)-contW)*(coords[1]/contW))];
        })
        //resize fn
        .find(".resize iframe").each(function(){
          $(this.contentWindow || this).on("resize",function(){
            $this.trigger("mousemove",1);
          });
        });
      //panning animation 30 FPS
      if(!timer){
        timer=setInterval(function(){
          $this.stop().animate({"top":-dest[0],"left":-dest[1]},speed,"pan");
        },33.3);
      }
    });
  }

Image panning without animation

imagePanning function

  $.fn.imagePanning=function(){
    var init="center";
    return this.each(function(){
      var $this=$(this);
      if($this.data("imagePanning")) return;
      $this.data("imagePanning",1)
        //create markup
        .wrap("<div class='img-pan-container' />")
        .after("<div class='resize' style='position:absolute; width:auto; height:auto; top:0; right:0; bottom:0; left:0; margin:0; padding:0; overflow:hidden; visibility:hidden; z-index:-1'><iframe style='width:100%; height:0; border:0; visibility:visible; margin:0' /><iframe style='width:0; height:100%; border:0; visibility:visible; margin:0' /></div>")
        //image loaded fn
        .one("load",function(){
          setTimeout(function(){ $this.addClass("loaded").trigger("mousemove",1); },1);
        }).each(function(){ //run load fn even if cached
          if(this.complete) $(this).load();
        })
        //panning fn
        .parent().on("mousemove touchmove MSPointerMove pointermove",function(e,p){
          var cont=$(this);
          e.preventDefault();
          var contH=cont.height(),contW=cont.width(),
            isTouch=e.type.indexOf("touch")!==-1,isPointer=e.type.indexOf("pointer")!==-1,
            evt=isPointer ? e.originalEvent : isTouch ? e.originalEvent.touches[0] || e.originalEvent.changedTouches[0] : e,
            coords=[
              !p ? evt.pageY-cont.offset().top : init==="center" ? contH/2 : 0,
              !p ? evt.pageX-cont.offset().left : init==="center" ? contW/2 : 0
            ],
            dest=[Math.round(($this.outerHeight(true)-contH)*(coords[0]/contH)),Math.round(($this.outerWidth(true)-contW)*(coords[1]/contW))];
          $this.css({"top":-dest[0],"left":-dest[1]});
        })
        //resize fn
        .find(".resize iframe").each(function(){
          $(this.contentWindow || this).on("resize",function(){
            $this.trigger("mousemove",1);
          });
        });
    });
  }

As with everything, you can change it, optimize it and pretty much use it anyway and anywhere you like πŸ™‚

 

FAQ


93 Comments

Post a comment

Comments pages: 1 2

  1. heather
    Posted on April 27, 2016 at 07:26 Permalink

    hi, great script! works well. i was wondering if there was a way so that on mouse out, the image could go back to being centered?

    Reply
    • malihu
      Posted on April 27, 2016 at 16:16 Permalink

      Yes but you need to add/trigger the js event. The easiest way would be to just extend the imagePanning function call like this:

      //call imagePanning fn when DOM is ready $(".content img").imagePanning().parent().on("mouseleave touchend MSPointerleave pointerleave",function(){ $(this).trigger("mousemove",1); });

      Reply
  2. zenith
    Posted on January 30, 2016 at 16:46 Permalink

    Is it possible to see image in thumbnail size and after mouse over it scale and zoom image to real size with animation then start moving, after mouse out it scale to thumbnail size?

    Reply
  3. Dan Bamberger
    Posted on November 13, 2015 at 20:42 Permalink

    This is great! Quick question: we’re implementing this on a site that used a base image, and a second layer of illustrations placed on top of the main background image. Any specific implementation suggestions to avoid moving the placement of the illustrations on top? Also, will this work well on tablet?

    Reply
    • malihu
      Posted on November 13, 2015 at 21:15 Permalink

      Yes it works with touch devices (it pans the same way on touch/tap move).

      About the first question:

      If you give the panned image an id, you can call the main function (imagePanning()) on this id (instead of the more generic img selector).

      For example, assuming the following markup:

      <div class="content"> <img src="img.jpg" id="panned-image" /> <!-- panned image --> <img src="img.jpg" /> <!-- your non-panned overlay element --> </div>

      you could change the script to:

      $("#panned-image").imagePanning();

      Hope this helps

      Reply
  4. Yann
    Posted on October 16, 2015 at 18:27 Permalink

    Great code!

    I have a question:
    how to do if I want to stop it working?
    I have some buttons on my website which start transitions (arrows to change from an image to another, with a slide move), but before these transitions start, I need to stop/disable all this panning (and start it again when the new is in place).

    How can I do ?

    Thank you very much!

    Reply
    • malihu
      Posted on October 16, 2015 at 22:18 Permalink

      You could add a variable in mousemove event to check its value and do the panning.

      For example, add the (global) variable before image panning function:

      var state=1; $.fn.imagePanning=function(){...

      and add the conditions in mousemove event and interval:

      // ... .parent().on("mousemove touchmove MSPointerMove pointermove",function(e,p){ if(!state){return;} // ... timer=setInterval(function(){ if(!state){return;} // ...

      Then in your script(s) you can do:

      state=0; to disable and state=1; to enable.

      Reply
  5. Isaiaseg
    Posted on October 7, 2015 at 17:39 Permalink

    Hello buddy,

    Thanks for this piece of code it helped me a lot.

    Now im wondering if there is a query to after i click a button will place the cursor where i decide within the viewport?

    Thanks a lot

    Reply
    • malihu
      Posted on October 8, 2015 at 08:42 Permalink

      You cannot take control user’s cursor with javascript and move it programmatically (it’s not allowed). This is a good thing because it would be chaos if you could πŸ™‚

      Reply
      • isaiaseg
        Posted on December 11, 2015 at 18:38 Permalink

        Very true.

        There is a way to make it draggable instead of hover? so it can be used on touch devices?

        Reply
        • malihu
          Posted on December 11, 2015 at 18:53 Permalink

          The script as it is, works on touch devices the same way as with the cursor (it pans the image opposite of touch-swipe). If you need an exact touch-drag behavior I’d suggest using a more complex plugin (with more options) like this one (panning demo).

          Reply
  6. Benjamin Gordon
    Posted on September 11, 2015 at 19:43 Permalink

    This is nice! Has anyone got it working with the accelerometer in iOS? This would be perfect for a project I am about to start… Thanks πŸ™‚

    Reply
  7. ash
    Posted on July 28, 2015 at 12:38 Permalink

    This is a great piece of code! I was wondering would it work with panning only a certain amount of a image? AKA constraining the image pan?

    Reply
    • malihu
      Posted on August 1, 2015 at 19:25 Permalink

      You’d need to pan a div with the image as a background, like this example (check the first one):
      http://manos.malihu.gr/repository/jquery-image-panning/demo/image-map-div-panning.html

      In this example you could use a combination of div dimensions with background position to do what you need. For example:
      .img-pan-container .img-wrapper .img{ /* div panning */ position: relative; /* make panned element smaller than its background image */ width: 1320px; height: 621px; } /* ... */ /* set its background position to get where you want */ <div class="img" style="background:url(img.jpg) no-repeat 70% bottom;">

      Reply
  8. christopher
    Posted on June 1, 2015 at 05:27 Permalink

    hi malihu,

    this is great – thanks!

    i’m wondering, could it be adapted to pan a div instead of an image? i’d like to pan a map (image or div with a background image), which has markers (divs) absolutely positioned on top of it. i was thinking if i could move the entire div, then those markers would stay in place.

    is this something possible? thanks!

    Reply
  9. Khai
    Posted on May 26, 2015 at 21:12 Permalink

    Hey

    First, love your work!
    I’m looking for a image panner for a project for the Norwegian Colorlab: http://colorlab.no/. The goal of the project is to have a research tool for image quality assessment and need a panner for large images.

    Would it be possible in the demo where there are multiple instances that they move paralell to each other? Meaning if you pan in either boxes/instances the others would move the same?

    Thanks in advance.

    Best,
    Khai

    Reply
    • malihu
      Posted on May 27, 2015 at 16:54 Permalink

      Hello,

      It can be done with few minor script mods. I’ll upload an example later today πŸ˜‰

      Reply
    • malihu
      Posted on May 27, 2015 at 17:23 Permalink

      Made some minor changes to the script and I’ve uploaded a new example here:
      http://manos.malihu.gr/repository/jquery-image-panning/demo/multiple-instances-synchronization.html

      Easy setup:

      1. Add a class to the instances you want to synchronize, e.g. “sync”
      <div class="content sync"> <img src="img.jpg" /> </div> <div class="content sync"> <img src="img.jpg" /> </div> <div class="content sync"> <img src="img.jpg" /> </div>

      2. Add the class selector to sync variable (in multiple-instances-synchronization.html on line: 84):
      sync=".sync"

      Reply
    • malihu
      Posted on May 27, 2015 at 17:26 Permalink

      I’ve added the multiple-instances-synchronization.html example in the demo links at the top of the page, as well as the download archive.

      Reply
      • Khai
        Posted on May 27, 2015 at 17:30 Permalink

        Thank you so much, this is really great and such a fast reply!

        Reply
  10. Laura
    Posted on March 25, 2015 at 21:24 Permalink

    Thanks for the code!

    Is there a way to position the image within the container so that the top of the image is aligned with the top of the container? I added autopan on window load. I’d like the image to load from top to bottom on window load.

    Reply
    • malihu
      Posted on April 4, 2015 at 15:20 Permalink

      Hello,

      I’ve just updated the script. In the updated code, you can change the centering by changing:
      var init="center"
      to:
      var init="0"

      Reply
      • Laura
        Posted on April 6, 2015 at 23:49 Permalink

        Thanks Malihu. I’m too far down the rabbit hole on the old code at this point. I got the image to pan from top to bottom on window load, but now when I include a setTimeout function for the MouseMove function, I can no longer pan the image. Ideas?

        Reply
        • malihu
          Posted on April 7, 2015 at 16:00 Permalink

          What do you need the timeout for? If you can send me your url/code I’ll be able to help.

          Reply
  11. cms
    Posted on February 25, 2014 at 12:19 Permalink

    I needed this code tanx a lot

    Reply
  12. mediahub
    Posted on August 26, 2013 at 08:59 Permalink

    Nice code, is it works or compatible with touch device like smartphone or tablets?

    Reply
  13. newwebsitethemes
    Posted on August 26, 2013 at 08:56 Permalink

    Love this effects, now trying to make some different position, thanks for the great tips.

    Reply
  14. rjgamer
    Posted on July 24, 2013 at 08:32 Permalink

    Hi,

    great code! Thanks dude!

    I’ve question: I want the mouse outside of the div for moving the image. I’ve changed this code:
    $imagePan.bind("mousemove", function(event){ MouseMove(event); });
    to this:
    $('body').bind("mousemove", function(event){ MouseMove(event); });

    But my change worked not correct. What have I to change?

    Thanks for helping.

    Reply
  15. Alicia
    Posted on April 30, 2013 at 10:14 Permalink

    Hey, nice script

    I was wondering if it’s possible to change the code for viewing on smartphones & tablets so that the user can drag instead of hover?

    Reply
  16. Alicia
    Posted on April 30, 2013 at 10:14 Permalink

    Hi there, great script!

    I was wondering if it’s possible to change the code for viewing on smartphones & tablets so that the user can drag instead of hover?

    Reply
  17. Dean
    Posted on February 20, 2013 at 00:49 Permalink

    Thanks for a great script it is the perfect answer to what I was looking for.
    I have impleted your script on a page of a website of mine to allow users to move a map around. I hope to eventually use it on photographs of mine.

    I am trying to understand how I can get the script to work on a page that resizes depending on what size screen the viewer is using and even how to implement it on a page with a header and a footer and the usaual menu buttons.

    I take my hat off to people who understand HTML, Java and CSS as I have to rely on WordPress themes and Dreamweaver. Having said that I still can’t get Dreamweaver to do what I want hence the switch to WordPress, apart from the page with the map.

    Okay I’m off to the library to get a book on the subject!

    Reply
  18. anija
    Posted on January 23, 2013 at 16:36 Permalink

    Hi! Thank you for this code πŸ™‚
    i’m using it with outside container fixed at 100% width and 100% height (a fullscreen image zoom) but i’ve a problem on the left side: when mouse reach position 0, the image is shifted on the right of about 200px, instead vertical movements are fine, and i just can’t realize why…

    Reply
  19. DongDongFace
    Posted on May 18, 2012 at 05:42 Permalink

    Fix ie6 display Bug:
    find line :
    var easeType="easeOutCirc";

    replace to:

    var easeType="easeOutCirc"; if ($.browser.msie && $.browser.version.substr(0,1)<7) {$imagePan_panning.css("margin-left","0px").css("margin-top","0px");}// fix ie6

    Reply
  20. Alan
    Posted on May 2, 2012 at 22:35 Permalink

    Hi. Thanks for this great script. I have used it on the Berndt Museum site for a virtual exhibition, using Lasso to build the clickable links by pulling information from the filemaker database. Check it out at http://berndt.uwa.edu.au/panorama.lasso?panID=1. Thanks a million!

    Reply
    • malihu
      Posted on May 2, 2012 at 23:53 Permalink

      Awesome! Thank you for using the script and for your comments Alan πŸ™‚

      Reply
  21. phonecluster
    Posted on April 27, 2012 at 13:05 Permalink

    Yes, I’m looking for this wonderful effect, thank you so much, great job.

    Reply

Comments pages: 1 2

Post a comment

Your e-mail is never published nor shared. Required fields are marked *

You may use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>
You can write or copy/paste code directly in your comment using the <code> tag:
<code>code here...</code>
You may also use the data-lang attribute to determine the code language like so:
<code data-lang-html>, <code data-lang-css>, <code data-lang-js> and <code data-lang-php>

css.php