如何在缓动鼠标移动事件后旋转画布对象? [英] How to rotate a canvas object following mouse move event with easing?

查看:94
本文介绍了如何在缓动鼠标移动事件后旋转画布对象?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我不确定我是否在这里使用了正确的词语。我想缓和意味着它不会立即跟踪鼠标但有一些延迟?

I am not sure if I have used the right word here. I guess easing means it does not follow the mouse immediately but with some delay?

此时虹膜旋转到我的鼠标方向。如果我希望它与这个?这样做很难或只需要简单的代码更改吗?是否有针对此类问题的标准方法/解决方案?

At the moment the iris is rotating to my mouse direction. What if I want it to have same effect as this?. Is it very hard to do so or just require simple code changes? Is there a standard way/solution for this kind of problem?

这是我当前的代码。它也可以在旋转光圈中找到。

Here is my current code. It can also be found at 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类别作为非确定性缓和。

I actually followed the link I posted in the question and use a similar way to ease the rotation, which I think is similar to what @Blindman67 categories as non-deterministic easing.

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

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

推荐答案

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

There are many ways to do easing. Two methods I will describe in short are deterministic easing and (surprisingly) non-deterministic. The difference being is that the destination of the ease is either known (determined) or unknown (awaiting more user input)

确定性缓和。

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

For this you have a starting value and an end value. What you want to do is find a position between the two based on some time value. That means that the start and end values need to also be associated with a time.

例如

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

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

You will want to find the value at time 150 halfway between the two. To do this you convert the time to a fraction where the time 100 (start) returns 0 and the time 200 (end) return 1, we call this normalised time. You can then multiply the difference between the start and end values by this fraction to find the offset.

因此,对于时间值150来获取值(theValue),我们可以下列。

So for a time value 150 to get the value (theValue) we do the following.

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.25,则缓动函数返回0.1,或0.5返回0.5,0.75返回0.9。正如您所看到的,修改会改变随时间变化的速率。

Now to apply a easing we need to modify the normalised time. A easing function simply takes a value from 0 to 1 inclusive and modifies it. So if you input 0.25 the easing function returns 0.1, or 0.5 return 0.5 and 0.75 returns 0.9. As you can see the modification changes the rate of change over the time.

缓动函数的一个示例。

An example of an easing function.

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,包括0和1)和功率。权力决定了宽松的数量。如果pow = 1则没有缓和,函数返回n。如果pow = 2则该功能与CSS easy in out功能相同,开始慢慢加速然后在结束时减速。如果pow< 1并且pow> 0然后轻松开始快速减速中途然后加速到最后。

This function takes two inputs, the fraction n (0 to 1 inclusive) and the power. The power determines the amount of easing. If pow = 1 then the is no easing and the function returns n. If pow = 2 then the function is the same as the CSS ease in out function, starts slowly speeds up then slows down at the end. if pow < 1 and pow > 0 then the ease start quickly slows down midway and then speeds up to the end.

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

To use the easing function in the above easing value example

// 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;

这是确定性宽松的完成方式

That is how deterministic easing is done

一个优秀的缓动功能页面简化示例和代码以及快速视觉宽松referance

An excellent easing function page Easing examples and code and another page for a quick visual easing referance

非确定性宽松

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

You may not know what the end result of the easing function is as at any time it may change due to new user input, if you use the above methods and change the end value mid way through the ease the result will be inconsistent and ugly. If you have ever done calculus you may recognise that the ease function above is a polynomial and is thus the anti derivative of a simpler function. This function simply determines the amount of change per time step. So for the non deterministic solution all we know is the change for the next time step. For an ease in function (start quick and slow down as we approch the destination) we keep a value to represent the current speed (the rate of change) and modify that speed as needed.

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

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

Then for each time step you do the following

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将approch并最终停在目的地(通过停止我的意思是速度将变得如此之小以至于毫无意义)

Now the currentVal will approch the destination value, if the destination changes than the rate of change (speed) also changes in a consistent way. The currentVal may never get to the destination if the destination is always changing, if however the destination stops changing the current val will approch and eventually stop at destination (by stop I mean the speed will get so small as to be pointless)

此方法行为非常依赖于两个系数,因此使用这些值会改变缓动。有些值可以让你有一点点晃动,有些值会很慢,就像穿过糖蜜一样。

This methods behaviour is very dependent on the two coefficients so playing with those values will vary the easing. Some values will give you an over shoot with a bit of a wobble, others will be very slow like moving through molasses.

你也可以通过添加它来使它变得更加复杂第二个变化率,因此你可以加速加速,这将模拟空气阻力,随着时间的推移改变加速度。您还可以添加最大变化率以设置速度限制。

You can also make it much more complex by adding a second rate of change, thus you can have accelerating acceleration, this will simulate things like air resistance that changes the acceleration over time. You can also add maximums to the rate of change to set speed limits.

这应该可以帮助您进行宽松。

That should help you do your easing.

更多信息
有关详细信息,请参阅我将如何设置动画如何在两点之间进行缩放

非-deterministic easing应用于您的示例

我已将缓动添加到您的函数中,但它引入了一个新问题,当使用循环值(如角度。由于我不想在此答案中进行讨论,因此您可以在查找最小角度中找到解决该问题的方法。

I have added the easing to your function but it has introduced a new problem that will happen when using cyclic values such as angle. As I will not go into it in this answer you can find a solution to that problem in 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();

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

这篇关于如何在缓动鼠标移动事件后旋转画布对象?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆