为 SVG 创建平移效果

Avatar of Louis Hoebregts
Louis Hoebregts

DigitalOcean 为您的旅程每个阶段提供云产品。 立即开始使用 $200 免费信用额度!

本月初在 Animation at Work Slack 上,我们讨论了如何找到一种方法让用户在 SVG 中平移。

我制作了下面的演示,以展示我如何处理这个问题。

查看 Pen Demo – SVG Panning by Louis Hoebregts (@Mamboleoo) on CodePen.

以下是如何使上述演示生效的四个步骤。

  1. 获取用户鼠标和触摸事件
  2. 计算鼠标相对于其原点的偏移量
  3. 保存新的 viewBox 坐标
  4. 处理动态视窗

让我们更仔细地逐一检查这些步骤。

1. 鼠标和触摸事件

要获取鼠标或触摸位置,我们首先需要在我们的 SVG 上添加事件监听器。 我们可以使用 指针事件 来处理所有类型的指针(鼠标/触摸/手写笔/…),但这些事件尚未得到所有浏览器的支持。 我们需要添加一些回退,以确保所有用户都能够拖动 SVG。

// We select the SVG into the page
var svg = document.querySelector('svg');

// If browser supports pointer events
if (window.PointerEvent) {
  svg.addEventListener('pointerdown', onPointerDown); // Pointer is pressed
  svg.addEventListener('pointerup', onPointerUp); // Releasing the pointer
  svg.addEventListener('pointerleave', onPointerUp); // Pointer gets out of the SVG area
  svg.addEventListener('pointermove', onPointerMove); // Pointer is moving
} else {
  // Add all mouse events listeners fallback
  svg.addEventListener('mousedown', onPointerDown); // Pressing the mouse
  svg.addEventListener('mouseup', onPointerUp); // Releasing the mouse
  svg.addEventListener('mouseleave', onPointerUp); // Mouse gets out of the SVG area
  svg.addEventListener('mousemove', onPointerMove); // Mouse is moving

  // Add all touch events listeners fallback
  svg.addEventListener('touchstart', onPointerDown); // Finger is touching the screen
  svg.addEventListener('touchend', onPointerUp); // Finger is no longer touching the screen
  svg.addEventListener('touchmove', onPointerMove); // Finger is moving
}

因为我们可能有触摸事件和指针事件,所以我们需要创建一个微型函数来返回第一根手指或指针的坐标。

// This function returns an object with X & Y values from the pointer event
function getPointFromEvent (event) {
  var point = {x:0, y:0};
  // If event is triggered by a touch event, we get the position of the first finger
  if (event.targetTouches) {
    point.x = event.targetTouches[0].clientX;
    point.y = event.targetTouches[0].clientY;
  } else {
    point.x = event.clientX;
    point.y = event.clientY;
  }
  
  return point;
}

页面准备好并等待任何用户交互后,我们可以开始处理 mousedown/touchstart 事件,以保存指针的原始坐标,并创建一个变量以告知我们指针是否按下。

// This variable will be used later for move events to check if pointer is down or not
var isPointerDown = false;

// This variable will contain the original coordinates when the user start pressing the mouse or touching the screen
var pointerOrigin = {
  x: 0,
  y: 0
};

// Function called by the event listeners when user start pressing/touching
function onPointerDown(event) {
  isPointerDown = true; // We set the pointer as down
  
  // We get the pointer position on click/touchdown so we can get the value once the user starts to drag
  var pointerPosition = getPointFromEvent(event);
  pointerOrigin.x = pointerPosition.x;
  pointerOrigin.y = pointerPosition.y;
}

2. 计算鼠标偏移量

现在我们有了用户开始在 SVG 内拖动的原始位置的坐标,我们可以计算当前指针位置与其原点之间的距离。 我们对 X 轴和 Y 轴都执行此操作,并将计算出的值应用于 viewBox

// We save the original values from the viewBox
var viewBox = {
  x: 0,
  y: 0,
  width: 500,
  height: 500
};

// The distances calculated from the pointer will be stored here
var newViewBox = {
  x: 0,
  y: 0
};

// Function called by the event listeners when user start moving/dragging
function onPointerMove (event) {
  // Only run this function if the pointer is down
  if (!isPointerDown) {
    return;
  }
  // This prevent user to do a selection on the page
  event.preventDefault();

  // Get the pointer position
  var pointerPosition = getPointFromEvent(event);

  // We calculate the distance between the pointer origin and the current position
  // The viewBox x & y values must be calculated from the original values and the distances
  newViewBox.x = viewBox.x - (pointerPosition.x - pointerOrigin.x);
  newViewBox.y = viewBox.y - (pointerPosition.y - pointerOrigin.y);

  // We create a string with the new viewBox values
  // The X & Y values are equal to the current viewBox minus the calculated distances
  var viewBoxString = `${newViewBox.x} ${newViewBox.y} ${viewBox.width} ${viewBox.height}`;
  // We apply the new viewBox values onto the SVG
  svg.setAttribute('viewBox', viewBoxString);
  
  document.querySelector('.viewbox').innerHTML = viewBoxString;
}

如果您不熟悉 viewBox 的概念,我建议您先阅读 Sara Soueidan 的这篇 优秀文章

3. 保存更新的 viewBox

现在 viewBox 已更新,我们需要在用户停止拖动 SVG 时保存其新值。

此步骤很重要,因为否则我们会始终从原始 viewBox 值计算指针偏移量,并且用户每次都会从起点拖动 SVG。

function onPointerUp() {
  // The pointer is no longer considered as down
  isPointerDown = false;

  // We save the viewBox coordinates based on the last pointer offsets
  viewBox.x = newViewBox.x;
  viewBox.y = newViewBox.y;
}

4. 处理动态视窗

如果我们在 SVG 上设置了自定义宽度,您可能会注意到,在下面的演示中拖动时,鸟的移动速度比您的指针快或慢。

查看 Pen Dynamic viewport – SVG Panning by Louis Hoebregts (@Mamboleoo) on CodePen.

在原始演示中,SVG 的宽度与 viewBox 的宽度完全匹配。 SVG 的实际大小也称为 viewport。 在理想情况下,当用户将指针移动 1px 时,我们希望 viewBox 平移 1px

但是,大多数情况下,SVG 具有响应式大小,并且 viewBox 很可能不会与 SVG viewport 相匹配。 如果 SVG 的宽度是 viewBox 的两倍,当用户将指针移动 1px 时,SVG 内部的图像将平移 2px

要解决此问题,我们需要计算 viewBoxviewport 之间的比率,并在计算新的 viewBox 时应用此比率。 此比率还必须在 SVG 大小可能发生变化时更新。

// Calculate the ratio based on the viewBox width and the SVG width
var ratio = viewBox.width / svg.getBoundingClientRect().width;
window.addEventListener('resize', function() {
  ratio = viewBox.width / svg.getBoundingClientRect().width;
});

一旦我们知道比率,我们需要将鼠标偏移量乘以比率,以按比例增加或减少偏移量。

function onMouseMove (e) {
  [...]
  newViewBox.x = viewBox.x - ((pointerPosition.x - pointerOrigin.x) * ratio);
  newViewBox.y = viewBox.y - ((pointerPosition.y - pointerOrigin.y) * ratio);
  [...]
}

以下是使用比 viewBox 宽度小的 viewport 的工作方式。

查看 Pen Smaller viewport – SVG Panning by Louis Hoebregts (@Mamboleoo) on CodePen.

另一个使用比 viewBox 宽度大的 viewport 的演示。

查看 Pen Bigger viewport – SVG Panning by Louis Hoebregts (@Mamboleoo) on CodePen.

[额外] 优化代码

为了使代码更简洁,我们可以使用 SVG 中的两个非常有用的概念。

SVG 点

第一个概念是使用 SVG 点 而不是基本的 Javascript 对象来保存指针的位置。 创建新的 SVG 点变量后,我们可以在其上应用一些矩阵转换,将其相对于屏幕的位置转换为相对于当前 SVG 用户单位的位置。

查看下面的代码以了解函数 getPointFromEvent()onPointerDown() 如何更改。

// Create an SVG point that contains x & y values
var point = svg.createSVGPoint();

function getPointFromEvent (event) {
  if (event.targetTouches) {
    point.x = event.targetTouches[0].clientX;
    point.y = event.targetTouches[0].clientY;
  } else {
    point.x = event.clientX;
    point.y = event.clientY;
  }
  
  // We get the current transformation matrix of the SVG and we inverse it
  var invertedSVGMatrix = svg.getScreenCTM().inverse();
  
  return point.matrixTransform(invertedSVGMatrix);
}

var pointerOrigin;
function onPointerDown(event) {
  isPointerDown = true; // We set the pointer as down
  
  // We get the pointer position on click/touchdown so we can get the value once the user starts to drag
  pointerOrigin = getPointFromEvent(event);
}

通过使用 SVG 点,您甚至不必处理应用于 SVG 的转换! 比较以下两个示例,第一个示例在对 SVG 应用旋转时会损坏,第二个示例使用 SVG 点。

查看 Pen Demo + transformation – SVG Panning by Louis Hoebregts (@Mamboleoo) on CodePen.

查看 Pen Demo Bonus + transform – SVG Panning by Louis Hoebregts (@Mamboleoo) on CodePen.

SVG 动画矩形

我们可以用来缩短代码的第二个未知 SVG 概念是使用 动画矩形

因为 viewBox 实际上被视为 SVG 矩形(x、y、宽度、高度),所以我们可以从其基本值创建一个变量,如果我们更新此变量,它将自动更新 viewBox

看看现在更新 SVG 的 viewBox 有多容易!

// We save the original values from the viewBox
var viewBox = svg.viewBox.baseVal;

function onPointerMove (event) {
  if (!isPointerDown) {
    return;
  }
  event.preventDefault();

  // Get the pointer position as an SVG Point
  var pointerPosition = getPointFromEvent(event);

  // Update the viewBox variable with the distance from origin and current position
  // We don't need to take care of a ratio because this is handled in the getPointFromEvent function
  viewBox.x -= (pointerPosition.x - pointerOrigin.x);
  viewBox.y -= (pointerPosition.y - pointerOrigin.y);
}

以下是最终演示。 看看代码现在有多短? 😀

查看 Pen Demo Bonus – SVG Panning by Louis Hoebregts (@Mamboleoo) on CodePen.

结论

此解决方案绝不是处理此类行为的唯一方法。 如果您已经在使用库来处理 SVG,它可能已经包含一个内置函数来处理它。

我希望本文能帮助您更多地了解 SVG 的强大功能! 请随时通过评论提供您的想法或此解决方案的替代方案来为代码做出贡献。

鸣谢