SVG带transform元素鼠标调整尺寸失效,求坐标转换方案
问题描述
我有一批SVG元素,想给它们加上鼠标拖拽调整尺寸的功能。元素没有任何transform属性(比如matrix、rotate、scale这些)的时候,功能完全正常;但只要给rect加上transform="matrix(1.1,1.1,1,0,0,0)"这种属性,功能就失效了。我知道应该用matrix.inverse()把鼠标坐标转换成元素自身的空间坐标,但试了好几次都没搞成,现在根本没法正确算出拖拽起始点和当前点的x、y差值。相关代码如下:
var rect = document.getElementById('rect'); var grip = document.getElementById('grip'); function moveGrip() { var baseMatrix = rect.getCTM(), x = rect.width.baseVal.value + rect.x.baseVal.value, y = rect.height.baseVal.value + rect.y.baseVal.value; grip.cx.baseVal.value = x * baseMatrix.a + y * baseMatrix.c + baseMatrix.e; grip.cy.baseVal.value = x * baseMatrix.b + y * baseMatrix.d + baseMatrix.f; } grip.addEventListener('mousedown', onGripMouseDown); document.addEventListener('mousemove', onDocMouseMove); var startDragPoint, startDragSize; function onGripMouseDown(evt) { startDragSize = {w: rect.width.baseVal.value, h: rect.height.baseVal.value}; startDragPoint = {x: evt.clientX, y: evt.clientY}; } function onDocMouseMove(evt) { if (evt.buttons & 1) { // dragging rect.width.baseVal.value = startDragSize.w + evt.clientX - startDragPoint.x; rect.height.baseVal.value = startDragSize.h + evt.clientY - startDragPoint.y; moveGrip(); } } moveGrip();
<svg width="500" height="400"> <rect id="rect" x="20" y="20" width="200" height="100" transform="matrix(1.1,1.1,1,0,0,0)" style="fill:none;stroke: #3a2dd0; stroke-width: 2px;"></rect> <g> <circle id="grip" r="3" stroke-width="1" stroke="green"></circle> </g> </svg>
解决办法
问题出在鼠标事件的坐标是屏幕坐标系的,而元素的尺寸是基于自身局部坐标系的——当元素有transform时,这俩坐标系不匹配,必须通过逆矩阵把屏幕坐标转成元素本地的坐标才行。
具体修改步骤
- 获取逆矩阵转换鼠标坐标:在按下拖拽点和拖拽移动的函数里,用
getScreenCTM()拿到元素到屏幕的变换矩阵,再取它的逆矩阵,把鼠标的clientX/clientY转换成元素局部空间的坐标。 - 调整拖拽逻辑:
- 记录拖拽开始时的局部坐标,不是屏幕坐标
- 用局部坐标的差值来调整元素尺寸,而不是直接用屏幕坐标的差值
修改后的完整代码
JavaScript
var rect = document.getElementById('rect'); var grip = document.getElementById('grip'); var svg = document.querySelector('svg'); function moveGrip() { var baseMatrix = rect.getCTM(), x = rect.width.baseVal.value + rect.x.baseVal.value, y = rect.height.baseVal.value + rect.y.baseVal.value; grip.cx.baseVal.value = x * baseMatrix.a + y * baseMatrix.c + baseMatrix.e; grip.cy.baseVal.value = x * baseMatrix.b + y * baseMatrix.d + baseMatrix.f; } grip.addEventListener('mousedown', onGripMouseDown); document.addEventListener('mousemove', onDocMouseMove); document.addEventListener('mouseup', () => startDragPoint = null); // 加个mouseup重置拖拽状态 var startDragPoint, startDragSize; function onGripMouseDown(evt) { startDragSize = {w: rect.width.baseVal.value, h: rect.height.baseVal.value}; // 拿到逆矩阵,把起始鼠标坐标转成元素局部坐标 var inverseMatrix = rect.getScreenCTM().inverse(); startDragPoint = svg.createSVGPoint(); startDragPoint.x = evt.clientX; startDragPoint.y = evt.clientY; startDragPoint = startDragPoint.matrixTransform(inverseMatrix); } function onDocMouseMove(evt) { if (!startDragPoint) return; if (evt.buttons & 1) { // 转换当前鼠标坐标到元素局部空间 var inverseMatrix = rect.getScreenCTM().inverse(); var currentPoint = svg.createSVGPoint(); currentPoint.x = evt.clientX; currentPoint.y = evt.clientY; currentPoint = currentPoint.matrixTransform(inverseMatrix); // 用局部坐标的差值调整尺寸 rect.width.baseVal.value = startDragSize.w + (currentPoint.x - startDragPoint.x); rect.height.baseVal.value = startDragSize.h + (currentPoint.y - startDragPoint.y); moveGrip(); } } moveGrip();
HTML
<svg width="500" height="400"> <rect id="rect" x="20" y="20" width="200" height="100" transform="matrix(1.1,1.1,1,0,0,0)" style="fill:none;stroke: #3a2dd0; stroke-width: 2px;"></rect> <g> <circle id="grip" r="3" stroke-width="1" stroke="green"></circle> </g> </svg>
关键细节解释
- getScreenCTM():获取元素从自身局部坐标系到屏幕坐标系的变换矩阵
- matrix.inverse():生成逆矩阵,用来把屏幕坐标转回到元素的局部坐标系
- createSVGPoint() + matrixTransform():完成坐标转换,确保差值计算是在元素自己的坐标系里进行的,这样不管元素怎么变换,拖拽逻辑都能正常工作
- 新增的
mouseup事件:防止鼠标松开后还一直触发拖拽,避免异常
内容的提问来源于stack exchange,提问作者Andrei
相关产品推荐
相关产品推荐

