如何使用 Javascript 平滑地平移内联 SVG

How to pan an inline SVG smoothly with Javascript

在我正在创建的 Cordova/Android 应用程序中,我必须实现我自己的内联 SVG 图像的缩放和平移(不允许也不适合库)。到目前为止,我的努力如下所示。

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>

关于我的要求和实施的几点说明:

简而言之,我是如何实现平移的

突出问题

  1. 仍有可能平移图片的极端边缘并最终显示空白 space
  2. 虽然这在 phones 的台式电脑上运行良好,但我发现平移操作不太流畅
  3. 我试图处理这个我没有响应的问题——在 Window.requestAnimationFrame 处理程序中——对每一个鼠标移动都没有响应,但这只是有点帮助
    1. 在手持设备上很难平移到边缘 - 在 Chrome 中设置的桌面显示器上模仿小 phone 屏幕效果很好,因为您可以简单地继续移动超出模拟手持屏幕的虚拟边缘

如果有人能提出改进平移过程的建议,我将不胜感激。

这里有两种方法(可能还有更多);

选项 1:您可以使用它,将 svg 放入可以调整大小和滚动的容器中。

使用 svg 大小进行缩放,使用容器滚动进行平移。

(如果你愿意,你可以隐藏 scollbars 并且仍然影响 scoll 或者让它们可见)

为此,容器必须是 display:inline-blockdisplay:block(因为 display:inline 不能设置宽度或高度)。

选项 2:适用于任何情况,与容器无关。

使用 svg viewbox 缩放和平移..


注意.

选项 1 可能会更快,因为您所做的工作较少,而将更多工作留给底层本机功能。选项 1 的编码也更简单。选项 1 还负责平移的限制 - 您不能滚动超出可用范围。

但是,对于选项 1,如果您缩放(调整大小),您可能需要等待浏览器重排文档,然后才能通过设置滚动值对齐 - 滚动的可用限制直到下一次重排 - 因此要缩放并保持对齐,您可能需要调整大小并调用 requestAnimationFrame 以在滚动可用时设置滚动。


代码

使用 OP svg 并在标记中添加一个视图框。

bootstrap 仅用于按钮样式。

选项 1 示例

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>

选项 2 示例(当 运行 这段代码时,您可能想要完整页面然后缩小浏览器 window 以正确查看它 - 您可以限制容器,但这是一个不受限制的容器)

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>

在实际情况下,当缩放和管理对齐/用户交互时,您可能需要转换 svg 像素 to/from 屏幕像素 - 有两种方法可以做到这一点;使用 maintained/calculated screen to svg pixel ratio(更快)或使用以下直接转换 to/from svg 点(可能更准确);

/*
 * 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 ;    
    }