在javascript中创建可调整大小/可拖动/旋转的视图

2022-03-13 00:00:00 coordinates math javascript html css

我一直在尝试在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维度的使用位置。

重新分配坐标

对我来说,从盒子的中心来考虑这个问题更容易。您的原始代码使用预置的左上角来跟踪位置,这在旋转的矩形上变得很难想象。另一方面,中心永远是中心。要使用中心,我添加/更改了此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>

相关文章