如何根据鼠标移动角度旋转正方形?

3
我有一个方块跟随我的鼠标,其边框顶部是红色的以便看到旋转是否正确。我试图根据鼠标移动的角度来旋转它,比如如果鼠标向右上方移动45度,则方块必须旋转45度。问题在于,当我慢慢移动鼠标时,方块开始疯狂旋转。但如果我移动鼠标得足够快,方块旋转得很平滑。
实际上这只是我要完成的任务的一部分。我的整个任务是制作自定义圆形光标,在鼠标移动时拉伸。我要实现的想法是:按鼠标移动角度旋转圆形,然后将其进行scaleX操作以实现拉伸效果。但由于上述问题,我无法做到这一点。我需要我的跟随者在鼠标速度较慢时平稳旋转。

class Cursor {
    constructor() {
        this.prevX = null;
        this.prevY = null;
        this.curX = null;
        this.curY = null;
        this.angle = null;

        this.container = document.querySelector(".cursor");
        this.follower = this.container.querySelector(".cursor-follower");

        document.addEventListener("mousemove", (event) => {
            this.curX = event.clientX;
            this.curY = event.clientY;
        });

        this.position();
    }


    position(timestamp) {
        this.follower.style.top = `${this.curY}px`;
        this.follower.style.left = `${this.curX}px`;


        this.angle = Math.atan2(this.curY - this.prevY, this.curX - this.prevX) * 180/Math.PI;
        console.log(this.angle + 90);

        this.follower.style.transform = `rotateZ(${this.angle + 90}deg)`;

        this.prevX = this.curX;
        this.prevY = this.curY;

        requestAnimationFrame(this.position.bind(this));
    }
}


        const cursor = new Cursor();
.cursor-follower {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 9999;

    pointer-events: none;
    user-select: none;

    width: 76px;
    height: 76px;
    margin: -38px;
    border: 1.5px solid #000;

    border-top: 1.5px solid red;
}
<div class="cursor">
  <div class="cursor-follower"></div>
</div>


1
尝试不要默认为90度?同时,只有在光标移动时才进行动画? - Rickard Elimää
这不是关于添加90度的问题。1)直线移动将使角度默认为90度。2)删除您的requestAnimationFrame,因为它也默认为90度。无法提供更多帮助,因为我无法找到#1的解决方案。 - Rickard Elimää
1
平均角度 => 存储最近的3个点,计算像a1 = α(curr, L1), a2 = α(curr, L2), a3 = α(curr, L3)这样的角度,然后a = a1 * w1 + a2 w2 + a3w3,其中(w1 + w2 w3 = 1)且(w1 > w2 > w3)。权重因子w1、w2、w3是经验性的,您应该找到一些好的值,如(w1 = 0.5,w2 = 0.35,w3 = 0.15)。另一种方法是通过几个点构建曲线,使用插值并找到切线(这可能太多了)。只要使用两个点,您的动画就不会平滑。 - DraganS
此外,您需要处理+/-PI/2的情况,因为在那里atan2会变得“疯狂”commons.wikimedia.org/wiki/File:Arctangent2.svg。这可能有些过度,但通常en.wikipedia.org/wiki/Quaternions_and_spatial_rotation可以帮助避免atan问题,即使对于像您这样简单的情况也是如此。目前不知道具体如何处理(已经很久没有参与视频游戏行业了)。 - DraganS
1
一切都掌握在你手中,算法本身几乎是尽可能平滑的。看看这个其他小玩意儿。它会在10秒后将光标从相当平滑变成真正快速的银色光标,可以感知鼠标移动中最微小的变化。您可以通过调整类开头的参数来调整光标行为。您还可以通过在代码末尾的最后一个if条件中将0切换为1来检查自动移动(在上一个if中将1切换为0)。 - Teemu
显示剩余10条评论
2个回答

1
紧跟光标移动并不像一开始感觉的那样简单。在现代浏览器中,mousemove事件以帧速率(通常为60 FPS)附近触发。当鼠标移动缓慢时,光标在事件之间仅移动一个或两个像素。在计算角度时,竖直+水平1px的移动相当于45度。然后还有另一个问题,事件触发率不一致,在鼠标移动期间,事件触发率可能会降至30 FPS,甚至24 FPS,这实际上有助于获得更准确的角度,但使比例计算严重不准确(你真正的任务似乎也需要比例计算)。
其中一种解决方案是使用CSS转换使动画更加平滑。但是,添加过渡会使角度计算变得更加复杂,因为当使用过渡时,跨越PI时返回的负角度和正角度Math.atan2之间的跳跃将变得可见。
以下是如何使用过渡使光标跟随器更加平滑的示例代码。

class Follower {
  // Default options
  threshold = 4;
  smoothness = 10;
  stretchRate = 100;
  stretchMax = 100;
  stretchSlow = 100;
  baseAngle = Math.PI / 2;
  // Class initialization
  initialized = false;
  // Listens mousemove event
  static moveCursor (e) {
    if (Follower.active) {
      Follower.prototype.crsrMove.call(Follower.active, e);
    }
  }
  static active = null;
  // Adds/removes mousemove listener
  static init () {
    if (this.initialized) {
      document.removeEventListener('mousemove', this.moveCursor);
      if (this.active) {
        this.active.cursor.classList.add('hidden');
      }
    } else {
      document.addEventListener('mousemove', this.moveCursor);
    }
    this.initialized = !this.initialized;
  }
  // Base values of instances
  x = -1000;
  y = -1000;
  angle = 0;
  restoreTimer = -1;
  stamp = 0;
  speed = [0];
  // Prototype properties
  constructor (selector) {
    this.cursor = document.querySelector(selector);
    this.restore = this.restore.bind(this);
  }
  // Activates a new cursor
  activate (options = {}) {
    // Remove the old cursor
    if (Follower.active) {
      Follower.active.cursor.classList.add('hidden');
      Follower.active.cursor.classList.remove('cursor', 'transitioned');
    }
    // Set the new cursor
    Object.assign(this, options);
    this.setCss = this.cursor.style.setProperty.bind(this.cursor.style);
    this.cursor.classList.remove('hidden');
    this.cHW = this.cursor.offsetWidth / 2;
    this.cHH = this.cursor.offsetHeight / 2;
    this.setCss('--smoothness', this.smoothness / 100 + 's');
    this.cursor.classList.add('cursor');
    setTimeout(() => this.cursor.classList.add('transitioned'), 0); // Snap to the current angle
    this.crsrMove({
      clientX: this.x,
      clientY: this.y
    });
    Follower.active = this;
    return this;
  }
  // Moves the cursor with effects
  crsrMove (e) {
    clearTimeout(this.restoreTimer); // Cancel reset timer
    const PI = Math.PI,
      pi = PI / 2,
      x = e.clientX,
      y = e.clientY,
      dX = x - this.x,
      dY = y - this.y,
      dist = Math.hypot(dX, dY);
    let rad = this.angle + this.baseAngle,
      dTime = e.timeStamp - this.stamp,
      len = this.speed.length,
      sSum = this.speed.reduce((a, s) => a += s),
      speed = dTime
        ? ((1000 / dTime) * dist + sSum) / len
        : this.speed[len - 1], // Old speed when dTime = 0
      scale = Math.min(
        this.stretchMax / 100,
        Math.max(speed / (500 - this.stretchRate || 1),
          this.stretchSlow / 100
        )
      );
    // Update base values and rotation angle
    if (isNaN(dTime)) {
      scale = this.scale;
    } // Prevents a snap of a new cursor
    if (len > 5) {
      this.speed.length = 1;
    }
    // Update angle only when mouse has moved enough from the previous update
    if (dist > this.threshold) {
      let angle = Math.atan2(dY, dX),
        dAngle = angle - this.angle,
        adAngle = Math.abs(dAngle),
        cw = 0;
      // Smoothen small angles
      if (adAngle < PI / 90) {
        angle += dAngle * 0.5;
      }
      // Crossing ±PI angles
      if (adAngle >= 3 * pi) {
        cw = -Math.sign(dAngle) * Math.sign(dX); // Rotation direction: -1 = CW, 1 = CCW
        angle += cw * 2 * PI - dAngle; // Restores the current position with negated angle
        // Update transform matrix without transition & rendering
        this.cursor.classList.remove('transitioned');
        this.setCss('--angle', `${angle + this.baseAngle}rad`);
        this.cursor.offsetWidth; // Matrix isn't updated without layout recalculation
        this.cursor.classList.add('transitioned');
        adAngle = 0; // The angle was handled, prevent further adjusts
      }
      // Orthogonal mouse turns
      if (adAngle >= pi && adAngle < 3 * pi) {
        this.cursor.classList.remove('transitioned');
        setTimeout(() => this.cursor.classList.add('transitioned'), 0);
      }
      rad = angle + this.baseAngle;
      this.x = x;
      this.y = y;
      this.angle = angle;
    }
    this.scale = scale;
    this.stamp = e.timeStamp;
    this.speed.push(speed);
    // Transform the cursor
    this.setCss('--angle', `${rad}rad`);
    this.setCss('--scale', `${scale}`);
    this.setCss('--tleft', `${x - this.cHW}px`);
    this.setCss('--ttop', `${y - this.cHH}px`);
    // Reset the cursor when mouse stops
    this.restoreTimer = setTimeout(this.restore, this.smoothness + 100, x, y);
  }
  // Returns the position parameters of the cursor
  position () {
    const {x, y, angle, scale, speed} = this;
    return {x, y, angle, scale, speed};
  }
  // Restores the cursor
  restore (x, y) {
    this.state = 0;
    this.setCss('--scale', 1);
    this.scale = 1;
    this.speed = [0];
    this.x = x;
    this.y = y;
  }
}
Follower.init();

const crsr = new Follower('.crsr').activate();
body {
  margin: 0px;
}

.crsr {
  width: 76px;
  height: 76px;
  border: 2px solid #000;
  border-radius: 0%;
  text-align: center;
  font-size: 20px;
}

.cursor {
  position: fixed;
  cursor: default;
  user-select: none;
  left: var(--tleft);
  top: var(--ttop);
  transform: rotate(var(--angle)) scaleY(var(--scale));
}

.transitioned {
  transition: transform var(--smoothness) linear;
}

.hidden {
  display: none;
}
<div class="crsr hidden">A</div>

代码的基本思想是等待鼠标移动足够的像素(threshold)来计算角度。"疯狂圆形"效应通过在穿过PI时将角度设置为相同位置但取反角度来解决。这个变化在渲染之间做出了不可见的改变。
CSS变量用于transform中的实际值,这允许一次更改转换函数的单个参数,无需重写整个规则。setCss方法只是语法糖,可以使代码更短一些。
当前的参数显示一个矩形跟随者,就像你的问题一样。例如,设置stretchMax = 300stretchSlow = 125,并在CSS中添加50%的边框半径,可能接近您最终需要的内容。stretchRate定义与鼠标速度相关的拉伸。如果慢动作仍然不够流畅,您可以创建一个更好的算法来处理// Smoothen small angles部分(在crsrMove方法中)。您可以在jsFiddle上尝试调整参数。

0

试试这样

class Cursor {
    constructor() {
        this.prevX = null;
        this.prevY = null;
        this.curX = null;
        this.curY = null;
        this.angle = null;

        this.container = document.querySelector(".cursor");
        this.follower = this.container.querySelector(".cursor-follower");

        document.addEventListener("mousemove", (event) => {
            this.curX = event.clientX;
            this.curY = event.clientY;
        });

        this.position();
    }


    position(timestamp) {
        this.follower.style.top = `${this.curY}px`;
        this.follower.style.left = `${this.curX}px`;


        if (this.curY !== this.prevY && this.curX !== this.prevX) {
        this.angle = Math.atan2(this.curY - this.prevY, this.curX - this.prevX) * 180/Math.PI;
        }
        console.log(this.angle + 90);

        this.follower.style.transform = `rotateZ(${this.angle + 90}deg)`;

        this.prevX = this.curX;
        this.prevY = this.curY;

        requestAnimationFrame(this.position.bind(this));
    }
}


        const cursor = new Cursor();

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