How to pan an inline SVG smoothly with Javascript

In a Cordova/Android app that I am creating I have to implement my own zooming & panning (no libraries allowed nor suitable) of an inline SVG image. My effort thus far is shown below.

var _hold = {zoom:1};

function preparePanZoom()
{
 
 var actuY,scaleX,scaleY;
      
 _hold.factorX = 1600/window.innerWidth;
 actuY = (0.855*window.innerHeight);
 _hold.factorY = 770/actuY;
    
 _hold.displaceY = 0.145*window.innerHeight;
 scaleX = 1/_hold.factorX;
 scaleY = 1/_hold.factorY;

 _hold.panMax = [0,_hold.displaceY - actuY];
 _hold.baseMatrix = `matrix(${scaleX} 0 0 ${scaleY} 0 0)`;
 _hold.baseScale = `scale(${scaleX},${scaleY})`;
 document.getElementById('btnReset').addEventListener('touchstart',resetZoom);

 var gOuter = document.getElementById('gOuter');
 gOuter.addEventListener('touchstart',zoomManage);
 gOuter.setAttribute('transform',_hold.baseScale);
}

function resetZoom()
{
 document.getElementById('btnReset').style.display = 'none';
 var gOuter = document.getElementById('gOuter');
 
 gOuter.setAttribute('transform',_hold.baseScale);
 gOuter.addEventListener('touchstart',zoomManage);
 gOuter.removeEventListener('touchstart',panStart);
 gOuter.removeEventListener('touchmove',panMove);
 _hold.zoom = 1;
}

function zoomManage(e)
{
 if (1 < _hold.zoom) return;   
 if (_hold.magnifier)
 {
  clearTimeout(_hold.magnifier);
  delete(_hold.magnifier);   
  if (0 < e.touches.length)
  {
   var tch = e.touches[0];   
   document.getElementById('btnReset').style.display = 'block';
   expandAround(tch.clientX,tch.clientY - _hold.displaceY);
  } 
 } else 
 {
  _hold.magnifier = setTimeout(clearMagnifier,200);   
  _hold.tapstart = Math.round(new Date().getTime()/50);
 }  
}

function clearMagnifier()
{
 if (_hold.magnifier)
 {
  clearTimeout(_hold.magnifier);
  delete(_hold.magnifier);   
 }  
}

function expandAround(cX,cY)
{
 var x = cX*1600/window.innerWidth,
     y = cY*770/(0.855*window.innerHeight),
     t1 = `translate(${-x},${-y})`,
     t2 = `translate(${x},${y})`,
     gOuter = document.getElementById('gOuter'),
    transform = `${_hold.baseScale} ${t2} scale(2,2) ${t1}`;

 _hold.panMin = [cX,cY];
 _hold.panMax[0]= cX - window.innerWidth;                       
 _hold.lastTransform = transform;                       
 gOuter.setAttribute('transform',transform);
 document.getElementById('btnReset').style.display = 'block';

 gOuter.removeEventListener('touchstart',zoomManage);
 gOuter.addEventListener('touchstart',panStart,{passive:true});
 gOuter.addEventListener('touchmove',panMove,{passive:true});
 _hold.zoom = 2;
}

function panStart(evt)
{
 evt.stopPropagation();
 _hold.rafCount = 0;
}

function panMove(evt)
{
 var cX,cY,
     moveX,moveY,
     cht = evt.changedTouches;   

 evt.stopPropagation();  
 if (3 < ++_hold.rafCount) return;
 _hold.rafCount = 0;

 if (0 < cht.length)   
 {
  cht = cht[0];   
  cX = cht.clientX;
  cY = cht.clientY;
  
  if ((0 >= cX) || (_hold.displaceY >= cY)) return;
  moveX = _hold.panMin[0] - cX;
  moveY = _hold.panMin[1] - cY;

  if (0 < moveX)
  {
   moveX = (moveX < _hold.panMax[0])?_hold.panMax[0]:moveX;
  } else
  {
   moveX = (moveX > _hold.panMin[0])?_hold.panMin[0]:moveX;   
  }

  if (0 < moveY)
  {
   moveY = (moveY < _hold.panMax[1])?_hold.panMax[1]:moveY;
  } else
  {
   moveY = (moveY > _hold.panMin[1])?_hold.panMin[1]:moveY;
  } 
  _hold.panText = ` translate(${moveX},${moveY})`;
  if (!_hold.queued) _hold.queued = window.requestAnimationFrame(performPan);   
 } 
}

function performPan()
{
 delete(_hold.queued); 
 var transform = _hold.lastTransform + _hold.panText;
 var gOuter = document.getElementById('gOuter');
 
 gOuter.setAttribute('transform',_hold.baseMatrix);
 gOuter.setAttribute('transform',transform);  
}

preparePanZoom();
body,html{padding:0;margin:0;font-family:arial;}
   #btnReset
   {
    border-radius:8px;
    padding:0.5em;
    background-color:blue;
    color:white;
    display:none;
   }

   #puzzle
   {
    position:relative;
    height:85.5vh !important;
    width:100vw !important;
   }

   #controlBar
   {
    min-height:14.5vh;
    background-color:blue;
    padding:0.25em;
    display:grid;
    place-items:right center; 
   }
<div id='controlBar'>
   <button id='btnReset'>Reset</button>
 </div>
 <svg width="100%" height="100%" preserveAspectRatio="none" id="puzzle" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
  <g id="gOuter">
      <rect x="1.135" y="-0.248" width="1597.73" height="767.092" style="fill:rgb(21,135,221);"/>
      <path d="M170.78,57.624C228.712,57.624 275.745,96.776 275.745,145C275.745,193.224 228.712,232.376 170.78,232.376C112.849,232.376 65.816,193.224 65.816,145C65.816,96.776 112.849,57.624 170.78,57.624ZM170.78,101.312C199.746,101.312 223.262,120.888 223.262,145C223.262,169.112 199.746,188.688 170.78,188.688C141.814,188.688 118.298,169.112 118.298,145C118.298,120.888 141.814,101.312 170.78,101.312Z" style="fill:rgb(199,21,221);"/>
      <path d="M743.696,185.2C737.747,184.555 731.756,184.555 725.807,185.2L722.861,201.216C717.159,202.19 711.571,203.862 706.197,206.201L696.473,193.977C691.033,196.739 685.844,200.083 680.981,203.964L685.601,219.478C681.1,223.505 677.009,228.073 673.402,233.099L659.507,227.941C656.032,233.37 653.037,239.163 650.563,245.239L661.512,256.095C659.416,262.096 657.919,268.336 657.046,274.702L642.703,277.992C642.125,284.634 642.125,291.323 642.703,297.966L657.046,301.255C657.919,307.622 659.416,313.862 661.512,319.862L650.563,330.719C653.037,336.794 656.032,342.587 659.507,348.017L673.402,342.858C677.009,347.885 681.1,352.453 685.601,356.479L680.981,371.994C685.844,375.874 691.033,379.219 696.473,381.981L706.197,369.756C711.571,372.096 717.159,373.768 722.861,374.742L725.807,390.757C731.756,391.402 737.747,391.402 743.696,390.757L746.642,374.742C752.344,373.768 757.933,372.096 763.307,369.756L773.03,381.981C778.471,379.219 783.659,375.874 788.522,371.994L783.902,356.479C788.404,352.453 792.495,347.885 796.101,342.858L809.996,348.017C813.471,342.587 816.467,336.794 818.941,330.719L807.992,319.862C810.087,313.862 811.585,307.622 812.457,301.255L826.801,297.966C827.379,291.323 827.379,284.634 826.801,277.992L812.457,274.702C811.585,268.336 810.087,262.096 807.992,256.095L818.941,245.239C816.467,239.163 813.471,233.37 809.996,227.941L796.101,233.099C792.495,228.073 788.404,223.505 783.902,219.478L788.522,203.964C783.659,200.083 778.471,196.739 773.03,193.977L763.307,206.201C757.933,203.862 752.344,202.19 746.642,201.216L743.696,185.2ZM734.752,267.326C744.96,267.326 753.248,276.58 753.248,287.979C753.248,299.377 744.96,308.631 734.752,308.631C724.543,308.631 716.255,299.377 716.255,287.979C716.255,276.58 724.543,267.326 734.752,267.326Z" style="fill:rgb(221,97,21);"/>
      <path d="M1104.68,419.383C1122.96,384.433 1159.51,384.433 1177.78,401.908C1196.06,419.383 1196.06,454.333 1177.78,489.284C1164.99,515.496 1132.09,541.709 1104.68,559.184C1077.27,541.709 1044.37,515.496 1031.58,489.284C1013.3,454.333 1013.3,419.383 1031.58,401.908C1049.85,384.433 1086.4,384.433 1104.68,419.383Z" style="fill:rgb(221,212,21);"/>
      <path d="M1418.44,147.496C1423.69,141.596 1434.21,141.596 1439.46,144.546C1444.72,147.496 1444.72,153.397 1439.46,159.298C1435.78,163.723 1426.32,168.149 1418.44,171.099C1410.56,168.149 1401.1,163.723 1397.42,159.298C1392.16,153.397 1392.16,147.496 1397.42,144.546C1402.67,141.596 1413.18,141.596 1418.44,147.496Z" style="fill:rgb(68,221,21);"/>
      <path d="M402.555,569.548L419.013,583.465L410.784,596.648L424.099,601.684L417.813,624.203L404.498,619.167L404.498,635.463L384.155,635.463L384.155,619.167L370.84,624.203L364.553,601.684L377.868,596.648L369.639,583.465L386.097,569.548L394.326,582.731L402.555,569.548Z" style="fill:rgb(68,221,21);"/>
      <path d="M1400.85,344.716L1406.84,363.44L1418.39,357.654L1416.54,370.591L1435.92,370.591L1420.24,382.163L1429.23,391.525L1416.54,393.735L1422.53,412.458L1406.84,400.887L1400.85,412.458L1394.86,400.887L1379.17,412.458L1385.16,393.735L1372.48,391.525L1381.46,382.163L1365.78,370.591L1385.16,370.591L1383.31,357.654L1394.86,363.44L1400.85,344.716Z" style="fill:rgb(21,57,221);"/>
      <path d="M332.482,332.234C299.894,332.234 273.475,360.685 273.475,395.78C273.475,430.852 299.915,459.326 332.482,459.326C365.071,459.326 391.489,430.876 391.489,395.78L332.482,395.78L332.482,332.234Z" style="fill:rgb(21,57,221);"/>
  </g>
</svg>

A few notes on my requirements and my implementation:

  • I only require one level of zoom - in effect a x 2 zoom.
  • Panning is only relevant AFTER zooming
  • Aspect ratio presrevation is not important
  • I wrap the entire contents of the SVG in a outer group bearing the id gOuter
  • This makes it easier for me to implement the zoom & pan code.

In brief here is how I have implemented panning

  • I use the touchmove event on the wrapping group, gOuter to calculate the extent of the move
  • The move is performed as a translation and the previously applied scaling transformation from the prior Zoom operation is reapplied to ensure that the image does not slide off the screen.

The outstanding issues

  1. It is still possible to pan off the extreme edgges of the picture and end up displaying a blank white space
  2. While this works well on a desktop PC on phones I have found that the panning operation is less than smooth
  3. I attempted to deal with this my not responding - the in the Window.requestAnimationFrame handler - to every single mouse move but this has only helped a bit
    1. On hand held devices it is difficult to pan to the edges - on desktop monitors set up in Chrome to mimic a small phone screen it works well since you can simply keep moving beyond the virtual edge of the mimicked handheld screen

I'd be grateful to anyone who might be able to suggest ways to improve the panning process.

Answers:

Answer

Here are 2 ways to do this (there may be more);

option 1 : You can use this where you can put the svg in a container which can be sized and scrolled.

Uses svg size to zoom and container scroll to pan.

(You can hide the scollbars if you want and still effect a scoll or leave them visible if you prefer)

For this to work the container must be display:inline-block or display:block (because with display:inline you cannot set width or height).

option 2 : For any situation regardless of container.

Uses svg viewbox to zoom and pan..


NB.

Option 1 may be faster as you're doing less of the work and leaving more of it to the underlying native functionality. Option 1 is also simpler to code. Option 1 also takes care of the limits of panning - you can't scroll beyond what's available.

However, with option 1, if you zoom (resize) you may need to wait for the browser to reflow the document before you can align by setting the scroll values - the available limits of scroll won't get updated until the next reflow - so to zoom and stay aligned you may need to resize and call requestAnimationFrame to set the scroll when the scroll becomes available.


code

Uses the OP svg with the addition of a viewbox in the markup.

bootstrap is just for button styling.

option 1 example

let svg = null ; //for zooming
let svgContainer = null ; // for scrolling/panning

let svgWidth = 0 ;// unknown
let svgHeight = 0 ; // unknown

const zoomFactor = 1.5 ;
let zoomValue = 1; 

function setSVGSize(){
    svgWidth = svg.getBoundingClientRect().width * zoomValue; // offsetWidth is not available on svgs and svg.width.baseVal.value does not behave the same in FF and Chrome;    
    svgHeight = svg.getBoundingClientRect().height * zoomValue;
    
    svg.style.height = svgHeight + "px" ;
    svg.style.width = svgWidth + "px" ; 
}
function zoom(zoomType){
    switch(zoomType){
        case -1://zoomValue out
            zoomValue = 1 / zoomFactor ;
            setSVGSize();
            break;        
        case 0://reset
            //just clear and let the browser decide what it should be
            zoomValue = 1 ;
            svg.style.height = "" ;
            svg.style.width = "" ; 
            break;
        case 1://zoomValue in
            zoomValue = zoomFactor ;
            setSVGSize();
            break;
        default:
            console.log("invalid zoomType");
    }    
}

function pan(dist){ 
    if(dist === 0){//reset     
        svgContainer.scrollLeft = 0 ;
    }
    else{
        svgContainer.scrollLeft += dist  ;
    }   
}

//initialise svg and svgContainer once available
function init(){
    svg = document.getElementById("svg") ; 
    svgContainer = document.getElementById("svgContainer") ; 
}
window.addEventListener("load",init);

/*
 * The next bit is just for pan animation / the purposes of demonstrating a smooth pan - it's the same code in both examples (option 1 and option 2).
 * However, in this option as you can't scroll beyond 0 or scrollWidth the pan calls will have no effect once you reach the edges of the scrollable content.
 * If moving / panning / scrolling in response to a touch gesture you might not need to animate - 
 * you might just set the new offset to the touch/pointer distance immediately.
 * ie. use the pan function above directly as pan(pointerMoveDistance).
 */
let animationFrameRequest = 0 ; // so we can cancel an unfinished pan animation if starting a new one / resetting.
const scrPxPanDistance = 200 ;  
const scrPxFrameSpeed = 1 ; //  scr px per frame 
const framesPerPan = scrPxPanDistance / scrPxFrameSpeed ;

let scrPxFrameVelocity = 0; // add a -ve sign to the scrPxFrameSpeed to reverse direction if necesary
let framesRemaining = 0 ;

function animatePan(){
    if(framesRemaining > 0){
        framesRemaining-- ;     
        pan(scrPxFrameVelocity);
        animationFrameRequest = requestAnimationFrame(animatePan) ;
    }   
}
function cancelCurrentAnimation(){
    if(animationFrameRequest){               
        //cancel any running animation
        cancelAnimationFrame(animationFrameRequest);
        animationFrameRequest = 0 ;
    }     
}
function startAnimatedPan(left){// false => right   
    cancelCurrentAnimation();
    scrPxFrameVelocity = left ? scrPxFrameSpeed : -scrPxFrameSpeed ;
    framesRemaining = framesPerPan  ;
    animatePan();
}
function resetPan(){
    cancelCurrentAnimation();   
    pan(0);
}
*{
    border:none;
    padding:0;       
    font-family:Arial;
    box-sizing:border-box;
}
body{
    margin:10px;
    background:lightblue;
}


#svgContainer{
    display: inline-block ; /* or "block" - plain inline has no "scollability" at present */
    overflow : hidden ; /* also hides the scrollbars but doesn't stop you from scrolling*/
    background-color: lightyellow; 
    
    /* if we don't set some limits on the container everything can just keep getting bigger and there will never be any need/ability to scroll/pan */
    max-width: 50vw;
    max-height: 60vh;
}

#svg{
    margin:0;
    display:inline;    
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet"/>
            <div id="svgContainer">
                <svg  id="svg" width="100%" height="100%" viewBox="0 0 1597.73 767.092" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
                    <g id="gOuter">
                        <rect x="1.135" y="-0.248" width="1597.73" height="767.092" style="fill:rgb(21,135,221);"/>
                        <path d="M170.78,57.624C228.712,57.624 275.745,96.776 275.745,145C275.745,193.224 228.712,232.376 170.78,232.376C112.849,232.376 65.816,193.224 65.816,145C65.816,96.776 112.849,57.624 170.78,57.624ZM170.78,101.312C199.746,101.312 223.262,120.888 223.262,145C223.262,169.112 199.746,188.688 170.78,188.688C141.814,188.688 118.298,169.112 118.298,145C118.298,120.888 141.814,101.312 170.78,101.312Z" style="fill:rgb(199,21,221);"/>
                        <path d="M743.696,185.2C737.747,184.555 731.756,184.555 725.807,185.2L722.861,201.216C717.159,202.19 711.571,203.862 706.197,206.201L696.473,193.977C691.033,196.739 685.844,200.083 680.981,203.964L685.601,219.478C681.1,223.505 677.009,228.073 673.402,233.099L659.507,227.941C656.032,233.37 653.037,239.163 650.563,245.239L661.512,256.095C659.416,262.096 657.919,268.336 657.046,274.702L642.703,277.992C642.125,284.634 642.125,291.323 642.703,297.966L657.046,301.255C657.919,307.622 659.416,313.862 661.512,319.862L650.563,330.719C653.037,336.794 656.032,342.587 659.507,348.017L673.402,342.858C677.009,347.885 681.1,352.453 685.601,356.479L680.981,371.994C685.844,375.874 691.033,379.219 696.473,381.981L706.197,369.756C711.571,372.096 717.159,373.768 722.861,374.742L725.807,390.757C731.756,391.402 737.747,391.402 743.696,390.757L746.642,374.742C752.344,373.768 757.933,372.096 763.307,369.756L773.03,381.981C778.471,379.219 783.659,375.874 788.522,371.994L783.902,356.479C788.404,352.453 792.495,347.885 796.101,342.858L809.996,348.017C813.471,342.587 816.467,336.794 818.941,330.719L807.992,319.862C810.087,313.862 811.585,307.622 812.457,301.255L826.801,297.966C827.379,291.323 827.379,284.634 826.801,277.992L812.457,274.702C811.585,268.336 810.087,262.096 807.992,256.095L818.941,245.239C816.467,239.163 813.471,233.37 809.996,227.941L796.101,233.099C792.495,228.073 788.404,223.505 783.902,219.478L788.522,203.964C783.659,200.083 778.471,196.739 773.03,193.977L763.307,206.201C757.933,203.862 752.344,202.19 746.642,201.216L743.696,185.2ZM734.752,267.326C744.96,267.326 753.248,276.58 753.248,287.979C753.248,299.377 744.96,308.631 734.752,308.631C724.543,308.631 716.255,299.377 716.255,287.979C716.255,276.58 724.543,267.326 734.752,267.326Z" style="fill:rgb(221,97,21);"/>
                        <path d="M1104.68,419.383C1122.96,384.433 1159.51,384.433 1177.78,401.908C1196.06,419.383 1196.06,454.333 1177.78,489.284C1164.99,515.496 1132.09,541.709 1104.68,559.184C1077.27,541.709 1044.37,515.496 1031.58,489.284C1013.3,454.333 1013.3,419.383 1031.58,401.908C1049.85,384.433 1086.4,384.433 1104.68,419.383Z" style="fill:rgb(221,212,21);"/> 
                        <path d="M1418.44,147.496C1423.69,141.596 1434.21,141.596 1439.46,144.546C1444.72,147.496 1444.72,153.397 1439.46,159.298C1435.78,163.723 1426.32,168.149 1418.44,171.099C1410.56,168.149 1401.1,163.723 1397.42,159.298C1392.16,153.397 1392.16,147.496 1397.42,144.546C1402.67,141.596 1413.18,141.596 1418.44,147.496Z" style="fill:rgb(68,221,21);"/>
                        <path d="M402.555,569.548L419.013,583.465L410.784,596.648L424.099,601.684L417.813,624.203L404.498,619.167L404.498,635.463L384.155,635.463L384.155,619.167L370.84,624.203L364.553,601.684L377.868,596.648L369.639,583.465L386.097,569.548L394.326,582.731L402.555,569.548Z" style="fill:rgb(68,221,21);"/>
                        <path d="M1400.85,344.716L1406.84,363.44L1418.39,357.654L1416.54,370.591L1435.92,370.591L1420.24,382.163L1429.23,391.525L1416.54,393.735L1422.53,412.458L1406.84,400.887L1400.85,412.458L1394.86,400.887L1379.17,412.458L1385.16,393.735L1372.48,391.525L1381.46,382.163L1365.78,370.591L1385.16,370.591L1383.31,357.654L1394.86,363.44L1400.85,344.716Z" style="fill:rgb(21,57,221);"/>
                        <path d="M332.482,332.234C299.894,332.234 273.475,360.685 273.475,395.78C273.475,430.852 299.915,459.326 332.482,459.326C365.071,459.326 391.489,430.876 391.489,395.78L332.482,395.78L332.482,332.234Z" style="fill:rgb(21,57,221);"/>
                    </g> 
                </svg>   
            </div>

            <br><br>
            <div class="container">
                <div class="btn-group">       
                    <button class="btn btn-primary" onclick="zoom(-1);">zoom out</button>  
                    <button class="btn btn-secondary" onclick="zoom(0);">reset</button>   
                    <button class="btn btn-primary" onclick="zoom(1);">zoom in</button>                 
                </div>
                <div class="btn-group">
                    <button class="btn btn-primary" onclick="startAnimatedPan(true);">pan left</button>   
                    <button class="btn btn-secondary" onclick="resetPan();">reset</button>         
                    <button class="btn btn-primary" onclick="startAnimatedPan(false);">pan right</button>   
                </div>        
            </div>

option 2 example ( When running this snippet you may want to go full page and then reduce your browser window to see it properly - you can restrict the container but this is with an unrestricted container )

let svg = null ;

const zoomFactor = 1.5 ;
let zoomLevel = 1;

const imageWidth = 1597.73; //img px as defined in the svg markup
const imageHeight = 767.092; //img px as defined in the svg markup

let offsetX = 0 ; //screen px
let pixelRatioX = null ;// img px / scr pixel

let viewWidth = imageWidth ;
let viewHeight = imageHeight;
        
function evalPixelRatioX(){
    let svgWidth = svg.getBoundingClientRect().width ; // offsetWidth is not available on svgs and svg.width.baseVal.value does not behave the same in FF and Chrome;    
    pixelRatioX = (imageWidth / svgWidth) /zoomLevel  ;
}
function setViewPort(){    
    viewWidth = imageWidth / zoomLevel ;
    viewHeight = imageHeight / zoomLevel ;      
    evalPixelRatioX(); 
    svg.setAttribute("viewBox",`${offsetX * pixelRatioX} 0 ${viewWidth} ${viewHeight}`) ;
}
function zoom(zoomType){
    switch(zoomType){
        case -1://zoom out
            zoomLevel = zoomLevel / zoomFactor ;
            break;        
        case 0://reset
            zoomLevel = 1 ;
            break;
        case 1://zoom in
            zoomLevel = zoomLevel * zoomFactor ;
            break;
        default:
            console.log("invalid zoomType");
    }
    setViewPort();
}
function pan(dist){//scr px
    if(dist === 0){//reset     
        offsetX = 0 ;
    }
    else{
        offsetX += dist ;
    }
    setViewPort();
}

//initialise svg once available
function init(){
    svg = document.getElementById("svg") ; 
}
window.addEventListener("load",init);

/*
 * The next bit is just for pan animation / the purposes of demonstrating a smooth pan - it's the same code in both examples (option 1 and option 2).
 * However, in this option panning in either direction can continue indefinitely as it is not limited by scrollWidth as in option 1
 * If moving / panning / scrolling in response to a touch gesture you might not need to animate - 
 * you might just set the new offset to the touch/pointer distance immediately.
 * ie. use the pan function above directly as pan(pointerMoveDistance).
 */
let animationFrameRequest = 0 ; // so we can cancel an unfinished pan animation if starting a new one / resetting.
const scrPxPanDistance = 200 ;  
const scrPxFrameSpeed = 1 ; //  scr px per frame 
const framesPerPan = scrPxPanDistance / scrPxFrameSpeed ;

let scrPxFrameVelocity = 0; // add a -ve sign to the scrPxFrameSpeed to reverse direction if necesary
let framesRemaining = 0 ;

function animatePan(){
    if(framesRemaining > 0){
        framesRemaining-- ;        
        pan(scrPxFrameVelocity);
        animationFrameRequest = requestAnimationFrame(animatePan) ;
    }   
}
function cancelCurrentAnimation(){
    if(animationFrameRequest){               
        //cancel any running animation
        cancelAnimationFrame(animationFrameRequest);
        animationFrameRequest = 0 ;
    }     
}
function startAnimatedPan(left){// false => right   
    cancelCurrentAnimation();
    scrPxFrameVelocity = left ? scrPxFrameSpeed : -scrPxFrameSpeed ;
    framesRemaining = framesPerPan  ;
    animatePan();
}
function resetPan(){
    cancelCurrentAnimation();   
    pan(0);
}
*{
    border:none;
    padding:0;       
    font-family:Arial;
    box-sizing:border-box;
}
body{
    margin:10px;
    background:lightblue;
}

#bkg{
    display: inline ;
    background-color: lightyellow; 
}
#svg{
    margin:0;
    display:inline;
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet"/>
        <div id="bkg"><!-- avoid extra space in inline element from markup line returns / whitespace
            --><svg  id="svg" width="100%" height="100%" viewBox="0 0 1597.73 767.092" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><!-- 
                    --><g id="gOuter"><!-- 
                        --><rect x="1.135" y="-0.248" width="1597.73" height="767.092" style="fill:rgb(21,135,221);"/><!-- 
                        --><path d="M170.78,57.624C228.712,57.624 275.745,96.776 275.745,145C275.745,193.224 228.712,232.376 170.78,232.376C112.849,232.376 65.816,193.224 65.816,145C65.816,96.776 112.849,57.624 170.78,57.624ZM170.78,101.312C199.746,101.312 223.262,120.888 223.262,145C223.262,169.112 199.746,188.688 170.78,188.688C141.814,188.688 118.298,169.112 118.298,145C118.298,120.888 141.814,101.312 170.78,101.312Z" style="fill:rgb(199,21,221);"/><!-- 
                        --><path d="M743.696,185.2C737.747,184.555 731.756,184.555 725.807,185.2L722.861,201.216C717.159,202.19 711.571,203.862 706.197,206.201L696.473,193.977C691.033,196.739 685.844,200.083 680.981,203.964L685.601,219.478C681.1,223.505 677.009,228.073 673.402,233.099L659.507,227.941C656.032,233.37 653.037,239.163 650.563,245.239L661.512,256.095C659.416,262.096 657.919,268.336 657.046,274.702L642.703,277.992C642.125,284.634 642.125,291.323 642.703,297.966L657.046,301.255C657.919,307.622 659.416,313.862 661.512,319.862L650.563,330.719C653.037,336.794 656.032,342.587 659.507,348.017L673.402,342.858C677.009,347.885 681.1,352.453 685.601,356.479L680.981,371.994C685.844,375.874 691.033,379.219 696.473,381.981L706.197,369.756C711.571,372.096 717.159,373.768 722.861,374.742L725.807,390.757C731.756,391.402 737.747,391.402 743.696,390.757L746.642,374.742C752.344,373.768 757.933,372.096 763.307,369.756L773.03,381.981C778.471,379.219 783.659,375.874 788.522,371.994L783.902,356.479C788.404,352.453 792.495,347.885 796.101,342.858L809.996,348.017C813.471,342.587 816.467,336.794 818.941,330.719L807.992,319.862C810.087,313.862 811.585,307.622 812.457,301.255L826.801,297.966C827.379,291.323 827.379,284.634 826.801,277.992L812.457,274.702C811.585,268.336 810.087,262.096 807.992,256.095L818.941,245.239C816.467,239.163 813.471,233.37 809.996,227.941L796.101,233.099C792.495,228.073 788.404,223.505 783.902,219.478L788.522,203.964C783.659,200.083 778.471,196.739 773.03,193.977L763.307,206.201C757.933,203.862 752.344,202.19 746.642,201.216L743.696,185.2ZM734.752,267.326C744.96,267.326 753.248,276.58 753.248,287.979C753.248,299.377 744.96,308.631 734.752,308.631C724.543,308.631 716.255,299.377 716.255,287.979C716.255,276.58 724.543,267.326 734.752,267.326Z" style="fill:rgb(221,97,21);"/><!-- 
                        --><path d="M1104.68,419.383C1122.96,384.433 1159.51,384.433 1177.78,401.908C1196.06,419.383 1196.06,454.333 1177.78,489.284C1164.99,515.496 1132.09,541.709 1104.68,559.184C1077.27,541.709 1044.37,515.496 1031.58,489.284C1013.3,454.333 1013.3,419.383 1031.58,401.908C1049.85,384.433 1086.4,384.433 1104.68,419.383Z" style="fill:rgb(221,212,21);"/><!-- 
                        --><path d="M1418.44,147.496C1423.69,141.596 1434.21,141.596 1439.46,144.546C1444.72,147.496 1444.72,153.397 1439.46,159.298C1435.78,163.723 1426.32,168.149 1418.44,171.099C1410.56,168.149 1401.1,163.723 1397.42,159.298C1392.16,153.397 1392.16,147.496 1397.42,144.546C1402.67,141.596 1413.18,141.596 1418.44,147.496Z" style="fill:rgb(68,221,21);"/><!-- 
                        --><path d="M402.555,569.548L419.013,583.465L410.784,596.648L424.099,601.684L417.813,624.203L404.498,619.167L404.498,635.463L384.155,635.463L384.155,619.167L370.84,624.203L364.553,601.684L377.868,596.648L369.639,583.465L386.097,569.548L394.326,582.731L402.555,569.548Z" style="fill:rgb(68,221,21);"/><!-- 
                        --><path d="M1400.85,344.716L1406.84,363.44L1418.39,357.654L1416.54,370.591L1435.92,370.591L1420.24,382.163L1429.23,391.525L1416.54,393.735L1422.53,412.458L1406.84,400.887L1400.85,412.458L1394.86,400.887L1379.17,412.458L1385.16,393.735L1372.48,391.525L1381.46,382.163L1365.78,370.591L1385.16,370.591L1383.31,357.654L1394.86,363.44L1400.85,344.716Z" style="fill:rgb(21,57,221);"/><!-- 
                        --><path d="M332.482,332.234C299.894,332.234 273.475,360.685 273.475,395.78C273.475,430.852 299.915,459.326 332.482,459.326C365.071,459.326 391.489,430.876 391.489,395.78L332.482,395.78L332.482,332.234Z" style="fill:rgb(21,57,221);"/><!-- 
                    --></g><!-- 
                --></svg><!--                   
        --></div>
  
        <br><br>
        <div class="container">
            <div class="btn-group">       
                <button class="btn btn-primary" onclick="zoom(-1);">zoom out</button>  
                <button class="btn btn-secondary" onclick="zoom(0);">reset</button>   
                <button class="btn btn-primary" onclick="zoom(1);">zoom in</button>                 
            </div>
            <div class="btn-group">
                <button class="btn btn-primary" onclick="startAnimatedPan(true);">pan left</button>   
                <button class="btn btn-secondary" onclick="resetPan();">reset</button>         
                <button class="btn btn-primary" onclick="startAnimatedPan(false);">pan right</button>   
            </div>        
        </div>

In a real situation, when zoomed and managing alignment / user interaction you may need to convert svg pixels to/from screen pixels - there are 2 ways to do this ; use a maintained/calculated screen to svg pixel ratio (faster) or use the following to convert directly to/from svg points (possibly more accurate);

/*
 * EDIT scr/screen here is the document containing the svg so the
 * following should convert pageX,pageY to svg coords
*/

    function convertCoords(x,y,toSvg){// toSvg ; true scr->svg, false svg->scr
        let pt = svg.createSVGPoint(); // svg defined elsewhere
        pt.x = x; 
        pt.y = y;    
        let screenCTM = svg.getScreenCTM() ;

        if(toSvg){
            screenCTM = screenCTM.inverse() ; 
        }
        let result =  pt.matrixTransform(screenCTM);

        return result ;    
    }        

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.