You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用D3.js实现可拖拽、缩放、旋转的交互式矩形?

D3.js 实现可拖拽、缩放、旋转的矩形

需求说明

  • 在矩形四个角落添加小圆圈,作为缩放/旋转锚点
  • 拖拽角落圆圈时,矩形随鼠标移动进行缩放
  • 按住Shift键拖拽角落圆圈时,触发旋转操作而非缩放
  • 拖拽矩形非锚点区域可移动整个矩形

原代码存在的问题

  1. 仅实现了3个锚点,未覆盖四个角落
  2. 未区分Shift键触发旋转的逻辑
  3. 旋转后矩形的坐标计算逻辑错误,导致缩放、锚点位置更新异常
  4. 缺少矩形整体移动的拖拽功能

完整实现代码

<!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>

关键实现说明

  1. 状态统一管理:用rectState对象存储矩形的中心坐标、宽高、旋转角度,避免分散变量导致的同步问题
  2. 组元素优化:将矩形和锚点放在同一个<g>元素下,通过修改组的transform属性统一处理平移和旋转,简化坐标计算
  3. 锚点逻辑整合:四个锚点共用一套拖拽逻辑,通过数据区分角落类型,根据拖拽方向计算新的宽高
  4. Shift键判断:在拖拽事件中通过d3.event.sourceEvent.shiftKey判断是否按住Shift,切换旋转/缩放模式
  5. 坐标转换:旋转时将鼠标坐标转换为矩形局部坐标系,确保缩放逻辑不受旋转角度影响
  6. 边界限制:设置矩形最小尺寸,避免缩放到过小导致交互异常

内容的提问来源于stack exchange,提问作者NIYAS RAHMAN

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 23:32:12