在 javascript 中创建 resizable/draggable/rotate 视图

Creating a resizable/draggable/rotate view in javascript

我一直在尝试在 Javascript 中创建这样的东西:

如您所见,容器可以拖动、旋转和调整大小。大多数事情都可以正常工作,但是旋转容器时调整容器大小会产生奇怪的输出。

我希望这会发生:

相反,我得到了这个:

完整代码如下:

https://jsfiddle.net/c0krownz/

或者,

var box = document.getElementById("box");
var boxWrapper = document.getElementById("box-wrapper");

var initX, initY, mousePressX, mousePressY, initW, initH, initRotate;

function repositionElement(x, y) {
    boxWrapper.style.left = x;
    boxWrapper.style.top = y;
}

function resize(w, h) {
    box.style.width = w + 'px';
    box.style.height = h + 'px';
    boxWrapper.style.width = w;
    boxWrapper.style.height = h;
}


function getCurrentRotation(el) {
    var st = window.getComputedStyle(el, null);
    var tm = st.getPropertyValue("-webkit-transform") ||
        st.getPropertyValue("-moz-transform") ||
        st.getPropertyValue("-ms-transform") ||
        st.getPropertyValue("-o-transform") ||
        st.getPropertyValue("transform")
    "none";
    if (tm != "none") {
        var values = tm.split('(')[1].split(')')[0].split(',');
        var angle = Math.round(Math.atan2(values[1], values[0]) * (180 / Math.PI));
        return (angle < 0 ? angle + 360 : angle);
    }
    return 0;
}

function rotateBox(deg) {
    boxWrapper.style.transform = `rotate(${deg}deg)`;
}

// drag support
boxWrapper.addEventListener('mousedown', function (event) {
    if (event.target.className.indexOf("dot") > -1) {
        return;
    }

    initX = this.offsetLeft;
    initY = this.offsetTop;
    mousePressX = event.clientX;
    mousePressY = event.clientY;


    function eventMoveHandler(event) {
        repositionElement(initX + (event.clientX - mousePressX) + 'px',
            initY + (event.clientY - mousePressY) + 'px');
    }

    boxWrapper.addEventListener('mousemove', eventMoveHandler, false);

    window.addEventListener('mouseup', function () {
        boxWrapper.removeEventListener('mousemove', eventMoveHandler, false);
    }, false);

}, false);
// done drag support

// handle resize
var rightMid = document.getElementById("right-mid");
var leftMid = document.getElementById("left-mid");
var topMid = document.getElementById("top-mid");
var bottomMid = document.getElementById("bottom-mid");

var leftTop = document.getElementById("left-top");
var rightTop = document.getElementById("right-top");
var rightBottom = document.getElementById("right-bottom");
var leftBottom = document.getElementById("left-bottom");

function resizeHandler(event, left = false, top = false, xResize = false, yResize = false) {
    initX = boxWrapper.offsetLeft;
    initY = boxWrapper.offsetTop;
    mousePressX = event.clientX;
    mousePressY = event.clientY;

    initW = box.offsetWidth;
    initH = box.offsetHeight;

    initRotate = getCurrentRotation(boxWrapper);

    function eventMoveHandler(event) {
        var wDiff = event.clientX - mousePressX;
        var hDiff = event.clientY - mousePressY;

        var newW = initW, newH = initH, newX = initX, newY = initY;

        if (xResize) {
            if (left) {
                newW = initW - wDiff;
                newX = initX + wDiff;
            } else {
                newW = initW + wDiff;
            }
        }

        if (yResize) {
            if (top) {
                newH = initH - hDiff;
                newY = initY + hDiff;
            } else {
                newH = initH + hDiff;
            }
        }

        resize(newW, newH);
        repositionElement(newX, newY);
    }

    window.addEventListener('mousemove', eventMoveHandler, false);

    window.addEventListener('mouseup', function () {
        window.removeEventListener('mousemove', eventMoveHandler, false);
    }, false);
}


rightMid.addEventListener('mousedown', e => resizeHandler(e, false, false, true, false));
leftMid.addEventListener('mousedown', e => resizeHandler(e, true, false, true, false));
topMid.addEventListener('mousedown', e => resizeHandler(e, false, true, false, true));
bottomMid.addEventListener('mousedown', e => resizeHandler(e, false, false, false, true));
leftTop.addEventListener('mousedown', e => resizeHandler(e, true, true, true, true));
rightTop.addEventListener('mousedown', e => resizeHandler(e, false, true, true, true));
rightBottom.addEventListener('mousedown', e => resizeHandler(e, false, false, true, true));
leftBottom.addEventListener('mousedown', e => resizeHandler(e, true, false, true, true));

// handle rotation
var rotate = document.getElementById("rotate");
rotate.addEventListener('mousedown', function (event) {
    // if (event.target.className.indexOf("dot") > -1) {
    //     return;
    // }

    initX = this.offsetLeft;
    initY = this.offsetTop;
    mousePressX = event.clientX;
    mousePressY = event.clientY;


    var arrow = document.querySelector("#box");
    var arrowRects = arrow.getBoundingClientRect();
    var arrowX = arrowRects.left + arrowRects.width / 2;
    var arrowY = arrowRects.top + arrowRects.height / 2;

    function eventMoveHandler(event) {
        var angle = Math.atan2(event.clientY - arrowY, event.clientX - arrowX) + Math.PI / 2;
        rotateBox(angle * 180 / Math.PI);
    }

    window.addEventListener('mousemove', eventMoveHandler, false);

    window.addEventListener('mouseup', function () {
        window.removeEventListener('mousemove', eventMoveHandler, false);
    }, false);

}, false);



resize(300, 200);
repositionElement(100, 100);
.box {
    background-color: #00BCD4;
    position: relative;
    user-select: none;
}

.box-wrapper {
    position: absolute;
    transform-origin: center center;
    user-select: none;
}

.dot {
    height: 10px;
    width: 10px;
    background-color: #1E88E5;
    position: absolute;
    border-radius: 100px;
    border: 1px solid white;
    user-select: none;
}

.dot:hover {
    background-color: #0D47A1;
}

.dot.left-top {
    top: -5px;
    left: -5px;
    /* cursor: nw-resize; */
}

.dot.left-bottom {
    bottom: -5px;
    left: -5px;
    /* cursor: sw-resize; */
}

.dot.right-top {
    top: -5px;
    right: -5px;
    /* cursor: ne-resize; */
}

.dot.right-bottom {
    bottom: -5px;
    right: -5px;
    /* cursor: se-resize; */
}

.dot.top-mid {
    top: -5px;
    left: calc(50% - 5px);
    /* cursor: n-resize; */
}

.dot.left-mid {
    left: -5px;
    top: calc(50% - 5px);
    /* cursor: w-resize; */
}

.dot.right-mid {
    right: -5px;
    top: calc(50% - 5px);
    /* cursor: e-resize; */
}

.dot.bottom-mid {
    bottom: -5px;
    left: calc(50% - 5px);
    /* cursor: s-resize; */
}

.dot.rotate {
    top: -30px;
    left: calc(50% - 5px);
    cursor: url('https://findicons.com/files/icons/1620/crystal_project/16/rotate_ccw.png'), auto;
}

.rotate-link {
    position: absolute;
    width: 1px;
    height: 15px;
    background-color: #1E88E5;
    top: -20px;
    left: calc(50% + 0.5px);
    z-index: -1;
}
<div class="box-wrapper" id="box-wrapper">
    <div class="box" id="box">
        <div class="dot rotate" id="rotate"></div>
        <div class="dot left-top" id="left-top"></div>
        <div class="dot left-bottom" id="left-bottom"></div>
        <div class="dot top-mid" id="top-mid"></div>
        <div class="dot bottom-mid" id="bottom-mid"></div>
        <div class="dot left-mid" id="left-mid"></div>
        <div class="dot right-mid" id="right-mid"></div>
        <div class="dot right-bottom" id="right-bottom"></div>
        <div class="dot right-top" id="right-top"></div>
        <div class="rotate-link"></div>
    </div>
</div>

分配CSS单位

当你设置element.style.topelement.style.left时,你需要指定单位(通常是像素,px,当做这种类型的元素转换时)。在你的例子中,你只在 eventMoveHandler 中设置单位,这使得它只在移动处理程序中工作。

在下面的代码片段中,我将其更改为自动将 px 添加到 repositionElement,并从 eventMoveHandler 中删除了单位。我还删除了 resize 中的 boxWrapper.style.width = w;boxWrapper.style.height = h;,因为它们没有单位,并且不清楚在哪里使用了 boxWrapper 维度。

重新分配坐标

对我来说,从盒子的中心来考虑这个问题更容易。您的原始代码使用预旋转的左上角来跟踪位置,这在旋转的矩形上变得很难想象。另一方面,中心永远是中心。要使用中心,我 added/changed 这个 css:

.box {
    transform: translate(-50%, -50%);
}
.box-wrapper {
    transform-origin: top left; /* changed from `center center` */
}

它还稍微简化了 resizeHandler/eventMoveHandler 中的代码:

if (xResize) {
    if (left) {
        newW = initW - wDiff;
    } else {
        newW = initW + wDiff;
    }
    newX += 0.5 * wDiff;
}
if (yResize) {
    if (top) {
        newH = initH - hDiff;
    } else {
        newH = initH + hDiff;
    }
    newY += 0.5 * hDiff;
}

现在box-wrapperstyle.topstyle.left坐标实际上是在box的中心。如果这个坐标系不起作用,我们可以重新访问它。

调整大小时考虑旋转

从这里开始,我们需要在调整框的大小时考虑到框的旋转。例如,当盒子旋转 90 度时,所有的 x 变化都变成 y 变化。要转换它们,您可以使用 Math.cosMath.sin

var initRadians = initRotate * Math.PI / 180;
var cosFraction = Math.cos(initRadians);
var sinFraction = Math.sin(initRadians);
//...
var wDiff = (event.clientX - mousePressX);
var hDiff = (event.clientY - mousePressY);
var rotatedWDiff = cosFraction * wDiff + sinFraction * hDiff;
var rotatedHDiff = cosFraction * hDiff - sinFraction * wDiff;
//...
if (xResize) {
    if (left) {
        newW = initW - rotatedWDiff;
    } else {
        newW = initW + rotatedWDiff;
    }
    //...
}
if (yResize) {
    if (top) {
        newH = initH - rotatedHDiff;
    } else {
        newH = initH + rotatedHDiff;
    }
    //...
}

另外,当你纠正你的位置时,你也应该使用 sin 和 cos 分数,因为 style.topstyle.left 设置的位置通常不考虑旋转:

if (xResize) {
   //...
   newX += 0.5 * rotatedWDiff * cosFraction;
   newY += 0.5 * rotatedWDiff * sinFraction;
}
if (yResize) {
   //...
   newX -= 0.5 * rotatedHDiff * sinFraction;
   newY += 0.5 * rotatedHDiff * cosFraction;
}

应用最小宽度和高度

正如评论中所指出的,当拖动的边缘或角超出锚定边缘时,行为很奇怪。在这种情况下,您希望框被翻转,或者框停止调整大小。我将在这里使用最小宽度和高度,因为实现起来似乎更简单。

const minWidth = 40;
const minHeight = 40;
//...
if (xResize) {
    if (left) {
        newW = initW - rotatedWDiff;
        if (newW < minWidth) {
          newW = minWidth;
          rotatedWDiff = initW - minWidth;
        }
    } else {
        newW = initW + rotatedWDiff;
        if (newW < minWidth) {
          newW = minWidth;
          rotatedWDiff = minWidth - initW;
        }
    }
    //..
}
if (yResize) {
    if (top) {
        newH = initH - rotatedHDiff;
        if (newH < minHeight) {
          newH = minHeight;
          rotatedHDiff = initH - minHeight;
        }
    } else {
        newH = initH + rotatedHDiff;
        if (newH < minHeight) {
          newH = minHeight;
          rotatedHDiff = minHeight - initH;
        }
    }
    //...
}
旁白:删除事件侦听器

与问题的核心无关的是删除事件侦听器。具有讽刺意味的是,删除事件监听器的代码在 mouseup:

中留下了一个事件监听器
window.addEventListener('mouseup', function() {
    window.removeEventListener('mousemove', eventMoveHandler, false);
}, false);

这不是什么大问题,因为如果重复 运行 函数不会做太多事情,并且闭包不会真正占用那么多内存。但要真正清理它,我们可以将其更改为:

window.addEventListener('mouseup', function eventEndHandler() {
    window.removeEventListener('mousemove', eventMoveHandler, false);
    window.removeEventListener('mouseup', eventEndHandler, false);
}, false);

结果

加在一起,看起来像:

var box = document.getElementById("box");
var boxWrapper = document.getElementById("box-wrapper");
const minWidth = 40;
const minHeight = 40;


var initX, initY, mousePressX, mousePressY, initW, initH, initRotate;

function repositionElement(x, y) {
    boxWrapper.style.left = x + 'px';
    boxWrapper.style.top = y + 'px';
}

function resize(w, h) {
    box.style.width = w + 'px';
    box.style.height = h + 'px';
}


function getCurrentRotation(el) {
    var st = window.getComputedStyle(el, null);
    var tm = st.getPropertyValue("-webkit-transform") ||
        st.getPropertyValue("-moz-transform") ||
        st.getPropertyValue("-ms-transform") ||
        st.getPropertyValue("-o-transform") ||
        st.getPropertyValue("transform")
    "none";
    if (tm != "none") {
        var values = tm.split('(')[1].split(')')[0].split(',');
        var angle = Math.round(Math.atan2(values[1], values[0]) * (180 / Math.PI));
        return (angle < 0 ? angle + 360 : angle);
    }
    return 0;
}

function rotateBox(deg) {
    boxWrapper.style.transform = `rotate(${deg}deg)`;
}

// drag support
boxWrapper.addEventListener('mousedown', function (event) {
    if (event.target.className.indexOf("dot") > -1) {
        return;
    }

    initX = this.offsetLeft;
    initY = this.offsetTop;
    mousePressX = event.clientX;
    mousePressY = event.clientY;


    function eventMoveHandler(event) {
        repositionElement(initX + (event.clientX - mousePressX),
            initY + (event.clientY - mousePressY));
    }

    boxWrapper.addEventListener('mousemove', eventMoveHandler, false);
    window.addEventListener('mouseup', function eventEndHandler() {
        boxWrapper.removeEventListener('mousemove', eventMoveHandler, false);
        window.removeEventListener('mouseup', eventEndHandler);
    }, false);

}, false);
// done drag support

// handle resize
var rightMid = document.getElementById("right-mid");
var leftMid = document.getElementById("left-mid");
var topMid = document.getElementById("top-mid");
var bottomMid = document.getElementById("bottom-mid");

var leftTop = document.getElementById("left-top");
var rightTop = document.getElementById("right-top");
var rightBottom = document.getElementById("right-bottom");
var leftBottom = document.getElementById("left-bottom");

function resizeHandler(event, left = false, top = false, xResize = false, yResize = false) {
    initX = boxWrapper.offsetLeft;
    initY = boxWrapper.offsetTop;
    mousePressX = event.clientX;
    mousePressY = event.clientY;

    initW = box.offsetWidth;
    initH = box.offsetHeight;

    initRotate = getCurrentRotation(boxWrapper);
    var initRadians = initRotate * Math.PI / 180;
    var cosFraction = Math.cos(initRadians);
    var sinFraction = Math.sin(initRadians);
    function eventMoveHandler(event) {
        var wDiff = (event.clientX - mousePressX);
        var hDiff = (event.clientY - mousePressY);
        var rotatedWDiff = cosFraction * wDiff + sinFraction * hDiff;
        var rotatedHDiff = cosFraction * hDiff - sinFraction * wDiff;

        var newW = initW, newH = initH, newX = initX, newY = initY;

        if (xResize) {
            if (left) {
                newW = initW - rotatedWDiff;
                if (newW < minWidth) {
                  newW = minWidth;
                  rotatedWDiff = initW - minWidth;
                }
            } else {
                newW = initW + rotatedWDiff;
                if (newW < minWidth) {
                  newW = minWidth;
                  rotatedWDiff = minWidth - initW;
                }
            }
            newX += 0.5 * rotatedWDiff * cosFraction;
            newY += 0.5 * rotatedWDiff * sinFraction;
        }

        if (yResize) {
            if (top) {
                newH = initH - rotatedHDiff;
                if (newH < minHeight) {
                  newH = minHeight;
                  rotatedHDiff = initH - minHeight;
                }
            } else {
                newH = initH + rotatedHDiff;
                if (newH < minHeight) {
                  newH = minHeight;
                  rotatedHDiff = minHeight - initH;
                }
            }
            newX -= 0.5 * rotatedHDiff * sinFraction;
            newY += 0.5 * rotatedHDiff * cosFraction;
        }

        resize(newW, newH);
        repositionElement(newX, newY);
    }


    window.addEventListener('mousemove', eventMoveHandler, false);
    window.addEventListener('mouseup', function eventEndHandler() {
        window.removeEventListener('mousemove', eventMoveHandler, false);
        window.removeEventListener('mouseup', eventEndHandler);
    }, false);
}


rightMid.addEventListener('mousedown', e => resizeHandler(e, false, false, true, false));
leftMid.addEventListener('mousedown', e => resizeHandler(e, true, false, true, false));
topMid.addEventListener('mousedown', e => resizeHandler(e, false, true, false, true));
bottomMid.addEventListener('mousedown', e => resizeHandler(e, false, false, false, true));
leftTop.addEventListener('mousedown', e => resizeHandler(e, true, true, true, true));
rightTop.addEventListener('mousedown', e => resizeHandler(e, false, true, true, true));
rightBottom.addEventListener('mousedown', e => resizeHandler(e, false, false, true, true));
leftBottom.addEventListener('mousedown', e => resizeHandler(e, true, false, true, true));

// handle rotation
var rotate = document.getElementById("rotate");
rotate.addEventListener('mousedown', function (event) {
    // if (event.target.className.indexOf("dot") > -1) {
    //     return;
    // }

    initX = this.offsetLeft;
    initY = this.offsetTop;
    mousePressX = event.clientX;
    mousePressY = event.clientY;


    var arrow = document.querySelector("#box");
    var arrowRects = arrow.getBoundingClientRect();
    var arrowX = arrowRects.left + arrowRects.width / 2;
    var arrowY = arrowRects.top + arrowRects.height / 2;

    function eventMoveHandler(event) {
        var angle = Math.atan2(event.clientY - arrowY, event.clientX - arrowX) + Math.PI / 2;
        rotateBox(angle * 180 / Math.PI);
    }

    window.addEventListener('mousemove', eventMoveHandler, false);

    window.addEventListener('mouseup', function eventEndHandler() {
        window.removeEventListener('mousemove', eventMoveHandler, false);
        window.removeEventListener('mouseup', eventEndHandler);
    }, false);
}, false);

resize(300, 200);
repositionElement(200, 200);
.box {
    background-color: #00BCD4;
    position: relative;
    user-select: none;
    transform: translate(-50%, -50%);
}

.box-wrapper {
    position: absolute;
    transform-origin: top left;
    user-select: none;
}

.dot {
    height: 10px;
    width: 10px;
    background-color: #1E88E5;
    position: absolute;
    border-radius: 100px;
    border: 1px solid white;
    user-select: none;
}

.dot:hover {
    background-color: #0D47A1;
}

.dot.left-top {
    top: -5px;
    left: -5px;
    /* cursor: nw-resize; */
}

.dot.left-bottom {
    bottom: -5px;
    left: -5px;
    /* cursor: sw-resize; */
}

.dot.right-top {
    top: -5px;
    right: -5px;
    /* cursor: ne-resize; */
}

.dot.right-bottom {
    bottom: -5px;
    right: -5px;
    /* cursor: se-resize; */
}

.dot.top-mid {
    top: -5px;
    left: calc(50% - 5px);
    /* cursor: n-resize; */
}

.dot.left-mid {
    left: -5px;
    top: calc(50% - 5px);
    /* cursor: w-resize; */
}

.dot.right-mid {
    right: -5px;
    top: calc(50% - 5px);
    /* cursor: e-resize; */
}

.dot.bottom-mid {
    bottom: -5px;
    left: calc(50% - 5px);
    /* cursor: s-resize; */
}

.dot.rotate {
    top: -30px;
    left: calc(50% - 5px);
    cursor: url('https://findicons.com/files/icons/1620/crystal_project/16/rotate_ccw.png'), auto;
}

.rotate-link {
    position: absolute;
    width: 1px;
    height: 15px;
    background-color: #1E88E5;
    top: -20px;
    left: calc(50% + 0.5px);
    z-index: -1;
}
<div class="box-wrapper" id="box-wrapper">
    <div class="box" id="box">
        <div class="dot rotate" id="rotate"></div>
        <div class="dot left-top" id="left-top"></div>
        <div class="dot left-bottom" id="left-bottom"></div>
        <div class="dot top-mid" id="top-mid"></div>
        <div class="dot bottom-mid" id="bottom-mid"></div>
        <div class="dot left-mid" id="left-mid"></div>
        <div class="dot right-mid" id="right-mid"></div>
        <div class="dot right-bottom" id="right-bottom"></div>
        <div class="dot right-top" id="right-top"></div>
        <div class="rotate-link"></div>
    </div>
</div>

虽然了解形状变换的基础知识是一个很好的练习,但幸运的是你不必这样做,因为这已经是一个已解决的问题。我相信史蒂夫已经在您现有的代码中添加了问题修复程序,但我建议使用现有的解决方案,例如 konvajs 而不是重新发明轮子。

var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
  container: 'container',
  width: width,
  height: height,
});

var layer = new Konva.Layer();
stage.add(layer);

var rect1 = new Konva.Rect({
  x: 60,
  y: 60,
  width: 100,
  height: 90,
  fill: 'red',
  name: 'rect',
  draggable: true,
});
layer.add(rect1);

var tr = new Konva.Transformer();
layer.add(tr);
tr.nodes([rect1]);
layer.draw();
#container {
  border:1px solid black;
  width: 100%;
  height: 100%;
}
<script src="https://unpkg.com/konva@7.1.4/konva.min.js"></script>
<h1>Resizable shape</h1>
<div id="container"></div>