Вращающийся куб CSS на фиксированных осях

У меня есть куб, построенный с использованием CSS. Он состоит из 6 граней, и каждая грань трансформируется в одну грань куба, а все 6 граней находятся под одной <div> с классом .cube, Любое вращение, которое я делаю с кубом, делается на этом ограждении. cube учебный класс.

Я хочу, чтобы куб вращался на основе перетаскивания мышью. Пока это вроде работает. Я просто перевожу движение мыши x и y в вращение куба вокруг осей x и y.

Но есть одна серьезная проблема с этим. Я выполняю вращение как простой

transform: rotateX(xdeg) rotateY(ydeg)

CSS свойство Проблема в том, что ось вращения Y вращается вместе с вращением X.

Предположим, я повернул куб на 90 градусов вокруг оси x. Теперь, если я попытаюсь повернуть куб на 90 градусов вдоль оси y, я бы ожидал, что куб повернет на 90 градусов вправо или влево (с моей точки зрения). Но вместо этого он вращается вокруг видимой в данный момент передней грани. Таким образом, ось Y была повернута на 90 градусов благодаря повороту оси X, который был первым, и теперь, с точки зрения пользователя, выглядит так, как будто куб вращается вокруг своей оси Z.

Я хочу иметь возможность вращать куб таким образом, чтобы оси xy и z оставались фиксированными с точки зрения пользователя. Также куб должен вращаться из текущего состояния в случае, если пользователь убирает палец с кнопки, нажимает снова и перетаскивает.

Мне было трудно это сделать. Я чувствую, что это может быть невозможно, используя только rotateX/Y/Z свойства и вместо этого я мог бы использовать свойства 3d matrix или 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 и, соответственно, вращает куб вокруг осей x и y, изменяя свойство transform .cube со значением, как.

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

2 ответа

Решение

Примечание. Оказывается, эту проблему довольно трудно решить в CSS. Если вам действительно нужно сложное преобразование, подобное этому, где новые преобразования должны применяться к предыдущему состоянию, возможно, попробуйте другой метод.

Так или иначе, я сначала собираюсь объяснить шаги, которые я прошел, проблемы, с которыми я столкнулся, и шаги, которые я предпринял, чтобы решить это. Это действительно запутанный и грязный, но это работает. В конце я поместил код, который использовал как JavaScript.

объяснение

Итак, я пришел к пониманию нескольких вещей о преобразованиях в CSS. Главное, что когда вы передаете строку преобразований transform собственность, как это

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

эти преобразования не объединяются в одно составное преобразование. Вместо этого применяется первый, затем следующий применяется поверх этого и так далее. Поэтому, хотя я ожидал, что куб будет вращаться по диагонали на 90 градусов, он этого не сделал.

Как предложил @ihazkode, rotate3d был способ пойти. Это позволяет вращение вокруг любых произвольных осей вместо того, чтобы быть ограниченным осями X, Y и Z. rotate3d принимает 3 аргумента

rotate3d(x, y, z, angle).

x y и z определяют ось вращения. Взгляд на это выглядит так: представьте, что рисуете линию из (x,y,z) к transform-origin Вы указали. Эта линия будет осью вращения. Теперь представьте, что вы смотрите на происхождение из (x,y,z), С этой точки зрения объект будет вращаться по часовой стрелке на angle градусов.

Тем не менее, я все еще столкнулся с проблемой. Хотя rotate3d позвольте мне вращать куб гораздо более интуитивно понятным способом. Я все еще сталкивался с проблемой, когда после однократного вращения куба (с помощью мыши), если я снова нажму и попробую повернуть куб, он вернется в исходное состояние и оттуда повернется. что не то, что я хотел. Я хотел, чтобы он вращался из своего текущего состояния, каким бы оно ни было.

Я нашел очень грязный способ сделать это с помощью matrix3d имущество. Обычно я выполняю эти шаги каждый раз, когда происходят события mousedown и mousemove.

  1. Я бы рассчитал вектор на основе положения, в котором произошел mousedown, и текущей позиции мыши по перемещению мыши. Например, если mousedown произошел в точке (123,145), а затем мышиный ход произошел в точке (120,143), то из этих двух точек можно сделать вектор как [x, y, z, m], где

    х - это компонент х, который представляет собой новую позицию х минус мышь вниз, позиция х = 120 - 123 = -3

    y является компонентом y, аналогичным x, который = 143-145 = -2

    z = 0, так как мышь не может двигаться в направлении z

    m - величина вектора, которая может быть рассчитана как квадратный корень (x 2 + y 2) = 3,606

    Таким образом, движение мыши может быть представлено как вектор [-3, -2, 0, 3.606]

  2. Теперь обратите внимание, что вектор вращения куба должен быть перпендикулярен движению мыши. Например, если я перемещаю мышь прямо на 3 пикселя вверх, вектор движения мыши будет равен [0,-1,0,3] (у отрицателен, потому что в браузере верхний левый угол является источником). Но если я использую этот вектор как вектор вращения и передам его в rotate3d, который вращает куб по часовой стрелке (если смотреть сверху) вокруг оси y. Но это не правильно! Если я проведу мышью вверх, она должна вращаться вокруг своей оси x! Чтобы решить эту проблему, просто поменяйте местами x и y и отрицайте новый x. То есть вектор должен быть [1,0,0,3]. Следовательно, вектор с шага 1 вместо этого должен быть [2,-3,0,3.606].

Теперь я просто установил transform свойство моего куба как

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

Итак, теперь я выяснил, как правильно вращать куб, основываясь на движении мыши, не сталкиваясь с предыдущей проблемой попытки сделать rotateX а потом rotateY,

  1. Теперь куб может вращаться правильно. Но что, если я отпущу мышь, а затем снова выполню размышление и попробую вращать куб. Если я буду следовать тем же шагам сверху, то получится новый вектор, который я передам rotate3d заменит старый. Таким образом, куб возвращается в исходное положение и к нему применяется новое вращение. Но это не правильно. Я хочу, чтобы куб оставался в том состоянии, в котором он находился ранее, и затем из этого состояния он должен вращаться дальше на новый вектор вращения.

Для этого мне нужно добавить новый поворот на предыдущий поворот. Так что я мог бы сделать что-то вроде этого

transform: "rotate3d(previous_rotation_vector) rotate3d(new_rotation_vector)"

В конце концов, это выполнит первое вращение, а затем выполнит второе вращение поверх этого. Но тогда представьте, что вы выполняете 100 вращений. transform собственность должна быть накормлена 100 rotate3d s. Это не самый лучший способ сделать это.

Вот что я понял. В любой момент, если вы запросите transform CSS свойство узла типа

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

вы получите одно из 3 значений: "none", если объект еще не был преобразован, матрица 2D-преобразования (выглядит как matrix2d(...)) если выполняется только 2D-преобразование или матрица 3D-преобразования (выглядит как matrix3d(...) иначе.

Поэтому я могу сразу же после выполнения операции поворота запросить и получить матрицу преобразования куба и сохранить ее. В следующий раз, когда я выполню новую ротацию, сделайте это:

transform: "matrix3d(saved_matrix_from_last_rotation) rotate3d(new_rotation_vector)"

Это сначала преобразовало бы куб в его последнее состояние вращения, а затем применило новое вращение поверх этого. Не нужно сдавать 100 rotate3d s.

  1. Есть одна последняя проблема, которую я обнаружил. Там все та же проблема осей объекта, вращающегося вместе с объектом.

Предположим, что я поворачиваю куб на 90 градусов вдоль оси x с

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, и, наконец, куб ведет себя точно так, как должен.

Код 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;
}

Использовать 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>

Другие вопросы по тегам