如何在旋转后的父元素中使用draggable拖动元素?

编辑2:在仔细阅读了jquery-ui的源代码,并充分理解了其原理后,我制作出了能够符合我的需求的代码:能够在旋转后的父元素中使用jquery-ui的draggable功能以实现拖动子元素的效果。我希望能分享给大家,或许有其他人有着和我类似的需求,可以提供给您作为参考。

需要写在最前面强调的是,这个功能有一定的局限性,例如旋转的父元素和拖拽的子元素必须都是position:absolute,并且一部分draggable的功能例如snap将可能无法正确工作。另外,由于涉及到了更多计算,这会影响到代码的运行效率。如果这些限制可以满足您的需求,请往下看。

为了完成这个功能,我们首先需要明确一点:jquery中的offset功能对于旋转后的元素而言,只会得到错误的结果,无一例外。如果您想要移动一个旋转后的元素,必须在这个元素外面嵌套一个父元素用于定位offset,并且移动的draggable绑定的对象也必须是这个父元素。不过,只要我们将其高度和宽度设定为0px,位置设定为absolute的话,就不需要担心这个多出来的父元素会影响到我们的元素结构。wrapper的用处是获取元素在旋转前左上角的offset,并结合角度等计算出旋转后的元素的offset,并以这个旋转后的offset来计算元素的位移值
对于我而言,我将其设置为:

HTML:

<div class="wrapper"> 
      <div id="转动的父元素">
          <div class="wrapper>
              <div id="移动的子元素"></div>  
          </div>
      </div>
</div>

CSS:

.wrapper{
    height:0px;
    width:0px;
    position:absolute;
}

js:

$(旋转的父元素).css({
    "transform" : "rotate(45deg)",
//注意这里,我将父元素的中心设置为元素的中心,如果您需要以其他位置为中心进行旋转,请注意修改下方的return_playground_wrapperOffset()函数中有关中心点坐标和偏移量的部分
    "transform-origin" : "center" 
)
$(移动的子元素).parent(".wrapper").draggable()

随后,我们还需要一些准备工作,例如:
1.我们需要能够获取到“转动的父元素”所转动的角度,在这里我将其记录在父元素内: $("#转动的父元素").attr("angle",转动的角度),并通过return_angle()函数获取这个值

2.我们需要一个额外的函数,用来求得“旋转的父元素”在旋转后的左上角的位置,我的函数如下:

function return_playground_wrapperOffset(){

  //父元素wrapper的坐标:
  var wrapper = $("#旋转的父元素").parent(".wrapper")
  var wrapper_offset = $(wrapper).offset()

  //中心点的坐标
  var center_x = wrapper_offset.left + $(huabu).width()/2 * return_scale()
  var center_y = wrapper_offset.top + $(huabu).height()/2 * return_scale()

  //wrapper相对于中心点的偏移量
  var delta_x = - $(huabu).width()/2 * return_scale()
  var delta_y = - $(huabu).height()/2 * return_scale()

  //旋转弧度
  var radian = return_angle() * Math.PI / 180

  //计算旋转后的相对坐标
  var new_delta_x = delta_x * Math.cos(radian) + delta_y * Math.sin(radian)
  var new_delta_y = - delta_x * Math.sin(radian) + delta_y * Math.cos(radian)

  //将相对坐标加上中心点的坐标,得到旋转后的左上角顶点的坐标
  var new_x = center_x + new_delta_y
  var new_y = center_y + new_delta_x

  return {left:new_x,top:new_y}

}

随后,我修改了jquery-ui的源代码,也就是jquery-ui.js的内容,修改的内容包括两个部分,首先是第约10006行的_refreshOffsets: function( event ) {……}
我将其修改为:

_refreshOffsets: function( event ) {
    this.offset = {
        top: this.positionAbs.top - this.margins.top,
        left: this.positionAbs.left - this.margins.left,
        scroll: false,
        parent: this._getParentOffset(),
        relative: this._getRelativeOffset()
    };
    //获取父元素转动的角度,并转化为弧度以供三角函数使用
    var radian = return_angle() * Math.PI / 180
    //旧的偏移量表示的是这个鼠标点击位置与元素左上角的距离,但由于其原理使用到了offset,因此其无法在旋转后正确工作
    var old_left = event.pageX - this.offset.left
    var old_top = event.pageY - this.offset.top
    //不过,我们可以对错误的值进行偏转,依次得到鼠标相对于旋转后的子元素的偏移量,并将其以旋转后的子元素的Left和top的方式进行表示,之所以使用left和top的方式,是因为之后我们在移动元素时,实际操作的是元素的top和left值,这也是为什么在offset无法正确工作的情况下,我们能够通过一些修复来使得draggable能够正确工作,因为其实际的工作原理与offset无关,只是一部分值涉及到了offset的使用
    var new_top = old_top * Math.cos(radian) - old_left * Math.sin(radian) 
    var new_left = old_top * Math.sin(radian) + old_left * Math.cos(radian) 

    this.offset.click = {
        top: new_top,
        left: new_left
    };
},

随后我们还需要修改第约10380行的_generatePosition: function( event, constrainPosition ) {……}函数,修改后的函数如下所示:

_generatePosition: function( event, constrainPosition ) {
    //一直到下一个注解前的内容都可以忽略
    var containment, co, top, left,
        o = this.options,
        scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] ),
        pageX = event.pageX,
        pageY = event.pageY;

    if ( !scrollIsRootNode || !this.offset.scroll ) {
        this.offset.scroll = {
            top: this.scrollParent.scrollTop(),
            left: this.scrollParent.scrollLeft()
        };
    }

    if ( constrainPosition ) {
        if ( this.containment ) {
            if ( this.relativeContainer ) {
                co = this.relativeContainer.offset();
                containment = [
                    this.containment[ 0 ] + co.left,
                    this.containment[ 1 ] + co.top,
                    this.containment[ 2 ] + co.left,
                    this.containment[ 3 ] + co.top
                ];
            } else {
                containment = this.containment;
            }

            if ( event.pageX - this.offset.click.left < containment[ 0 ] ) {
                pageX = containment[ 0 ] + this.offset.click.left;
            }
            if ( event.pageY - this.offset.click.top < containment[ 1 ] ) {
                pageY = containment[ 1 ] + this.offset.click.top;
            }
            if ( event.pageX - this.offset.click.left > containment[ 2 ] ) {
                pageX = containment[ 2 ] + this.offset.click.left;
            }
            if ( event.pageY - this.offset.click.top > containment[ 3 ] ) {
                pageY = containment[ 3 ] + this.offset.click.top;
            }
        }

        if ( o.grid ) {
            top = o.grid[ 1 ] ? this.originalPageY + Math.round( ( pageY -
                this.originalPageY ) / o.grid[ 1 ] ) * o.grid[ 1 ] : this.originalPageY;
            pageY = containment ? ( ( top - this.offset.click.top >= containment[ 1 ] ||
                top - this.offset.click.top > containment[ 3 ] ) ?
                    top :
                    ( ( top - this.offset.click.top >= containment[ 1 ] ) ?
                        top - o.grid[ 1 ] : top + o.grid[ 1 ] ) ) : top;

            left = o.grid[ 0 ] ? this.originalPageX +
                Math.round( ( pageX - this.originalPageX ) / o.grid[ 0 ] ) * o.grid[ 0 ] :
                this.originalPageX;
            pageX = containment ? ( ( left - this.offset.click.left >= containment[ 0 ] ||
                left - this.offset.click.left > containment[ 2 ] ) ?
                    left :
                    ( ( left - this.offset.click.left >= containment[ 0 ] ) ?
                        left - o.grid[ 0 ] : left + o.grid[ 0 ] ) ) : left;
        }

        if ( o.axis === "y" ) {
            pageX = this.originalPageX;
        }

        if ( o.axis === "x" ) {
            pageY = this.originalPageY;
        }
    }

    //此处开始我们的修正,首先仍然是获取旋转的父元素的旋转角度并转化为弧度
    var radian = return_angle() * Math.PI / 180
    //通过上面提到的函数,获取旋转后父元素的左上角此时的位置
    var wrapper_offset = return_playground_wrapperOffset()

    var old_top = pageY - wrapper_offset.top
    var old_left = pageX - wrapper_offset.left
    //进行修正,请不要忘记“- this.offset.click.left/top”
    var new_left = old_left * Math.cos(radian) + old_top * Math.sin(radian)
                    - this.offset.click.left
    var new_top =  - old_left * Math.sin(radian) + old_top * Math.cos(radian)
                    - this.offset.click.top
    //将这个函数返回的值修改为如下
    return {
        top: new_top ,
        left: new_left 
    };

},

至此,我们完成了所有的修改,理论上无论您的父元素如何旋转,其都能正确地进行定位,另外,我建议您在进行修改之前将您的jquery-ui.js文件进行备份,以免错误或误差导致您的修改前的内容丢失。最后,如果我的代码能够帮助到你,或是给您启发,这是我的荣幸!

以下为原问题内容,其中提出的猜想没有参考价值,仅作为存档所用

首先我需要道歉,标题中的问题,实际上我已经解决了一部分,我希望得到回答的并不完全是我的题目。

我尝试在一个经过了transform:rotate(45deg) 的旋转的父元素中,使用jquery-ui的draggable拖动其中的子元素,然而jquery-ui的draggable并不支持transform属性,直接使用draggable是不可能的。

事实上,在这之前我也遇到过类似的问题,并且最后通过修改jquery-ui的源文件解决,这一次我也想进行同样的尝试。

我通过修改jquery-ui.js文件中,约10382行的内容,也就是

$.widget( "ui.draggable", $.ui.mouse, { 
 ……
_generatePosition: function( event, constrainPosition ) {
……
}

中的内容,成功做到了鼠标的移动与对象的移动“同步”,也就是说是鼠标向左移动100px的距离,拖拽的对象也会向左移动100px的距离。只不过我遇到了一些难以解决的bug,因此我需要您的帮助。

我遇到的问题是:当拖拽开始时,对象会“瞬移”一段距离,我必须为position增添一些固定的值才能解决这个问题,然而我并不知道这些固定的值是如何产生的,以及为什么对象会“瞬移”,我希望能解决这些问题

以下是我的代码,其中#playground是我的容器元素,#ball是容器中的可拖拽元素

CSS:

#playground{
    width:1000px;
    height:1000px;
    positition:absolute;
    transform:rotate(45deg);
    transform-origin:center center;
    background-color:white;
}
#ball{
    width:100px;
    height:100px;
    position:absolute;
}

JS:

//下面的是我修改jquery-ui后的代码,修改内容仅限_generatePosition最后的return内容,其余内容均未有任何改动
    var old_top = pageY-
                  this.offset.click.top -
                  this.offset.relative.top -
                  this.offset.parent.top +
                  (this.cssPosition === "fixed" ?
                        -this.offset.scroll.top :
                      (scrollIsRootNode ? 0 : this.offset.scroll.top ))
    var old_left = pageX-
                   this.offset.click.left -
                   this.offset.relative.left -
                   this.offset.parent.left +
                   (this.cssPosition === "fixed" ?
                      -this.offset.scroll.left :
                      (scrollIsRootNode ? 0 : this.offset.scroll.left ))
//以上两个值为原本函数中的返回值,我将其进行了一定的修改,以适配旋转后的主元素
    var angle = 45
    var radian = angle * Math.PI / 180
    var new_left = old_top * Math.sin(radian) + old_left * Math.cos(radian)
    var new_top = old_top * Math.cos(radian) - old_left * Math.sin(radian)
//这里我必须增添两个值才能使得#ball元素不会瞬移
//最终本函数会返回一个位置的值,并使用这个位置的值来修改被拖拽元素的位置,从而使得被拖拽元素能够与鼠标一同移动
    return {
        top: new_top + 450,
        left: new_left - 450
    };

我个人的猜想是:瞬移是因为position的初值不正确,最有可能是父元素角度变化导致了原本的old_left和old_top的值出现异常,但是我并没有那么深刻的理解,我不明白为什么这样的事情会发生我也不明白为什么最终我需要增加和减少“450”这个偏差值,我想知道这个偏差值是为什么会产生,如何获得的,因为我希望在后续的功能中增加其他的旋转角度

目前已知的信息是:这个偏差值会随着角度的变化而变化,也会随着#ball的height变化而变化,当角度在0-90°之间时,随着角度增大,或者height增大时,这个偏差值的数值也需要随之增大。有趣的是,width的变化似乎不会产生影响。

感谢你的任何建议,如果我没能描述清楚我的问题,我可以提供更多,谢谢您!

编辑1:经过多次的实验后,我大致得出了45°角时的偏差值规律为(1000 - 100)/2 ,也就是:(父元素的高度 - 子元素的高度)/ 2的值,这个结果让我感到很疑惑,为什么只有高度参与了这个偏差值的变化,宽度为什么不会参与?为什么要除以1/2?我正在尝试其他角度规律,有任何进展都愿意分享给大家,但我仍然希望能获得为什么会产生这样的变化的原因。

编辑2:我想我得到了top的偏差值的计算公式:

var offset_top = ($("#playground").height() - $(this.bindings).height()) * Math.pow(Math.sin(radian),2)

在任何角度下,这个top的偏差值都能准确奏效,但问题是left的偏差值却仍然是一个谜团,无论是 Math.pow(Math.sin(radian),2) 还是 Math.pow(Math.cos(radian),2) 都不是一个完美的值,我感到十分困惑,并且我并不了解为什么这个偏差值会是这样生成的,尤其是将三角函数开平方这件事完全在我的意料之外,无论如何,我仍然需求帮助,谢谢您!

阅读 1.1k
1 个回答

下面的例子在 Chrome118 和 FireFox118 中运行良好,复制到一个新的 html 文件再用浏览器打开就可以体验:

<!DOCTYPE html>
<html lang="zh-CN" charset="utf-8">
  <head>
    <title>Drag in rotated</title>
    <style>
      .stage {
        position: absolute;
        width: 800px;
        height: 800px;
        outline: solid 1px #2f89ff;
        transform-origin: center;
      }
      .dancer {
        position: absolute;
        width: 16px;
        height: 16px;
        cursor: move;
        background-color: red;
        border-radius: 8px;
      }
      .inputs {
        position: relative;
        z-index: 10;
      }
    </style>
  </head>
  <body>
    <div class="inputs">
      <div>
        <label>
          <span>旋转:</span>
          <input
            type="number"
            name="angle"
            id="angle"
            max="360"
            min="-360"
            step="1"
            value="0"
          />
        </label>
      </div>
      <div>
        <label>
          <span>缩小:</span>
          <input
            type="number"
            name="scale"
            id="scale"
            max="1"
            min="0.6"
            step="0.05"
            value="1"
          />
        </label>
      </div>
    </div>
    <div class="wrapper">
      <div id="stage" class="stage">
        <div id="dancer" class="dancer" style="top: 120px; left: 120px"></div>
      </div>
    </div>
  </body>
  <script>
    const metaMatrix = [1, 0, 0, 1, 0, 0];
    let actualAngle = 0;
    let actualScale = 1;
    function applyMatrixToStage() {
      const matrix = new DOMMatrixReadOnly(metaMatrix)
        .rotate(actualAngle)
        .scale(actualScale)
        .toString();
      stage.style.transform = matrix;
    }

    function toNormalMatrix({ a, b, c, d, e, f }) {
      return {
        _value: new Float32Array([a, c, e, b, d, f]),
        point([x, y]) {
          const [m11, m12, m13, m21, m22, m23] = this._value;
          return [m11 * x + m12 * y + m13, m21 * x + m22 * y + m23];
        },
      };
    }

    angle.addEventListener("change", ({ target: { value } }) => {
      actualAngle = +value;
      applyMatrixToStage();
    });

    scale.addEventListener("change", ({ target: { value } }) => {
      actualScale = +value;
      applyMatrixToStage();
    });

    stage.addEventListener("mousedown", ({ target, clientX, clientY }) => {
      if (target === dancer) {
        const { style } = dancer;

        const matrix = new DOMMatrixReadOnly(metaMatrix)
          .rotate(actualAngle)
          .scale(actualScale);

        const startAt = [parseFloat(style.left), parseFloat(style.top)];
        const dragCallback = ({
          target,
          clientX: _clientX,
          clientY: _clientY,
        }) => {
          // 把逆变换应用到拖动的差量即可
          const offset = toNormalMatrix(matrix.inverse().toJSON()).point([
            _clientX - clientX,
            _clientY - clientY,
          ]);
          style.left = startAt[0] + offset[0] + "px";
          style.top = startAt[1] + offset[1] + "px";
        };
        const cancelDrag = () => {
          stage.removeEventListener("mousemove", dragCallback);
        };
        stage.addEventListener("mousemove", dragCallback);

        stage.addEventListener("mouseup", cancelDrag);
        stage.addEventListener("mouseleave", cancelDrag);
      }
    });
  </script>
</html>
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题