固定轴上旋转的CSS立方体

4
我有一个使用CSS构建的立方体。它由6个面组成,每个面都被转换成立方体的一个面,并且所有6个面都位于一个class为“cube”的
中。我对立方体进行的任何旋转都是在这个包含的“cube”类上完成的。
我希望立方体根据鼠标拖动输入进行旋转。到目前为止,它有点起作用。我只是将鼠标的x和y移动转换为绕x和y轴旋转的立方体。
但是这里有一个主要问题。我执行旋转时是简单地
transform: rotateX(xdeg) rotateY(ydeg)

CSS属性。问题在于y轴旋转也随着x轴旋转而旋转。
假设我将正方体绕x轴旋转90度。现在,如果我试图沿y轴旋转正方体90度,我希望从我的视角看到正方体向左或向右旋转90度。但实际上,它是围绕当前可见的前面进行旋转。也就是说,由于先前的x轴旋转使y轴旋转了90度,因此现在从用户的角度来看,它看起来像正方体围绕z轴旋转。
我希望能够以一种方式旋转立方体,使得用户的视角中x、y和z轴保持不变。同时,如果用户放开按钮并再次单击并拖动,则需要从当前状态开始旋转。
我发现这很难做到。我觉得可能无法仅使用“rotateX/Y/Z”属性实现这一点,而必须使用3D矩阵或rotate3d属性?
我知道使用CSS可能不是最容易的事情,但我仍然想做到这一点。有人可以指导我如何解决这个问题吗?

#cube-wrapper {
  position: absolute;
  left: 50%;
  top: 50%;
  perspective: 1500px;
}

.cube {
  position: relative;
  transform-style: preserve-3d;
}


/* Size and border color for each face */

.face {
  position: absolute;
  width: 200px;
  height: 200px;
  border: solid green 3px;
}


/* Transforming every face into their correct positions */

#front_face {
  transform: translateX(-100px) translateY(-100px) translateZ(100px);
}

#back_face {
  transform: translateX(-100px) translateY(-100px) translateZ(-100px);
}

#right_face {
  transform: translateY(-100px) rotateY(90deg);
}

#left_face {
  transform: translateY(-100px) translateX(-200px) rotateY(90deg);
}

#top_face {
  transform: translateX(-100px) translateY(-200px) rotateX(90deg);
}

#bottom_face {
  transform: translateX(-100px) rotateX(90deg);
}

.cube {
  transform: rotateX(90deg) rotateY(90deg);
}
<!-- Wrapper for the cube -->
<div id="cube-wrapper">
  <div class="cube">
    <!-- A div for each face of the cube -->
    <div id="front_face" class="face"></div>
    <div id="right_face" class="face"></div>
    <div id="back_face" class="face"></div>
    <div id="left_face" class="face"></div>
    <div id="top_face" class="face"></div>
    <div id="bottom_face" class="face"></div>
  </div>
</div>

我无法添加任何JavaScript代码,因为我实际上是用purescript编写逻辑。但是该代码只是注册一个mousedown处理程序,获取当前鼠标的x和y坐标,将其与上次的x和y坐标进行比较,并通过更改.cube的transform属性来相应地围绕x和y轴旋转立方体,值如下。

  {transform: "rotateX(90deg) rotateY(90deg)"}

1
请查看此问题的演示和文档,您可能会找到一些提示!=> https://dev59.com/_uo6XIcBkEYKwwoYKRHd?noredirect=1&lq=1 - twekz
@Ihazkode 我已经添加了HTML和CSS,但我是用PureScript编写的,而不是JavaScript,这两者非常不同。 - George V.M.
2个回答

7

注意: 发现这个问题在CSS中有点难以解决。如果你真的需要像这样进行新变换应用于前一个状态的复杂变换,可以尝试其他方法。

无论如何,我首先会解释我经历的步骤,我所面临的问题以及我采取的解决方法。虽然它非常复杂和混乱,但它确实有效。最后,我将提供我使用的JavaScript代码。

说明

因此,我已经了解了一些关于CSS中变换的事情。其中一个主要的事情是,当您将一系列变换字符串传递给transform属性时,就像这样:

transform: "rotateX(90deg) rotateY(90deg)"

这些变换没有组合成一个复合变换。相反,首先应用第一个变换,然后在其上应用下一个变换,以此类推。因此,尽管我期望立方体对角线旋转90度,但它并没有这样做。
正如@ihazkode建议的那样,rotate3d是正确的方法。它允许绕任意轴旋转,而不仅仅限于X、Y和Z轴。 rotate3d需要三个参数。
rotate3d(x, y, z, angle).

xyz指定了旋转轴。可以这样理解:想象一条从(x,y,z)指向你指定的transform-origin的直线,这条直线将成为旋转轴。现在假设你从(x,y,z)向原点看去,那么从这个视角下,物体将会以顺时针方向旋转angle度。

然而,我仍然遇到了一个问题。虽然rotate3d让我以更加直观的方式旋转立方体,但当我用鼠标旋转立方体后,再次单击并尝试旋转它时,它会回到原来的状态并从那里旋转,这不是我想要的。我希望它按照当前的状态旋转,无论旋转状态是什么。

我找到了一种非常混乱的方法,可以使用matrix3d属性实现。基本上,每次出现mousedown和mousemove事件时,我都会按以下步骤进行操作:

  1. 我会根据mousedown发生的位置和mousemove时的当前鼠标位置计算一个向量。例如,如果mousedown发生在(123,145),然后mousemove发生在(120,143),那么可以从这两个点形成一个向量[x,y,z,m],其中

    x是x分量,即新的x位置减去mousedown时的x位置=120-123=-3

    y是y分量,与x类似,等于143-145=-2

    z=0,因为鼠标不能在z方向移动

    m是向量的大小,可以计算为squareroot(x2+y2)=3.606

    因此,鼠标移动可以表示为向量[-3,-2,0,3.606]

  2. 现在注意到立方体的旋转向量应该垂直于鼠标移动。例如,如果我向上移动鼠标3像素,鼠标移动向量就是[0,-1,0,3](y为负值,因为在浏览器中左上角是原点)。但是如果我使用该向量作为旋转向量并传递给rotate3d,它会使立方体顺时针旋转(从上面看)。但这不对!如果我向上挥动鼠标,它应该围绕它的x轴旋转!要解决这个问题,只需交换x和y,并否定新的x。也就是说,向量应该是[1,0,0,3]。因此,第一步中的向量应该改为[2,-3,0,3.606]。

现在,我只需要将立方体的transform属性设置为:

transform: "rotate3d(2,-3,0,3.606)"

现在,我已经找到了基于鼠标移动正确旋转立方体的方法,而不会出现以前试图进行rotateX和rotateY的问题。
现在立方体可以正确旋转。但是,如果我松开鼠标,再次按下鼠标并尝试旋转立方体呢?如果我按照上面的步骤进行,那么发生的情况就是我传递给rotate3d的新向量将替换旧向量。因此,立方体将重置为其初始位置,并应用新的旋转。但是这不对。我希望立方体保持之前的状态,然后从该状态开始通过新的旋转向量进一步旋转。
要做到这一点,我需要将新的旋转附加到先前的旋转上。因此,我可以像这样做:
transform: "rotate3d(previous_rotation_vector) rotate3d(new_rotation_vector)"

毕竟,这将执行第一次旋转,然后在此基础上执行第二次旋转。但是,想象一下执行100次旋转。 transform 属性需要输入 100 个 rotate3d。这不是最好的方法。

我想到了以下解决方案。如果您查询节点的 transform CSS 属性,则可以在任何时候使用:

$('.cube').css('transform');

您会得到以下三个值之一:“none”(如果对象到目前为止还没有被转换过),一个2D变换矩阵(看起来像matrix2d(...))(如果只进行了2D变换),或者一个3D变换矩阵(看起来像matrix3d(...))(否则)。
所以,我能做的是,在执行旋转操作后立即查询并获取立方体的变换矩阵并保存它。下次执行新旋转时,这样做:
transform: "matrix3d(saved_matrix_from_last_rotation) rotate3d(new_rotation_vector)"

这将首先将立方体转换为其最后旋转状态,然后在其上应用新的旋转。不需要传递100个rotate3d

  1. 我发现还有最后一个问题。对象的轴仍会随着对象一起旋转。

假设我沿x轴旋转立方体90度:

transform: rotate3d(1,0,0,90deg);

然后围绕它的 y 轴旋转 45 度。

transform: matrix3d(saved values) rotate3d(0,1,0,45deg)

我原本期望立方体会先向上旋转90度,然后向右旋转45度。但实际上它先是向上旋转了90度,接着围绕目前可见正面旋转了45度而不是像期望的那样向右旋转。这与我在问题中提到的完全一样。问题在于,虽然rotate3d允许你围绕任何任意旋转轴旋转对象,但该任意轴仍然是相对于对象轴而言的,而不是相对于用户的固定x、y和z轴。问题就在于轴随着对象一起旋转。

因此,如果立方体当前处于某个旋转状态,并且我想让它进一步沿一个向量(x,y,z)旋转,就像步骤1和2中获取的那样,我首先需要以某种方式将这个向量转换为基于立方体当前状态的正确位置。

我注意到,如果您将旋转向量作为4x1矩阵,如下所示:

x
y
z
angle

matrix3d矩阵视为4x4矩阵,然后如果我将3D变换矩阵乘以旋转向量,则可以获得旧的旋转向量,但是它已经转换为其正确位置。现在,我可以像步骤3中那样在3d矩阵之后应用此向量,最终立方体的行为方式与应该完全一样。

JavaScript代码

好了,说了这么多。这是我使用的代码。如果不太清楚,请见谅。

var lastX; //stores x position from mousedown
var lastY; //y position from mousedown
var matrix3d = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] //this identity matrix performs no transformation

$(document).ready(function() {
  $('body').on('mousedown', function(event) {
    $('body').on('mouseup', function() {
      $('body').off('mousemove');
      m = $('.cube').css('transform');
      //if this condition is true, transform property is either "none" in initial state or "matrix2d" which happens when the cube is at 0 rotation.
      if(m.match(/matrix3d/) == null) 
        matrix3d = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]; //identity matrix for no transformaion
      else
        matrix3d = stringToMatrix(m.substring(8,m.length));
    });

    lastX=event.pageX;
    lastY=event.pageY;

    $('body').on('mousemove', function (event) {
      var x = -(event.pageY - lastY);
      var y = event.pageX - lastX;
      var angle = Math.sqrt(x*x + y*y);
      var r = [[x],[y],[0],[angle]]; //rotation vector
      rotate3d = multiply(matrix3d, r); //multiply to get correctly transformed rotation vector
      var str = 'matrix3d' + matrixToString(matrix3d)
            + ' rotate3d(' + rotate3d[0][0] + ', ' + rotate3d[1][0] + ', ' + rotate3d[2][0] + ', ' + rotate3d[3][0] + 'deg)';
      $('.cube').css('transform',str);
    });
  });
});

//converts transform matrix to a string of all elements separated by commas and enclosed in parentheses.
function matrixToString(matrix) {
  var s = "(";
  for(i=0; i<matrix.length; i++) {
    for(j=0; j<matrix[i].length; j++) {
      s+=matrix[i][j];
      if(i<matrix.length-1 || j<matrix[i].length-1) s+=", ";
    }
  }
  return s+")";
}

//converts a string of transform matrix into a matrix
function stringToMatrix(s) {
  array=s.substring(1,s.length-1).split(", ");
  return [array.slice(0,4), array.slice(4,8), array.slice(8,12), array.slice(12,16)];
}

//matrix multiplication
function multiply(a, b) {
  var aNumRows = a.length, aNumCols = a[0].length,
      bNumRows = b.length, bNumCols = b[0].length,
      m = new Array(aNumRows);  // initialize array of rows
  for (var r = 0; r < aNumRows; ++r) {
    m[r] = new Array(bNumCols); // initialize the current row
    for (var c = 0; c < bNumCols; ++c) {
      m[r][c] = 0;             // initialize the current cell
      for (var i = 0; i < aNumCols; ++i) {
        m[r][c] += a[r][i] * b[i][c];
      }
    }
  }
  return m;
}

帮助我得到了r(角度)。如果这不是那么古老的话,我会向您展示一个简化版本。但我喜欢提供3D矩阵的数学版本。 - Brad Vanderbush
太棒了。我碰到了这个问题,不确定是否有解决方案。现在找到了:)谢谢!还有几件事情:一件微小但重要的是 - jQuery的css可以用getComputedStyle代替,我只是尝试获取element.style.transform。感觉这是一个显而易见的事情,但我卡在那里大约20分钟。另一件事 - rotate3d虽然更直观,但如果你进行连续旋转,它仍然效果不好。如果你翻转对象,它仍会搞砸“直观”部分。但我想当我们从2D空间与3D对象交互时,这是不可避免的事情。 - RCKT

4

使用 rotate3d

它相对容易使用,但您仍需要将当前跟踪脚本链接到正确的参数

您可以控制旋转量(以度为单位)和受影响的轴(x、y、z)。您可以同时选择一个或多个。

示例1 - 旋转X轴:

#cube-wrapper {
  position: absolute;
  left: 50%;
  top: 50%;
  perspective: 1500px;
}

.cube {
  position: relative;
  transform-style: preserve-3d;
  animation-name: rotate;
  animation-duration: 30s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

@keyframes rotate {
  0% {
    transform: rotate3d(0, 0, 0, 0);
  }
  100% {
    transform: rotate3d(1, 0, 0, 360deg); /*controls rotation amount on one axis) */
    ;
  }
}


/* Size and border color for each face */

.face {
  position: absolute;
  width: 200px;
  height: 200px;
  border: solid green 3px;
}


/* Transforming every face into their correct positions */

#front_face {
  transform: translateX(-100px) translateY(-100px) translateZ(100px);
  background: rgba(255, 0, 0, 0.5);
}

#back_face {
  transform: translateX(-100px) translateY(-100px) translateZ(-100px);
  background: rgba(255, 0, 255, 0.5);
}

#right_face {
  transform: translateY(-100px) rotateY(90deg);
  background: rgba(255, 255, 0, 0.5);
}

#left_face {
  transform: translateY(-100px) translateX(-200px) rotateY(90deg);
  background: rgba(0, 255, 0, 0.5);
}

#top_face {
  transform: translateX(-100px) translateY(-200px) rotateX(90deg);
  background: rgba(0, 255, 255, 0.5);
}

#bottom_face {
  transform: translateX(-100px) rotateX(90deg);
  background: rgba(255, 255, 255, 0.5);
}

.cube {
  transform: rotateX(90deg) rotateY(90deg);
}
<html>

<head>
  <title>3D Cube in PureScript</title>
  <link rel="stylesheet" type="text/css" href="css/cube_ref.css" />
  <script type="text/javascript" src=../js/jquery-3.2.1.min.js></script>
</head>

<body style="width: 100%; height:100%;">
  <!-- Wrapper for the cube -->
  <div id="cube-wrapper">
    <div class="cube">
      <!-- A div for each face of the cube -->
      <div id="front_face" class="face"></div>
      <div id="right_face" class="face"></div>
      <div id="back_face" class="face"></div>
      <div id="left_face" class="face"></div>
      <div id="top_face" class="face"></div>
      <div id="bottom_face" class="face"></div>
    </div>
  </div>
</body>
<script type="text/javascript" src=js/cube.js></script>

</html>

示例2 - 绕Y轴旋转:

#cube-wrapper {
  position: absolute;
  left: 50%;
  top: 50%;
  perspective: 1500px;
}

.cube {
  position: relative;
  transform-style: preserve-3d;
  animation-name: rotate;
  animation-duration: 30s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

@keyframes rotate {
  0% {
    transform: rotate3d(0, 0, 0, 0);
  }
  100% {
    transform: rotate3d(0, 1, 0, 360deg); /*controls rotation amount on one axis) */
    ;
  }
}


/* Size and border color for each face */

.face {
  position: absolute;
  width: 200px;
  height: 200px;
  border: solid green 3px;
}


/* Transforming every face into their correct positions */

#front_face {
  transform: translateX(-100px) translateY(-100px) translateZ(100px);
  background: rgba(255, 0, 0, 0.5);
}

#back_face {
  transform: translateX(-100px) translateY(-100px) translateZ(-100px);
  background: rgba(255, 0, 255, 0.5);
}

#right_face {
  transform: translateY(-100px) rotateY(90deg);
  background: rgba(255, 255, 0, 0.5);
}

#left_face {
  transform: translateY(-100px) translateX(-200px) rotateY(90deg);
  background: rgba(0, 255, 0, 0.5);
}

#top_face {
  transform: translateX(-100px) translateY(-200px) rotateX(90deg);
  background: rgba(0, 255, 255, 0.5);
}

#bottom_face {
  transform: translateX(-100px) rotateX(90deg);
  background: rgba(255, 255, 255, 0.5);
}

.cube {
  transform: rotateX(90deg) rotateY(90deg);
}
<html>

<head>
  <title>3D Cube in PureScript</title>
  <link rel="stylesheet" type="text/css" href="css/cube_ref.css" />
  <script type="text/javascript" src=../js/jquery-3.2.1.min.js></script>
</head>

<body style="width: 100%; height:100%;">
  <!-- Wrapper for the cube -->
  <div id="cube-wrapper">
    <div class="cube">
      <!-- A div for each face of the cube -->
      <div id="front_face" class="face"></div>
      <div id="right_face" class="face"></div>
      <div id="back_face" class="face"></div>
      <div id="left_face" class="face"></div>
      <div id="top_face" class="face"></div>
      <div id="bottom_face" class="face"></div>
    </div>
  </div>
</body>
<script type="text/javascript" src=js/cube.js></script>

</html>

示例3 - 绕Z轴旋转:

#cube-wrapper {
  position: absolute;
  left: 50%;
  top: 50%;
  perspective: 1500px;
}

.cube {
  position: relative;
  transform-style: preserve-3d;
  animation-name: rotate;
  animation-duration: 30s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

@keyframes rotate {
  0% {
    transform: rotate3d(0, 0, 0, 0);
  }
  100% {
    transform: rotate3d(0, 0, 1, 360deg); /*controls rotation amount on one axis) */
    ;
  }
}


/* Size and border color for each face */

.face {
  position: absolute;
  width: 200px;
  height: 200px;
  border: solid green 3px;
}


/* Transforming every face into their correct positions */

#front_face {
  transform: translateX(-100px) translateY(-100px) translateZ(100px);
  background: rgba(255, 0, 0, 0.5);
}

#back_face {
  transform: translateX(-100px) translateY(-100px) translateZ(-100px);
  background: rgba(255, 0, 255, 0.5);
}

#right_face {
  transform: translateY(-100px) rotateY(90deg);
  background: rgba(255, 255, 0, 0.5);
}

#left_face {
  transform: translateY(-100px) translateX(-200px) rotateY(90deg);
  background: rgba(0, 255, 0, 0.5);
}

#top_face {
  transform: translateX(-100px) translateY(-200px) rotateX(90deg);
  background: rgba(0, 255, 255, 0.5);
}

#bottom_face {
  transform: translateX(-100px) rotateX(90deg);
  background: rgba(255, 255, 255, 0.5);
}

.cube {
  transform: rotateX(90deg) rotateY(90deg);
}
<html>

<head>
  <title>3D Cube in PureScript</title>
  <link rel="stylesheet" type="text/css" href="css/cube_ref.css" />
  <script type="text/javascript" src=../js/jquery-3.2.1.min.js></script>
</head>

<body style="width: 100%; height:100%;">
  <!-- Wrapper for the cube -->
  <div id="cube-wrapper">
    <div class="cube">
      <!-- A div for each face of the cube -->
      <div id="front_face" class="face"></div>
      <div id="right_face" class="face"></div>
      <div id="back_face" class="face"></div>
      <div id="left_face" class="face"></div>
      <div id="top_face" class="face"></div>
      <div id="bottom_face" class="face"></div>
    </div>
  </div>
</body>
<script type="text/javascript" src=js/cube.js></script>

</html>

示例4 - 同时旋转X、Y和Z:

#cube-wrapper {
  position: absolute;
  left: 50%;
  top: 50%;
  perspective: 1500px;
}

.cube {
  position: relative;
  transform-style: preserve-3d;
  animation-name: rotate;
  animation-duration: 30s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

@keyframes rotate {
  0% {
    transform: rotate3d(0, 0, 0, 0);
  }
  100% {
    transform: rotate3d(1, 1, 1, 360deg); /*controls rotation amount on one axis) */
    ;
  }
}


/* Size and border color for each face */

.face {
  position: absolute;
  width: 200px;
  height: 200px;
  border: solid green 3px;
}


/* Transforming every face into their correct positions */

#front_face {
  transform: translateX(-100px) translateY(-100px) translateZ(100px);
  background: rgba(255, 0, 0, 0.5);
}

#back_face {
  transform: translateX(-100px) translateY(-100px) translateZ(-100px);
  background: rgba(255, 0, 255, 0.5);
}

#right_face {
  transform: translateY(-100px) rotateY(90deg);
  background: rgba(255, 255, 0, 0.5);
}

#left_face {
  transform: translateY(-100px) translateX(-200px) rotateY(90deg);
  background: rgba(0, 255, 0, 0.5);
}

#top_face {
  transform: translateX(-100px) translateY(-200px) rotateX(90deg);
  background: rgba(0, 255, 255, 0.5);
}

#bottom_face {
  transform: translateX(-100px) rotateX(90deg);
  background: rgba(255, 255, 255, 0.5);
}

.cube {
  transform: rotateX(90deg) rotateY(90deg);
}
<html>

<head>
  <title>3D Cube in PureScript</title>
  <link rel="stylesheet" type="text/css" href="css/cube_ref.css" />
  <script type="text/javascript" src=../js/jquery-3.2.1.min.js></script>
</head>

<body style="width: 100%; height:100%;">
  <!-- Wrapper for the cube -->
  <div id="cube-wrapper">
    <div class="cube">
      <!-- A div for each face of the cube -->
      <div id="front_face" class="face"></div>
      <div id="right_face" class="face"></div>
      <div id="back_face" class="face"></div>
      <div id="left_face" class="face"></div>
      <div id="top_face" class="face"></div>
      <div id="bottom_face" class="face"></div>
    </div>
  </div>
</body>
<script type="text/javascript" src=js/cube.js></script>

</html>


你提供的解决方案有什么问题,需要设置悬赏吗? - Vadim Ovchinnikov
@ihazkode 哈哈,那个赏金有点好奇。我也想知道你为什么这样做?(不过还是谢谢你) - George V.M.
2
@GeorgeV.M. 没问题。即使它没有满足您的原始要求,您也为自己的问题提供了一个规范的答案。赏金是因为您写了它,尽管您实际上并不需要这样做。所以谢谢你。 - I haz kode

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接