本月初在 Animation at Work Slack 上,我们讨论了如何找到一种方法让用户在 SVG 中平移。
我制作了下面的演示,以展示我如何处理这个问题。
查看 Pen Demo – SVG Panning by Louis Hoebregts (@Mamboleoo) on CodePen.
以下是如何使上述演示生效的四个步骤。
让我们更仔细地逐一检查这些步骤。
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
。
要解决此问题,我们需要计算 viewBox
和 viewport
之间的比率,并在计算新的 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 的强大功能! 请随时通过评论提供您的想法或此解决方案的替代方案来为代码做出贡献。
鸣谢
- 鸟儿设计来自 Freepik
- 特别感谢 Blake 的宝贵帮助,以及 AAW Slack 上所有热心朋友的反馈。
这是否可以与调整大小事件和浏览器尺寸结合使用,以创建“响应式 SVG”,而不是默认情况下以更详细的方式渲染?
我之所以这样问,是因为尽管我喜欢 SVG,但我认为对于横幅来说,创建具有不同样式和显示方式的不同元素的 SVG 会非常棒,具体取决于给定的屏幕空间。
当然可以!
你可以查看 Sarah Drasner 制作的这两个演示。
两者都使用媒体查询根据视窗改变 SVG 的外观。第一个还会更新 SVG 的 viewBox 以针对不同的屏幕尺寸显示特定区域!
太好了,我们现在只需要双指手势。
很酷的演示。我最近发现了这个 SVG 地图的宝藏 (https://www.mallofamerica.com/directory - 点击地图视图),但还没来得及弄清楚它是如何工作的,但这已经是个很好的开始。我喜欢它的干净程度,而且响应性也非常好。
据我所知,viewBox 在拖动时不会更新,而是对整个地图容器应用了 CSS 变换,以确保与商店链接的 HTML 也会被拖动 :)
但无论如何,它都是一个很棒的交互式地图!