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


78 Comments

Post a comment

Comments pages: 1 2

  1. 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
  2. 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
  3. 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
  4. cms
    Posted on February 25, 2014 at 12:19 Permalink

    I needed this code tanx a lot

    Reply
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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>

css.php