He estado tratando de crear algo como esto en Javascript:
Como puede ver, el contenedor se puede arrastrar, rotar y cambiar de tamaño. La mayoría de las cosas funcionan bien, pero el cambio de tamaño del contenedor cuando se gira produce resultados extraños.
Espero que esto suceda:
En su lugar me sale esto:
Aquí está el código completo:
https://jsfiddle.net/c0krownz/
o,
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> Cuando establece element.style.top y element.style.left , debe especificar unidades (generalmente píxeles, px , al realizar este tipo de transformación de elementos). En su caso, solo establece las unidades en eventMoveHandler , lo que hace que solo funcione en el controlador de movimiento.
En el siguiente fragmento, lo cambié para agregar automáticamente px a repositionElement y eliminé las unidades de eventMoveHandler . También boxWrapper.style.width = w; y boxWrapper.style.height = h; en resize , ya que no tenían unidades, y no estaba claro dónde se usaban las dimensiones de boxWrapper .
Para mí, era más fácil pensar en este problema en términos del centro de la caja. Su código original usa la esquina superior izquierda pregirada para realizar un seguimiento de la posición, lo que se vuelve difícil de imaginar en el rectángulo girado. El centro, en cambio, es siempre el centro. Para usar el centro, agregué/cambié este css:
.box { transform: translate(-50%, -50%); } .box-wrapper { transform-origin: top left; /* changed from `center center` */ } También simplifica un poco el código en 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; } Ahora las coordenadas style.top y style.left box-wrapper están en el centro de la box . Si este sistema de coordenadas no funciona, podemos revisarlo.
A partir de aquí, debemos tener en cuenta la rotación de la caja al cambiar su tamaño. Por ejemplo, cuando el cuadro se gira 90 grados, todos los cambios de x se convierten en cambios de y. Para transformarlos, puedes usar Math.cos y Math.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; } //... } Además, cuando corrija su posición, también debe usar las fracciones seno y coseno, porque la posición establecida por style.top y style.left normalmente no tiene en cuenta la rotación:
if (xResize) { //... newX += 0.5 * rotatedWDiff * cosFraction; newY += 0.5 * rotatedWDiff * sinFraction; } if (yResize) { //... newX -= 0.5 * rotatedHDiff * sinFraction; newY += 0.5 * rotatedHDiff * cosFraction; }Como se señaló en los comentarios, el comportamiento es extraño cuando el borde o la esquina arrastrados van más allá de los bordes anclados. En este caso, esperaría que el cuadro se voltee o que el cuadro deje de cambiar de tamaño. Usaré un ancho y una altura mínimos aquí, ya que la implementación parece ser más simple.
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; } } //... } Aparte: Eliminación de oyentes de eventos No relacionado con el meollo del asunto es eliminar los detectores de eventos. Irónicamente, el código para eliminar su detector de eventos deja un detector de eventos en mouseup :
window.addEventListener('mouseup', function() { window.removeEventListener('mousemove', eventMoveHandler, false); }, false);Esto no es un gran problema, ya que la función no hace demasiado si se ejecuta repetidamente, y los cierres realmente no ocupan tanta memoria. Pero para realmente limpiarlo, podemos cambiarlo a algo como:
window.addEventListener('mouseup', function eventEndHandler() { window.removeEventListener('mousemove', eventMoveHandler, false); window.removeEventListener('mouseup', eventEndHandler, false); }, false);En conjunto, se parece a:
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>Si bien es un buen ejercicio para comprender los conceptos básicos de la transformación de formas, afortunadamente no es necesario que lo haga, ya que este ya es un problema resuelto. Creo que Steve ya agregó una solución para el problema en su código existente, pero recomendaría usar una solución existente como konvajs en lugar de reinventar la rueda.
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>