使用四元数通过 JavaScript 旋转 HTML 和 CSS 立方体

问题描述 投票:0回答:1

我有以下代码:

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta property="og:type" content="website">

  <title>RGBCube</title>
  <meta name="author" content="RGBCube" />

  <meta property="og:site_name" content="RGBCube" />
  <meta property="og:title" content="RGBCube" />

  <meta name="description" content="The official website and link portal of RGBCube and his work." />
  <meta property="og:description" content="The official website and link portal of RGBCube and his work." />

  <link rel="icon" href="favicon.gif" type="image/gif">

  <meta property="og:image" content="/thumbnail.png" />
  <meta property="og:image:type" content="image/png" />
  <meta property="og:image:height" content="" />
  <meta property="og:image:width" content="" />

  <meta property="og:url" content="https://rgbcube.github.io/" />
  <link rel="canonical" href="https://rgbcube.github.io/">
</head>

<body>
  <style>
    @font-face {
      font-family: "Bai Jamjuree";
      font-weight: 700;
      src: url(BaiJamjuree700.woff2) format(woff2);
    }

    body,
    html {
      height: 100%;
      margin: 0;
    }

    html {
      background-color: #000000;
      font-family: "Bai Jamjuree", sans;
      font-size: 450%;
    }

    a {
      color: #000000;
      text-decoration-line: none;
    }

    .frame {
      background-color: #FFFFFF;

      width: min-content;

      padding: 0 .3em;
      border-radius: 1em;

      user-select: none;
    }

    .frame:hover {
      background-color: #FFFF00;
    }

    .scene {
      height: 100%;
      width: 100%;
      perspective: 15em;

      display: flex;

      align-items: center;
      justify-content: center;
    }

    .cube {
      height: 5em;
      width: 5em;

      position: relative;

      transform: translateZ(-2.5em);
      transform-style: preserve-3d;
    }

    .face {
      background-size: cover;
      background-position: center;

      width: 5em;
      height: 5em;

      display: flex;

      align-items: center;
      justify-content: center;

      position: absolute;
    }

    .face::after {
      z-index: -1;
      content: "";
      width: inherit;
      height: inherit;
      position: absolute;
    }

    .front {
      transform: rotateY(0deg) translateZ(2.5em);
      background: linear-gradient(to bottom, cyan, blue);
    }

    .front::after {
      background: linear-gradient(to bottom, white, magenta);
      mask-image: linear-gradient(to left, magenta, transparent);
    }

    .top {
      transform: rotateX(90deg) translateZ(2.5em);
      background: linear-gradient(to bottom, lime, cyan);
    }

    .top::after {
      background: linear-gradient(to bottom, yellow, white);
      mask-image: linear-gradient(to left, white, transparent);
    }

    .back {
      transform: rotateY(180deg) translateZ(2.5em);
      background: linear-gradient(to bottom, yellow, red);
    }

    .back::after {
      background: linear-gradient(to bottom, lime, black);
      mask-image: linear-gradient(to left, black, transparent);
    }

    .bottom {
      transform: rotateX(-90deg) translateZ(2.5em);
      background: linear-gradient(to bottom, blue, black);
    }

    .bottom::after {
      background: linear-gradient(to bottom, magenta, red);
      mask-image: linear-gradient(to left, red, transparent);
    }

    .right {
      transform: rotateY(90deg) translateZ(2.5em);
      background: linear-gradient(to bottom, white, magenta);
    }

    .right::after {
      background: linear-gradient(to bottom, yellow, red);
      mask-image: linear-gradient(to left, red, transparent);
    }

    .left {
      transform: rotateY(-90deg) translateZ(2.5em);
      background: linear-gradient(to bottom, lime, black);
    }

    .left::after {
      background: linear-gradient(to bottom, cyan, blue);
      mask-image: linear-gradient(to left, blue, transparent);
    }
  </style>

  <div class="scene">
    <div class="cube">
      <div class="face front">
        <a href="/contact">
          <div class="frame">
            <p>contact</p>
          </div>
        </a>
      </div>

      <div class="face top">
        <a href="https://github.com/RGBCube">
          <div class="frame">
            <p>github</p>
          </div>
        </a>
      </div>

      <div class="face back">
        <a href="/">
          <div class="frame">
            <p></p>
          </div>
        </a>
      </div>

      <div class="face bottom">
        <a href="/">
          <div class="frame">
            <p></p>
          </div>
        </a>
      </div>

      <div class="face right">
        <a href="/">
          <div class="frame">
            <p></p>
          </div>
        </a>
      </div>

      <div class="face left">
        <a href="/">
          <div class="frame">
            <p></p>
          </div>
        </a>
      </div>
    </div>
  </div>

  <script>
    const __cube = document.querySelector(".cube");

    class Vec3 {
      static up = new Vec3(0, 1, 0);
      static right = new Vec3(1, 0, 0);

      constructor(x, y, z) {
        this.x = x;
        this.y = y;
        this.z = z;
      }

      normalize() {
        const length = Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2);

        if (length != 0) {
          this.x /= length;
          this.y /= length;
          this.z /= length;
        }
      }

      static sub(v, t) {
        return new Vec3(
          v.x - t.x,
          v.y - t.y,
          v.z - t.z,
        )
      }
    }

    class Vec2 extends Vec3 {
      constructor(x, y) {
        super(x, y, 0)
      }
    }

    class Quat {
      constructor(x, y, z, w) {
        this.x = x;
        this.y = y;
        this.z = z;
        this.w = w;
      }

      static fromAngleAxis(angle, axis) {
        axis.normalize();

        const half = angle / 2;

        const sinHalf = Math.sin(half);
        const cosHalf = Math.cos(half);

        const x = axis.x * sinHalf;
        const y = axis.y * sinHalf;
        const z = axis.z * sinHalf;
        const w = cosHalf;

        return new Quat(x, y, z, w);
      }

      static mul(q, r) {
        return new Quat(
          q.w * r.x + q.x * r.w + q.y * r.z - q.z * r.y,
          q.w * r.y - q.x * r.z + q.y * r.w + q.z * r.x,
          q.w * r.z + q.x * r.y - q.y * r.x + q.z * r.w,
          q.w * r.w - q.x * r.x - q.y * r.y - q.z * r.z,
        );
      }

      apply() {
        __cube.style.transform = `rotate3d(${this.x}, ${this.y}, ${this.z}, ${this.w}rad)`;
      }
    }

    let friction = 0.01;
    let sensitivity = 0.01;
    let velocity = 0;

    const orientation = {
      __value: new Quat(0, 0, 0, 1),

      set(value) {
        this.__value = value;
        this.__value.apply();
      },

      get() {
        return this.__value;
      },
    };

    (() => {
      const mouse = {
        down: false,
        lastMove: window.performance.now(),
        previous: new Vec2(0, 0),
      };

      document.addEventListener("mouseleave", () => {
        mouse.down = false;
      });

      document.addEventListener("mouseup", () => {
        mouse.down = false;
      });

      document.addEventListener("mousedown", (event) => {
        // Disables link dragging that occurs when spinning.
        event.preventDefault();
        mouse.down = true;
      });

      document.addEventListener("mousemove", (event) => {
        if (!mouse.down) return;

        const newMouse = new Vec2(event.clientX, event.clientY);

        if (window.performance.now() - mouse.lastMove > 100) {
          // This is a fresh scroll.
          mouse.previous = newMouse;
        }

        const delta = Vec2.sub(newMouse, mouse.previous);

        mouse.previous = newMouse;
        mouse.lastMove = window.performance.now();

        const rotation = Quat.mul(
          Quat.fromAngleAxis(delta.x * sensitivity, Vec3.up),
          Quat.fromAngleAxis(delta.y * sensitivity, Vec3.right),
        );

        orientation.set(Quat.mul(orientation.get(), rotation));
      });
    })();
  </script>
</body>

</html>

我看不出我哪里做错了,我使用 .fromAngleAxis 使用鼠标增量计算移动四元数并将其应用于方向,但立方体根本无法正确旋转。

修复此问题后,我想使用速度和摩擦力(当前未使用)保持立方体旋转,因此任何与此相关的提示/代码将不胜感激。

这是我希望它如何表现的示例:https://projects.defmech.com/thirdjsobjectrotationwithquaternion/

javascript html css physics quaternions
1个回答
0
投票

解决了,这是未来任何人的代码:

或者如果您想要交互式演示,请看这里:https://rgbcube.github.io/

"use strict";

class Vec3 {
  constructor(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
  }

  static zero() {
    return new Vec(0, 0, 0);
  }

  length() {
    return Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2);
  }

  scale(factor) {
    this.x *= factor;
    this.y *= factor;
    this.z *= factor;

    return this;
  }

  normalize() {
    const length = this.length();

    if (length != 0) {
      this.x /= length;
      this.y /= length;
      this.z /= length;
    }

    return this;
  }

  static sub(v, t) {
    return new Vec3(
      v.x - t.x,
      v.y - t.y,
      v.z - t.z,
    )
  }

  static sum(v, t) {
    return new Vec3(
      v.x + t.x,
      v.y + t.y,
      v.z + t.z,
    )
  }
}

class Quat {
  constructor(x, y, z, w) {
    this.x = x;
    this.y = y;
    this.z = z;
    this.w = w;
  }

  static fromAxis(axis) {
    const angle = axis.length();

    axis.normalize();

    const half = angle / 2;

    const sinHalf = Math.sin(half);
    const cosHalf = Math.cos(half);

    const x = axis.x * sinHalf;
    const y = axis.y * sinHalf;
    const z = axis.z * sinHalf;
    const w = cosHalf;

    return new Quat(x, y, z, w);
  }

  static mul(q, r) {
    return new Quat(
      q.w * r.x + q.x * r.w + q.y * r.z - q.z * r.y,
      q.w * r.y - q.x * r.z + q.y * r.w + q.z * r.x,
      q.w * r.z + q.x * r.y - q.y * r.x + q.z * r.w,
      q.w * r.w - q.x * r.x - q.y * r.y - q.z * r.z,
    );
  }
}

let friction = 3;
let sensitivity = 0.01;
let velocity = new Vec3(0, 0, 0);

const orientation = {
  __cube: document.querySelector(".cube"),
  __value: new Quat(0, 0, 0, 1),

  set(value) {
    this.__value = value;

    const q = this.__value;
    this.__cube.style.transform = `rotate3d(${q.x}, ${q.y}, ${q.z}, ${Math.acos(q.w) * 2}rad)`;
  },

  get() {
    return this.__value;
  },
};

{
  const mouse = {
    down: false,
    lastMove: -10_000,
    previous: null,
  };

  let impulseThisFrame = new Vec3(0, 0, 0);

  const handleUp = () => {
    mouse.down = false;
  };

  document.addEventListener("mouseup", handleUp);
  document.addEventListener("touchend", handleUp);

  const handleDown = (event) => {
    // Disables link dragging that occurs when spinning.
    event.preventDefault();

    mouse.down = true;

    velocity = new Vec3(0, 0, 0);
  };

  document.addEventListener("mousedown", handleDown);
  document.addEventListener("touchstart", handleDown);

  const handleMove = (event) => {
    // Disables scrolling.
    event.preventDefault();

    if (!mouse.down) return;

    const newMouse = new Vec3(event.clientX, event.clientY, 0);

    const timeDelta = (window.performance.now() - mouse.lastMove) / 1000;

    if (timeDelta > 0.1) {
      // This is a fresh scroll.
      mouse.previous = newMouse;
    }

    const delta = Vec3.sub(newMouse, mouse.previous);

    mouse.previous = newMouse;
    mouse.lastMove = window.performance.now();

    const axis = new Vec3(-delta.y, delta.x, 0)
      .normalize()
      .scale(delta.length() * sensitivity);

    impulseThisFrame = Vec3.sum(impulseThisFrame, axis);

    const rotation = Quat.fromAxis(axis);

    orientation.set(Quat.mul(rotation, orientation.get()));
  };

  document.addEventListener("mousemove", handleMove);
  document.addEventListener("touchmove", (event) => {
    const delta = event.changedTouches[0];

    event.clientX = delta.clientX;
    event.clientY = delta.clientY;

    handleMove(event);
  });

  let lastUpdate = 0;

  const updateFrame = (timestamp) => {
    if (lastUpdate == 0) lastUpdate = timestamp;

    const delta = (timestamp - lastUpdate) / 1000;
    lastUpdate = timestamp;

    if (mouse.down) {
      velocity = impulseThisFrame.scale(1 / delta);
      impulseThisFrame = new Vec3(0, 0, 0);
    } else {
      const decay = Math.exp(-delta * friction);

      const effectiveDelta = friction > 0 ? (1 - decay) / friction : delta;

      let theta = effectiveDelta * velocity.length();

      velocity.x *= decay;
      velocity.y *= decay;
      velocity.z *= decay;

      if (friction > 0 && velocity.length() < 0.00001) {
        theta += velocity.length() / friction;

        velocity.x = 0;
        velocity.y = 0;
        velocity.z = 0;
      }

      if (window.performance.now() - mouse.lastMove > 10_000) {
        velocity = new Vec3(1, 1, -1);
      }

      const axis = new Vec3(velocity.x, velocity.y, velocity.z)
        .normalize()
        .scale(theta);

      const rotation = Quat.fromAxis(axis);

      orientation.set(Quat.mul(rotation, orientation.get()));
    }

    requestAnimationFrame(updateFrame);
  };

  updateFrame(0);
}
© www.soinside.com 2019 - 2024. All rights reserved.