jQuery image panning
Image panning with animation easing that works on mouse movement with css and jQuery.
view demo Example without animation
Multiple instances demo
Multiple instances synchronization example
Multiple instances demo
Multiple instances synchronization example
download Get all files related to post
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 🙂
Hi there! Any way to disable/enable the script upon clicking a div outside the panned content?
This is a very nice script and has saved me tons of headache — thank you…
Tnx for sharing, very good job
Hello
That is very useful and I am happy to find your article about this method.
Thanks.
thank you.
Thanks
very good, thanks.
great job thanks
I enjoyed it so much
Do not be tired
I sent it to my friend
Thankful
loading site is go000ood
Good for me
Anyone can understand this
The installation process is explained very well and in a very definite way.
good jobs , thanks
very good
Hi there! Any way to disable/enable the script upon clicking a div outside the panned content?
this is the best and amazing post, I liked it a lot! Thank so much!
Thanks for sharing
So flexible …
thanks
thanks a lot for share this post
very good
it is amazing script
Hi! this code is great! is possible set two divs (o more) to moving a different speeds, simulating a perspective scenario?
Thanks!
thanks to you for your kind effort
great!!!
These codes are very functional
Greetings,
very great stuff! there’s a way to invert panning on mobile? thanks
very good
thanks a lot for share this best post
thanks a lot for share this post
very good
very good, thanks.
I have a problem on Mac. I’m using some mix for image zoom and then when class for zoom is added, I’m calling this plugin for panning. On Windows, it works perfect if I add $this.trigger(“mousemove”,1) just before resize function. On Mac, piece of zoomed image that is shown on click is not good before I start moving the mouse. Then some repositioning happen and place where you clicked at first place is shown. It seems like triggering mousemove event is not working on Mac maybe.. Does anybody have some hint?
tanks for sharing
These codes are very functional thanks to you for your kind effort
tanks for sharing
thank you, very very nice, kiss kiss
very good
thanks
Hi there! Any way to disable/enable the script upon clicking a div outside the panned content?
This is a very nice script and has saved me tons of headache — thank you!
I have a problem on Mac. I’m using some mix for image zoom and then when class for zoom is added, I’m calling this plugin for panning. On Windows, it works perfect if I add $this.trigger(“mousemove”,1) just before resize function. On Mac, piece of zoomed image that is shown on click is not good before I start moving the mouse. Then some repositioning happen and place where you clicked at first place is shown. It seems like triggering mousemove event is not working on Mac maybe.. Does anybody have some hint?
Thanks!
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 !