Search code examples
javascriptjquerymath

Resize div with drag handle when rotated


I could find similar questions involving jQuery UI lib, or only CSS with no handle to drag, but nothing with pure maths.

What I try to perform is to have a resizable and rotatable div. So far so easy and I could do it.

But it gets more complicate when rotated, the resize handle does calculation in opposite way: it decreases the size instead of increasing when dragging away from shape.

Apart from the calculation, I would like to be able to change the cursor of the resize handle according to the rotation to always make sense. For that I was thinking to detect which quadrant is the resize handle in and apply a class to change cursor via CSS.

  1. I don't want to reinvent the wheel, but I want to have a lightweight code and simple UI. So my requirement is jQuery but nothing else. I prefer not to add jQuery UI.
  2. I could develop until achieving this but it's getting too mathematical for me now. I need to detect when the rotation is enough to have the calculation reversed.

I am also looking for UX improvements.

Here is my code and a Codepen to try: http://codepen.io/anon/pen/rrAWJA

<html>
<head>
    <style>
    html, body {height: 100%;}

    #square {
        width: 100px;
        height: 100px;
        margin: 20% auto;
        background: orange;
        position: relative;
    }
    .handle * {
        position: absolute;
        width: 20px;
        height: 20px;
        background: turquoise;
        border-radius: 20px;
    }
    .resize {
        bottom: -10px;
        right: -10px;
        cursor: nwse-resize;
    }
    .rotate {
        top: -10px;
        right: -10px;
        cursor: alias;
    }
    </style>
    <script type="text/javascript" src="js/jquery.js"></script>
    <script>
        $(document).ready(function()
        {
            new resizeRotate('#square');
        });

        var resizeRotate = function(targetElement)
        {
            var self = this;
            self.target = $(targetElement);
            self.handles = $('<div class="handle"><div class="resize" data-position="bottom-right"></div><div class="rotate"></div></div>');
            self.currentRotation = 0;
            self.positions = ['bottom-right', 'bottom-left', 'top-left', 'top-right'];

            self.bindEvents = function()
            {
                self.handles
                    //=============================== Resize ==============================//
                    .on('mousedown', '.resize', function(e)
                    {
                        // Attach mouse move event only when first clicked.
                        $(document).on('mousemove', function(e)
                        {
                            var topLeft = self.target.offset(),
                                bottomRight = {x: topLeft.left + self.target.width(), y: topLeft.top + self.target.height()},
                                delta = {x: e.pageX - bottomRight.x, y: e.pageY - bottomRight.y};

                            self.target.css({width: '+=' + delta.x, height: '+=' + delta.y});
                        })
                        .one('mouseup', function(e)
                        {
                            // When releasing handle, round up width and height values :)
                            self.target.css({width: parseInt(self.target.width()), height: parseInt(self.target.height())});
                            $(document).off('mousemove');
                        });
                    })
                    //============================== Rotate ===============================//
                    .on('mousedown', '.rotate', function(e)
                    {
                        // Attach mouse move event only when first clicked.
                        $(document).on('mousemove', function(e)
                        {
                            var topLeft = self.target.offset(),
                                center = {x: topLeft.left + self.target.width() / 2, y: topLeft.top + self.target.height() / 2},
                                rad = Math.atan2(e.pageX - center.x, e.pageY - center.y),
                                deg = (rad * (180 / Math.PI) * -1) + 135;

                            self.currentRotation = deg;
                            // console.log(rad, deg);
                            self.target.css({transform: 'rotate(' + (deg)+ 'deg)'});
                        })
                        .one('mouseup', function(e)
                        {
                            $(document).off('mousemove');
                            // console.log(self.positions[parseInt(self.currentRotation/90-45)]);
                            $('.handle.resize').attr('data-position', self.positions[parseInt(self.currentRotation/90-45)]);
                        });
                    });
            };
            self.init = function()
            {
                self.bindEvents();
                self.target.append(self.handles.clone(true));
            }();
        }
    </script>
</head>
<body>
    <div id="all">
        <div id="square"></div>
    </div>
</body>
</html>

Solution

  • Here is a modification of your code that achieves what you want:

    $(document).ready(function() {
      new resizeRotate('#square');
    });
    
    var resizeRotate = function(targetElement) {
      var self = this;
      self.target = $(targetElement);
      self.handles = $('<div class="handle"><div class="resize" data-position="bottom-right"></div><div class="rotate"></div></div>');
      self.currentRotation = 0;
      self.w = parseInt(self.target.width());
      self.h = parseInt(self.target.height());
      self.positions = ['bottom-right', 'bottom-left', 'top-left', 'top-right'];
    
      self.bindEvents = function() {
        self.handles
          //=============================== Resize ==============================//
          .on('mousedown', '.resize', function(e) {
            // Attach mouse move event only when first clicked.
            $(document).on('mousemove', function(e) {
                var topLeft = self.target.offset();           
    
                var centerX = topLeft.left + self.target.width() / 2;
                var centerY = topLeft.top + self.target.height() / 2;
    
                var mouseRelativeX = e.pageX - centerX;
                var mouseRelativeY = e.pageY - centerY;
    
                //reverse rotation
                var rad = self.currentRotation * Math.PI / 180;
                var s = Math.sin(rad);
                var c = Math.cos(rad);
                var mouseLocalX = c * mouseRelativeX + s * mouseRelativeY;
                var mouseLocalY = -s * mouseRelativeX + c * mouseRelativeY;
    
                self.w = 2 * mouseLocalX;
                self.h = 2 * mouseLocalY;
                self.target.css({
                  width: self.w,
                  height: self.h
                });
              })
              .one('mouseup', function(e) {
                    $(document).off('mousemove');
              });
          })
          //============================== Rotate ===============================//
          .on('mousedown', '.rotate', function(e) {
            // Attach mouse move event only when first clicked.
            $(document).on('mousemove', function(e) {
                var topLeft = self.target.offset(),
                  center = {
                    x: topLeft.left + self.target.width() / 2,
                    y: topLeft.top + self.target.height() / 2
                  },
                  rad = Math.atan2(e.pageX - center.x, center.y - e.pageY) - Math.atan(self.w / self.h),
                  deg = rad * 180 / Math.PI;
    
                self.currentRotation = deg;
                self.target.css({
                  transform: 'rotate(' + (deg) + 'deg)'
                });
              })
              .one('mouseup', function(e) {
                $(document).off('mousemove');
                $('.handle.resize').attr('data-position', self.positions[parseInt(self.currentRotation / 90 - 45)]);
              });
          });
      };
      self.init = function() {
        self.bindEvents();
        self.target.append(self.handles.clone(true));
      }();
    }
    

    The major changes are the following:

    In the resize event, the mouse position is transformed to the local coordinate system based on the current rotation. The size is then determined by the position of the mouse in the local system.

    The rotate event accounts for the aspect ratio of the box (the - Math.atan(self.w / self.h) part).

    If you want to change the cursor based on the current rotation, check the angle of the handle (i.e. self.currentRotation + Math.atan(self.w / self.h) * 180 / Math.PI). E.g. if you have a cursor per quadrant, just check if this value is between 0..90, 90..180 and so on. You may want to check the documentation if and when negative numbers are returned by atan2.

    Note: the occasional flickering is caused by the box not being vertically centered.