一、SVG中拖拉的概念
我们试着用一张图来说明整个svg画布在拖移的概念
将svg画布从左边拖到右边
拖拽的过程,其实知识 viewBox 中 x-min 的改变。但为了要计算 x-min 应该要给它多少,我们会需要几个连续的步骤。我用下面这张图来说明要进行的步骤,图形当中的数字标示是步骤的顺序:
- 为了之后坐标系统的转换,我们要先取当前svg元素的 viewBox 值,假设一开始的值是 (x, y, w, h),这里我们只需要关注 x,简称 viewBoxX0
- 通过上一篇 - 彻底理解SVG图片缩放与位移的原理 1 所说明的相关内容,此时我们可以去的鼠标点击下去时的 clientX 的值,简称为 clientX0,在上图中对应到的值是10.
- 我们可以把 clientX 的值转成 svg 坐标系统中的值,简称为 svgX0,这里对应的是值是20
- 按住鼠标拖动画布,在移动的过程中可以得到当前鼠标坐标的 clientX 值,简称为 clientX1,这里对应的值是20
- 同样,我们可以把 clientX 的值转成 svg 坐标系统中的值,简称为 svgX1,这里对应的值是40.
- 我们可以计算鼠标在拖拉时,在svg坐标系统中的位移(svgX0 - svgX1),这里就会是 (20 - 40)
- 我们可以得到最终 viewBox x要带入的值就是 viewBoxX0 + (xvgX0 - svgX1),也就是 viewBoxX0+(20-40),从这个意义上来说,就是将svg的坐标系统整个的向右移动了20个单位,所以整个svg的坐标就向右移动了20个单位。
同理,我们也可以知道,如果进行上下拖拉的话, viewBox 的 y 会改变,而最终的 viewBox 中 y 值就是 viewBoxY0 + (svgY0 - svgY1)
转成js代码如下:
js
/**
开始:鼠标拖拉的效果
**/
let moving
// 鼠标点下,开始拖拉
function mouseDown(e){
moving = true
}
// 拖拉的移动过程
function drag(e){
if(moving === true){
// 1. 取得一开始的 viewBox 值,原本是字符串,分割成阵列,方便后面计算
let startViewBox = svg.getAttribute('viewBox').split(' ').map( n => parseFloat(n))
// 2. 取得鼠标当前 viewport 中 client 坐标值
let startClient = {
x: e.clientX,
y: e.clientY
}
// 3. 计算对应的 SVG 坐标值
let newSVGPoint = svg.createSVGPoint()
let CTM = svg.getScreenCTM()
newSVGPoint.x = startClient.x
newSVGPoint.y = startClient.y
let startSVGPoint = newSVGPoint.matrixTransform(CTM.inverse())
// 4. js拖拽后鼠标所在的 viewport client 坐标值
let moveToClient = {
x: e.clientX + e.movementX, // movement 可以取得鼠标位移量
y: e.clientY + e.movementY
}
// 5. 计算对应回去的 SVG 坐标值
newSVGPoint = svg.createSVGPoint()
CTM = svg.getScreenCTM()
newSVGPoint.x = moveToClient.x
newSVGPoint.y = moveToClient.y
let moveToSVGPoint = newSVGPoint.matrixTransform(CTM.inverse())
// 6. 计算位移量
let delta = {
dx: startSVGPoint.x - moveToSVGPoint.x,
dy: startSVGPoint.y - moveToSVGPoint.y
}
// 7. 设定新的 viewBox 值
let moveToViewBox = `${startViewBox[0] + delta.dx} ${startViewBox[1] + delta.dy} ${startViewBox[2]} ${startViewBox[3]}`
svg.setAttribute('viewBox', moveToViewBox)
console.log(moveToViewBox)
}
}
// 鼠标点击结束(拖拽結束)
function mouseUp(){
moving = false
showViewBox()
} // 整个过程结束
// 在svg元素上监听事件
svgElement.addEventListener('mousedown', mouseDown, false)
svgElement.addEventListener('mousemove', drag, false)
svgElement.addEventListener('mouseup', mouseUp, false)
二、SVG 中缩放的概念
缩放的做法,其实同样是通过改变 viewBox 的设定值,只是上面改变的是 <min-x>
和 <min-y>
,缩放要改变的是 <width>
和 <height>
。
另外,上一篇文中提到的一个很重要的概念,就是通过 viewBox 来缩放时,实际上它是从整个 svg 元素的左上角(下图中橘色的点)进行缩放:
我们通过下面一张图可以更清楚的看到为什么在缩放的过程中,一开始的圆点为什么会跑到那个位置。为了简化理解,我们还是先只看 x 方向。
如图所示:在一开始的时候,svg 坐标系统中的1单位等于 viewport 坐标系统中的 1px,圆点对应过去的 clientX是10,将坐标系统转换后会得到 svg的x是20;在缩放的过程中,会以svg元素的左上角为原点进行缩放;缩放后,整个svg坐标系统的单位尺寸就改变了,在这里因为我们放大了两倍,所以svg坐标系统的1单位就会变成viewport坐标系统的2px,但是圆点的svg x 仍然是20。
这是因为:
- 以svg左上角原点做缩放(这个点其实就是当时 viewBox的
min-x
和min-y
的值) - svg坐标系统对应会viewport坐标系统的单位尺寸已经改变。
所以,这个圆点在放大的过程中,被往右下方移动了(放大两倍是利用让viewBox的 width/2 和 height/2),这个情况会让我们点击的点,在缩放过程中相当于 viewport Client的坐标值会一直在改变,如下图中的圆圈1。 为了不让使用者在缩放时,觉得我们点击的那个店距离我们越来越远,所以我们在缩放完后要再把这个点搬回使用者点击的那个起始点,让使用者感觉这个点看起来像是在原地缩放,如下图中的圆圈2。
了解了这个概念后,我们可以尝试把步骤列出来,整个流程会如下图所示:
- 取得一开始的viewBox,这样我们就知道缩放的依据,简称viewBox0,其中的width简称为 viewBox0.width。
- 取得鼠标执行缩放位置的viewport client坐标,并取得svg坐标,简称 clientX0和svgX0
- 进行缩放,如果让原本的尺寸缩放2倍的话,width和height就要除以2;假设 r 为我们缩放的倍率,缩放后的viewBox为 viewBox1;所以 viewBox1.width = viewBox1.width / r,viewBox1.height = viewBox1.height / r。
- 将一开始鼠标执行缩放位置的viewport client坐标(也就是 clientX1),转为为对应的svg坐标,简称svgX1.
- 取得在缩放过程中该圆点的位移量:svgX0 - svgX1
- 得到最终的viewBox2: viewBoxX2 = viewBoxX0 + (svgX0 - svgX1),同理:viewBoxY2 = viewBoxY0 + (svgY0 - svgY1)
转成js代码如下:
js
/*
开始:鼠标缩放的效果
*/
function zoom(e){
// 1.取得一卡死的 viewBox。
let startViewBox = svg.getAttribute('viewBox').split(' ').map( n => parseFloat(n))
// 2.取得鼠标执行缩放位置的 viewPort Client 坐标,並利用 CTM 取得 SVG 对应的坐标。
// 2.1 取得鼠标执行缩放的位置
let startClient = {
x: e.clientX,
y: e.clientY
}
// 2.2 转换成 SVG 坐标系统中的 SVG 坐标点
let newSVGPoint = svg.createSVGPoint()
let CTM = svg.getScreenCTM()
newSVGPoint.x = startClient.x
newSVGPoint.y = startClient.y
let startSVGPoint = newSVGPoint.matrixTransform(CTM.inverse())
// 3.进行缩放,如果要让原本的尺寸缩放两倍的话。
// 3.1 设定缩放倍率
let r
if (e.deltaY > 0) {
r = 0.9
} else if (e.deltaY < 0) {
r = 1.1
} else {
r = 1
}
// 3.2 進行缩放
svg.setAttribute('viewBox', `${startViewBox[0]} ${startViewBox[1]} ${startViewBox[2] * r} ${startViewBox[3] * r}`)
// 4.将一开始鼠标的执行缩放位置的 viewPort Client 坐标利用新的 CTM ,转换出对应的 SVG 坐标。
CTM = svg.getScreenCTM()
let moveToSVGPoint = newSVGPoint.matrixTransform(CTM.inverse())
// 5.取得在缩放过程中该圆点的位移量 `(svgX0 - svgX1)`。
let delta = {
dx: startSVGPoint.x - moveToSVGPoint.x,
dy: startSVGPoint.y - moveToSVGPoint.y
}
// 6.设定最终的 viewBox2
let middleViewBox = svg.getAttribute('viewBox').split(' ').map( n => parseFloat(n))
let moveBackViewBox = `${middleViewBox[0] + delta.dx} ${middleViewBox[1] + delta.dy} ${middleViewBox[2]} ${middleViewBox[3]}`
svg.setAttribute('viewBox', moveBackViewBox)
// 更新 viewBox
showViewBox()
} // 結束
// 监听事件
svgElement.addEventListener('wheel', zoom, false)
理解了svg缩放和位移的原理后,我们就能够更好的使用svg图片。