【问题标题】:How to rotate square following mouse by mouse movement angle?如何通过鼠标移动角度旋转鼠标跟随的正方形?
【发布时间】:2021-10-13 01:51:32
【问题描述】:

我有一个跟随光标的正方形。 它的边框顶部是红色的,以查看旋转是否正确。 我正在尝试根据鼠标移动角度旋转它。就像如果鼠标向右上方移动 45 度,那么正方形必须旋转 45 度。 问题是当我慢慢移动鼠标时,方块开始疯狂地旋转。但如果我将鼠标移动得足够快,正方形就会非常顺畅地旋转。

实际上,这只是我想要完成的任务的一部分。我的整个任务是制作自定义圆形光标,当鼠标移动时会伸展。我试图实现的想法: 通过鼠标移动角度旋转圆圈,然后缩放它以产生拉伸效果。但由于我上面描述的问题,我不能这样做。当鼠标速度慢时,我需要我的跟随者平稳旋转。

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>

【问题讨论】:

  • 尽量不要默认为90度?另外,只有在光标移动时才动画?
  • 我将 90degs 添加到 atan2 结果,因为它的输出与我传递给手动旋转的内容不对应。示例:如果我将光标移动到右上角(旋转函数中为 45 度),atan2 将返回 -45。我添加 90 使其变为 45,然后传递给旋转()。您可以自己删除 +90,看看这不是问题。
  • 这不是增加 90 度。 1) 直线移动将使角度默认为 90 度。 2)删除您的requestAnimationFrame,因为它也默认为90度。无法为您提供更多帮助,因为我无法找到解决 #1 的方法。
  • 平均角度 => 存储最后 3 个点左右并计算角度,例如 a1 = α(curr, L1), a2 = α(curr, L2), a3 = α(curr, L3), than a = a1 * w1 + a2 *w2 + a3*w3 其中 (w1 + w2 w3 = 1) 和 (w1 > w2 > w3)。权重因子 w1、w2、w3 是经验性的,您应该为它们找到一些不错的值,例如 (w1 = 0.5, w2 = 0.35, w3 = 0.15)。其他方法是通过几个点构建一条曲线并使用插值并找到一个切线(这可能太多了)。只要只使用两个点,您的动画就不会流畅。
  • 一切尽在掌握,算法本身几乎和你能得到的一样流畅。看看this other fiddle。它将光标(10 秒后)从非常平滑变为真正快速的银色光标,它可以感知鼠标移动中的最小变化。您可以使用参数(在类的开头)来调整光标行为。您还可以通过在代码末尾的最后一个 if 条件中将 0 切换为 1 来检查自动移动(在之前的 if 中将 1 切换为 0)。

标签: javascript


【解决方案1】:

平滑地跟随光标切线并不像最初感觉那么简单。在现代浏览器中,mousemove 事件会在附近以帧速率(通常为 60 FPS)触发。当鼠标缓慢移动时,光标在事件之间仅移动一两个像素。计算角度时,1px 的垂直 + 水平移动解析为 45deg。然后还有一个问题,事件触发率并不一致,在鼠标移动过程中,事件触发率可能会下降到 30 FPS 甚至 24 FPS,这实际上有助于获得更准确的角度,但会使比例计算严重不准确(你真正的任务似乎也需要规模计算)。

一种解决方案是使用CSS Transitions 使动画更流畅。但是,添加过渡会使角度计算变得更加复杂,因为在交叉 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;
}
&lt;div class="crsr hidden"&gt;A&lt;/div&gt;

代码的基本思想是等到鼠标移动了足够多的像素(threshold)来计算角度。通过将角度设置为相同位置来解决“疯狂圆圈”效应,但在穿过 PI 时设置为负角度。这种变化在渲染之间是不可见的。

CSS 变量用于transform 中的实际值,这允许同时更改转换函数的单个参数,您不必重写整个规则。 setCss 方法只是语法糖,它使代码更短。

当前参数显示一个矩形跟随器,就像您的问题一样。设置前。 stretchMax = 300stretchSlow = 125 并为 CSS 添加 50% 的边框半径可能接近您最终需要的。 stretchRate 定义与鼠标速度相关的拉伸。如果慢动作对于您的目的仍然不够流畅,您可以为// Smoothen small angles 部分创建更好的算法(在crsrMove 方法中)。您可以在jsFiddle 处使用参数。

【讨论】:

    【解决方案2】:

    这样试试

    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();
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-18
      • 1970-01-01
      • 2020-02-14
      • 1970-01-01
      • 1970-01-01
      • 2017-05-03
      相关资源
      最近更新 更多