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


111 Comments

Post a comment

Comments pages: 1 2 3

  1. Roee Yossef
    Posted on January 27, 2017 at 12:19 Permalink

    Thanks for the amazing script !

    It works well, but i wonder how can i get to the point where i have content on the image and the image itself keeps panning while i’m moving the mouse on the content as well. obviously, currently the image doesnt pan while the mouse is on the content..

    I’ll Appreciate a tip !

    Reply

Comments pages: 1 2 3

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