如何用D3.js实现可拖拽、缩放、旋转的交互式矩形?
D3.js 实现可拖拽、缩放、旋转的矩形
需求说明
- 在矩形四个角落添加小圆圈,作为缩放/旋转锚点
- 拖拽角落圆圈时,矩形随鼠标移动进行缩放
- 按住Shift键拖拽角落圆圈时,触发旋转操作而非缩放
- 拖拽矩形非锚点区域可移动整个矩形
原代码存在的问题
- 仅实现了3个锚点,未覆盖四个角落
- 未区分Shift键触发旋转的逻辑
- 旋转后矩形的坐标计算逻辑错误,导致缩放、锚点位置更新异常
- 缺少矩形整体移动的拖拽功能
完整实现代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Rotating Rectangle with D3</title> <script src="https://d3js.org/d3.v5.min.js"></script> <style> .rect-handle { cursor: grab; } .rect-body { cursor: move; } </style> </head> <body> <script> // SVG容器设置 const svgWidth = 600; const svgHeight = 600; const svg = d3.select("body") .append("svg") .attr("width", svgWidth) .attr("height", svgHeight) .style("border", "1px solid #ccc"); // 矩形状态管理(用对象统一存储,避免分散变量) const rectState = { cx: svgWidth / 2, cy: svgHeight / 2, width: 150, height: 100, angle: 0 }; // 创建矩形组(把矩形和锚点放在同一个g元素下,方便统一变换) const rectGroup = svg.append("g"); // 创建主矩形 const rectangle = rectGroup.append("rect") .attr("class", "rect-body") .attr("width", rectState.width) .attr("height", rectState.height) .attr("fill", "#4287f5") .attr("stroke", "#222") .attr("stroke-width", 2) .attr("x", -rectState.width / 2) .attr("y", -rectState.height / 2); // 创建四个角落的锚点 const anchors = rectGroup.selectAll(".rect-handle") .data([ {type: "top-left", x: -rectState.width/2, y: -rectState.height/2}, {type: "top-right", x: rectState.width/2, y: -rectState.height/2}, {type: "bottom-right", x: rectState.width/2, y: rectState.height/2}, {type: "bottom-left", x: -rectState.width/2, y: rectState.height/2} ]) .enter() .append("circle") .attr("class", "rect-handle") .attr("cx", d => d.x) .attr("cy", d => d.y) .attr("r", 8) .attr("fill", "#ff5252") .attr("stroke", "#222") .attr("stroke-width", 2); // 更新矩形组的变换(统一处理平移和旋转) function updateTransform() { rectGroup.attr("transform", `translate(${rectState.cx}, ${rectState.cy}) rotate(${rectState.angle})`); } // 更新锚点位置(缩放后同步更新) function updateAnchors() { anchors .attr("cx", d => d.type.includes("left") ? -rectState.width/2 : rectState.width/2) .attr("cy", d => d.type.includes("top") ? -rectState.height/2 : rectState.height/2); } // 初始化变换 updateTransform(); // 矩形移动拖拽逻辑 rectangle.call(d3.drag() .on("drag", function() { rectState.cx += d3.event.dx; rectState.cy += d3.event.dy; updateTransform(); }) ); // 锚点拖拽逻辑(缩放/旋转) anchors.call(d3.drag() .on("start", function(d) { // 记录拖拽起始状态 d.startWidth = rectState.width; d.startHeight = rectState.height; d.startAngle = rectState.angle; d.startMouseX = d3.event.x; d.startMouseY = d3.event.y; d.startCx = rectState.cx; d.startCy = rectState.cy; }) .on("drag", function(d) { if (d3.event.sourceEvent.shiftKey) { // 按住Shift键:旋转逻辑 // 计算鼠标相对于矩形中心的角度 const dx = d3.event.x - rectState.cx; const dy = d3.event.y - rectState.cy; const newAngle = Math.atan2(dy, dx) * 180 / Math.PI; // 计算角度变化量 const startDx = d.startMouseX - d.startCx; const startDy = d.startMouseY - d.startCy; const startAngle = Math.atan2(startDy, startDx) * 180 / Math.PI; rectState.angle = d.startAngle + (newAngle - startAngle); updateTransform(); } else { // 普通拖拽:缩放逻辑 // 将鼠标坐标转换为矩形局部坐标系(逆变换) const mouseLocal = d3.mouse(rectGroup.node()); let newWidth, newHeight; switch(d.type) { case "top-left": newWidth = d.startWidth - mouseLocal[0]; newHeight = d.startHeight - mouseLocal[1]; break; case "top-right": newWidth = mouseLocal[0] + d.startWidth/2; newHeight = d.startHeight - mouseLocal[1]; break; case "bottom-right": newWidth = mouseLocal[0] + d.startWidth/2; newHeight = mouseLocal[1] + d.startHeight/2; break; case "bottom-left": newWidth = d.startWidth - mouseLocal[0]; newHeight = mouseLocal[1] + d.startHeight/2; break; } // 限制最小尺寸 rectState.width = Math.max(20, newWidth); rectState.height = Math.max(20, newHeight); // 更新矩形尺寸和锚点位置 rectangle.attr("width", rectState.width).attr("height", rectState.height) .attr("x", -rectState.width/2).attr("y", -rectState.height/2); updateAnchors(); } }) ); </script> </body> </html>
关键实现说明
- 状态统一管理:用
rectState对象存储矩形的中心坐标、宽高、旋转角度,避免分散变量导致的同步问题 - 组元素优化:将矩形和锚点放在同一个
<g>元素下,通过修改组的transform属性统一处理平移和旋转,简化坐标计算 - 锚点逻辑整合:四个锚点共用一套拖拽逻辑,通过数据区分角落类型,根据拖拽方向计算新的宽高
- Shift键判断:在拖拽事件中通过
d3.event.sourceEvent.shiftKey判断是否按住Shift,切换旋转/缩放模式 - 坐标转换:旋转时将鼠标坐标转换为矩形局部坐标系,确保缩放逻辑不受旋转角度影响
- 边界限制:设置矩形最小尺寸,避免缩放到过小导致交互异常
内容的提问来源于stack exchange,提问作者NIYAS RAHMAN
相关产品推荐
相关产品推荐

