【问题标题】:How to rotate a canvas object following mouse move event with easing?如何在鼠标移动事件后通过缓动旋转画布对象?
【发布时间】:2016-06-23 00:53:33
【问题描述】:

我不确定我在这里是否使用了正确的词。我猜缓动意味着它不会立即跟随鼠标而是有一些延迟?

目前虹膜正在向我的鼠标方向旋转。如果我希望它具有与this 相同的效果怎么办?这样做很难还是只需要简单的代码更改?这类问题有标准的方法/解决方案吗?

这是我当前的代码。也可以在Rotating Iris 找到。

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
    
class Circle {
    constructor(options) {
      this.cx = options.x;
      this.cy = options.y;
      this.radius = options.radius;
      this.color = options.color;

      this.angle = options.angle;

      this.binding();
    }
      
    binding() {
      const self = this;
      window.addEventListener('mousemove', (e) => {
        self.calculateAngle(e);
      });
    }
      
    calculateAngle(e) {
      if (!e) return;
      let rect = canvas.getBoundingClientRect(),
          vx = e.clientX - this.cx,
          vy = e.clientY - this.cy;
      this.angle = Math.atan2(vy, vx);

    }
      
    renderEye() {
      ctx.setTransform(1, 0, 0, 1, this.cx, this.cy);

      ctx.rotate(this.angle);

      let eyeRadius = this.radius / 3;

      ctx.beginPath();
      ctx.arc(this.radius / 2, 0, eyeRadius, 0, Math.PI * 2);
      ctx.fill();

    }
    
    render() {
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.beginPath();
      ctx.arc(this.cx, this.cy, this.radius, 0, Math.PI * 2);
      ctx.closePath();
      ctx.strokeStyle = '#09f';
      ctx.lineWidth = 1;
      ctx.stroke();

      this.renderMessage();
      this.renderEye();

    }
    
    renderMessage() {
      ctx.font = "18px serif";
      ctx.strokeStyle = 'black';
      ctx.fillText('Angle: ' + this.angle, 30, canvas.height - 40);
    }
}
    
var rotatingCircle = new Circle({
    x: 320,
  y: 160,
  radius: 40,
  color: 'black',
  angle: Math.random() * Math.PI * 2
});

function animate() {
    rotatingCircle.render();
    requestAnimationFrame(animate);
}

animate();
<canvas id='canvas' style='width: 700; height: 500;'></canvas>

更新了可能的解决方案:

我实际上是按照我在问题中发布的链接并使用类似的方式来缓解旋转,我认为这类似于 @Blindman67 将其归类为非确定性缓动。

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

class Circle {
	constructor(options) {
  	this.cx = options.x;
    this.cy = options.y;
    this.radius = options.radius;
    this.color = options.color;
    this.toAngle = 0;
    this.angle = options.angle;
    this.velocity = 0;
    this.maxAccel = 0.04;
    this.binding();
  }
  
  binding() {
  	const self = this;
  	window.addEventListener('mousemove', (e) => {
      self.calculateAngle(e);
    });
  }
  
  calculateAngle(e) {
    if (!e) return;
    let rect = canvas.getBoundingClientRect(),
        // mx = parseInt(e.clientX - rect.left),
        // my = parseInt(e.clientY - rect.top),
        vx = e.clientX - this.cx,
        vy = e.clientY - this.cy;
  	this.toAngle = Math.atan2(vy, vx);

  }
  
  clip(x, min, max) {
    return x < min ? min : x > max ? max : x;
  }

  renderEye() {
    ctx.setTransform(1, 0, 0, 1, this.cx, this.cy);

    let radDiff = 0;
    if (this.toAngle != undefined) {
       radDiff = this.toAngle - this.angle;
    }

    if (radDiff > Math.PI) {
      this.angle += 2 * Math.PI;
    } else if (radDiff < -Math.PI) {
      this.angle -= 2 * Math.PI;
    }

    let easing = 0.06;
    let targetVel = radDiff * easing;
    this.velocity = this.clip(targetVel, this.velocity - this.maxAccel, this.velocity + this.maxAccel);
    this.angle += this.velocity;

    ctx.rotate(this.angle);
        
    let eyeRadius = this.radius / 3;

    ctx.beginPath();
    ctx.arc(this.radius / 2, 0, eyeRadius, 0, Math.PI * 2);
    ctx.fill();

  }

  render() {
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.clearRect(0, 0, canvas.width, canvas.height);
  	ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.beginPath();
    ctx.arc(this.cx, this.cy, this.radius, 0, Math.PI * 2);
    ctx.closePath();
    ctx.strokeStyle = '#09f';
    ctx.lineWidth = 1;
    ctx.stroke();
   
    this.renderMessage();
    this.renderEye();
    
  }

  renderMessage() {
    ctx.font = "18px serif";
    ctx.strokeStyle = 'black';
    ctx.fillText('Angle: ' + this.angle.toFixed(3), 30, canvas.height - 40);
    ctx.fillText('toAngle: ' + this.toAngle.toFixed(3), 30, canvas.height - 20);
  }
}

var rotatingCircle = new Circle({
	x: 250,
  y: 130,
  radius: 40,
  color: 'black',
  angle: Math.random() * Math.PI * 2
});

function animate() {
	rotatingCircle.render();
	requestAnimationFrame(animate);
}

animate();
&lt;canvas id='canvas' style='width: 700; height: 500;'&gt;&lt;/canvas&gt;

【问题讨论】:

    标签: javascript html html5-canvas


    【解决方案1】:

    有很多方法可以进行缓动。我将简要描述的两种方法是确定性缓动和(令人惊讶的)非确定性方法。不同之处在于,轻松的目的地要么是已知的(确定的)要么是未知的(等待更多用户输入)

    确定性缓动。

    为此,您有一个起始值和一个结束值。您要做的是根据某个时间值在两者之间找到一个位置。这意味着开始和结束值也需要与时间相关联。

    例如

    var startVal = 10;
    var startTime = 100;
    var endVal = 100;
    var endTime = 200;
    

    您将希望找到介于两者之间的时间 150 处的值。为此,您将时间转换为时间 100(开始)返回 0,时间 200(结束)返回 1 的分数,我们称之为标准化时间。然后,您可以将开始值和结束值之间的差乘以该分数以找到偏移量。

    所以对于时间值 150 来获取值 (theValue),我们执行以下操作。

    var time = 150;
    var timeDif = endTime - startTime
    var fraction = (startTime - time) / timeDif; // the normalised time
    var valueDif = endVal - startVal;
    var valueOffset = valueDif * fraction;
    var theValue = startVal + valueOffset;
    

    或更简洁。

    // nt is normalised time
    var nt = (startTime - time) / (endTime - startTime)
    var theValue = startVal + (endVal - startVal) * nt;
    

    现在要应用缓动,我们需要修改标准化时间。缓动函数只是取一个从 0 到 1 的值(包括 0 到 1)并对其进行修改。因此,如果您输入 0.25,缓动函数返回 0.1,或者 0.5 返回 0.5,而 0.75 返回 0.9。如您所见,修改会随着时间的推移而改变变化率。

    缓动函数的示例。

    var easeInOut = function (n, pow) {
        n = Math.min(1, Math.max(0, n)); // clamp n
        var nn = Math.pow( n, pow);
        return (nn / ( nn + Math.pow(1 - n, pow)))
    }
    

    这个函数有两个输入,分数 n(包括 0 到 1)和幂。权力决定了宽松的程度。如果 pow = 1 则没有缓动并且函数返回 n。如果 pow = 2 则该功能与 CSS 缓入输出功能相同,开始时缓慢加速,然后在结束时减速。如果 pow 0,则轻松开始会在中途迅速减速,然后加速到结束。

    在上述缓动值示例中使用缓动函数

    // nt is normalised time
    var nt = (startTime - time) / (endTime - startTime);
    nt = easeInOut(nt,2); // start slow speed up, end slow
    var theValue = startVal + (endVal - startVal) * nt;
    

    这就是确定性缓动的完成方式

    一个出色的缓动功能页面Easing examples 和代码以及另一个页面quick visual easing referance

    非确定性缓动

    您可能不知道缓动函数的最终结果是什么,因为它可能会因新用户输入而随时发生变化,如果您使用上述方法并在缓动中途更改结束值,结果将不一致和丑陋的。如果你曾经做过微积分,你可能会认识到上面的缓和函数是一个多项式,因此是一个更简单函数的反导数。该函数只是确定每个时间步的变化量。因此,对于非确定性解决方案,我们所知道的只是下一个时间步的变化。为了简化功能(快速开始并在接近目的地时减速),我们保留一个值来表示当前速度(变化率)并根据需要修改该速度。

    const ACCELERATION_COEFFICIENT = 0.3;
    const DRAG_COEFFICIENT = 0.99;
    var currentVal = 100;
    var destinationVal = 200;
    var currentSpeed = 0;
    

    然后对于每个时间步执行以下操作

    var accel = destinationVal - currentVal;  // get the acceleration
    accel *= ACCELERATION_COEFFICIENT; // modify it so we are not there instantly
    currentSpeed += accel; // add that to the speed
    currentSpeed *= DRAG_COEFFICIET; // add some drag to further ease the function as it approaches destination
    currentVal += currentSpeed; // add the speed to the current value
    

    现在 currentVal 将接近目标值,如果目标变化比变化率(速度)也以一致的方式变化。如果目的地一直在变化,currentVal 可能永远不会到达目的地,但是如果目的地停止改变,当前 val 将接近并最终在目的地停止(我所说的停止是指速度会变得如此之小以至于毫无意义)

    此方法的行为非常依赖于两个系数,因此使用这些值会改变缓动。一些值会让你在拍摄时有点晃动,而另一些值会像在糖蜜中移动一样非常缓慢。

    您还可以通过添加第二个变化率来使其更加复杂,因此您可以加速加速,这将模拟空气阻力等随时间变化的加速度。您还可以为变化率添加最大值以设置速度限制。

    这应该可以帮助你放松。

    更多信息 有关更多信息,请参阅这些答案 How would I animateHow to scale between two points

    应用于您的示例的非确定性缓动

    我已将缓动添加到您的函数中,但它引入了一个新问题,当使用诸如角度之类的循环值时会发生该问题。由于我不会在此答案中深入探讨,您可以在 Finding the smallest angle 中找到该问题的解决方案。

    var canvas = document.getElementById('canvas');
    var ctx = canvas.getContext('2d');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    const ACCELERATION_COEFFICIENT = 0.15;
    const DRAG_COEFFICIENT = 0.5;        
    class Circle {
        constructor(options) {
          this.cx = options.x;
          this.cy = options.y;
          this.radius = options.radius;
          this.color = options.color;
    
          this.angle = options.angle;
          this.angleSpeed = 0;
          this.currentAngle = this.angle;
    
          this.binding();
        }
          
        binding() {
          const self = this;
          window.addEventListener('mousemove', (e) => {
            self.calculateAngle(e);
          });
        }
          
        calculateAngle(e) {
          if (!e) return;
          let rect = canvas.getBoundingClientRect(),
              vx = e.clientX - this.cx,
              vy = e.clientY - this.cy;
          this.angle = Math.atan2(vy, vx);
    
        }
          
        renderEye() {
          // this should be in a separate function 
          this.angleSpeed += (this.angle - this.currentAngle) * ACCELERATION_COEFFICIENT;
          this.angleSpeed *= DRAG_COEFFICIENT;
          this.currentAngle += this.angleSpeed;
    
    
          ctx.setTransform(1, 0, 0, 1, this.cx, this.cy);
    
          ctx.rotate(this.currentAngle);
    
          let eyeRadius = this.radius / 3;
    
          ctx.beginPath();
          ctx.arc(this.radius / 2, 0, eyeRadius, 0, Math.PI * 2);
          ctx.fill();
    
        }
        
        render() {
          ctx.setTransform(1, 0, 0, 1, 0, 0);
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          ctx.setTransform(1, 0, 0, 1, 0, 0);
          ctx.beginPath();
          ctx.arc(this.cx, this.cy, this.radius, 0, Math.PI * 2);
          ctx.closePath();
          ctx.strokeStyle = '#09f';
          ctx.lineWidth = 1;
          ctx.stroke();
    
          this.renderMessage();
       
          this.renderEye();
    
        }
        
        renderMessage() {
          ctx.font = "18px serif";
          ctx.strokeStyle = 'black';
          ctx.fillText('Angle: ' + this.angle, 30, canvas.height - 40);
        }
    }
        
    var rotatingCircle = new Circle({
        x: 320,
      y: 160,
      radius: 40,
      color: 'black',
      angle: Math.random() * Math.PI * 2
    });
    
    function animate() {
        rotatingCircle.render();
        requestAnimationFrame(animate);
    }
    
    animate();
    &lt;canvas id='canvas' style='width: 700; height: 500;'&gt;&lt;/canvas&gt;

    【讨论】:

    • 您的两种方法都非常有帮助。我学到了很多东西。然而,在这个例子中,在 -2.9 附近的角度有一个小故障,虹膜会很快跳回底部。我已经用一个可能的解决方案更新了我的问题,该解决方案受到我在问题中发布的链接的启发。你能看看这个好不好?
    • @newguy 是的,(你的解决方案)是解决故障的一个很好的解决方案..
    • 您还可以通过获取Math.asin(crossProduct(v1,v2)) 找到两个向量之间的角度,其中 v1 是距眼睛中心的当前向量,v2 是前一个向量。 (注意向量必须标准化)。这将为您提供角度的相对变化,而不是绝对角度。交叉产品是v1.x * v2.y - v1.y * v2.x
    【解决方案2】:

    据我所知,使用 h5 画布,您可能需要自己编写缓动函数。然而,css3动画有几个内置的缓动函数,你可以写.foo {transition: bottom 1s ease},当.foo元素的bottom样式属性改变时,它们会以ease函数定义的速度移动。见:https://developer.mozilla.org/en-US/docs/Web/CSS/transitionhttps://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions

    另外,看看这些惊人的 BB-8 动画(用 css 动画构建):http://codepen.io/mdixondesigns/pen/PPEJwzhttp://codepen.io/Chyngyz/pen/YWwYGqhttp://codepen.io/bullerb/pen/gMpxNZ

    【讨论】:

      猜你喜欢
      • 2019-07-10
      • 1970-01-01
      • 1970-01-01
      • 2014-07-11
      • 1970-01-01
      • 2013-02-23
      • 1970-01-01
      • 2011-03-27
      • 2021-08-08
      相关资源
      最近更新 更多