如何在鼠标移动事件后通过缓动旋转画布对象?

2022-01-17 00:00:00 javascript html html5-canvas

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

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

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

var canvas = document.getElementById('canvas');var ctx = canvas.getContext('2d');canvas.width = window.innerWidth;canvas.height = window.innerHeight;类圈{构造函数(选项){this.cx = 选项.x;this.cy = 选项.y;this.radius = options.radius;this.color = options.color;this.angle = options.angle;this.binding();}捆绑() {常量自我 = 这个;window.addEventListener('mousemove', (e) => {self.calculateAngle(e);});}计算角度(e){如果(!e)返回;让 rect = canvas.getBoundingClientRect(),vx = e.clientX - this.cx,vy = e.clientY - this.cy;this.angle = Math.atan2(vy, vx);}渲染眼(){ctx.setTransform(1, 0, 0, 1, this.cx, this.cy);ctx.rotate(this.angle);让 eyeRadius = this.radius/3;ctx.beginPath();ctx.arc(this.radius/2, 0, eyeRadius, 0, Math.PI * 2);ctx.fill();}使成为() {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();}渲染消息(){ctx.font = "18px 衬线";ctx.strokeStyle = '黑色';ctx.fillText('角度:' + this.angle, 30, canvas.height - 40);}}var 旋转圆 = 新圆({×:320,y: 160,半径:40,颜色:黑色',角度:Math.random() * Math.PI * 2});函数动画(){旋转圆.render();requestAnimationFrame(动画);}动画();

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

更新了可能的解决方案:

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

var canvas = document.getElementById('canvas');var ctx = canvas.getContext('2d');canvas.width = window.innerWidth;canvas.height = window.innerHeight;类圈{构造函数(选项){this.cx = 选项.x;this.cy = 选项.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();}捆绑() {常量自我 = 这个;window.addEventListener('mousemove', (e) => {self.calculateAngle(e);});}计算角度(e){如果(!e)返回;让 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);}剪辑(x,最小,最大){返回 x <分钟?最小:x >最大限度 ?最大值:x;}渲染眼(){ctx.setTransform(1, 0, 0, 1, this.cx, this.cy);让 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;}让缓动 = 0.06;让 targetVel = radDiff * 缓动;this.velocity = this.clip(targetVel, this.velocity - this.maxAccel, this.velocity + this.maxAccel);this.angle += this.velocity;ctx.rotate(this.angle);让 eyeRadius = this.radius/3;ctx.beginPath();ctx.arc(this.radius/2, 0, eyeRadius, 0, Math.PI * 2);ctx.fill();}使成为() {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();}渲染消息(){ctx.font = "18px 衬线";ctx.strokeStyle = '黑色';ctx.fillText('角度:' + this.angle.toFixed(3), 30, canvas.height - 40);ctx.fillText('toAngle: ' + this.toAngle.toFixed(3), 30, canvas.height - 20);}}var 旋转圆 = 新圆({×:250,y: 130,半径:40,颜色:黑色',角度:Math.random() * Math.PI * 2});函数动画(){旋转圆.render();requestAnimationFrame(动画);}动画();

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

解决方案

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

确定性缓动.

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

例如

var startVal = 10;变量开始时间 = 100;var endVal = 100;var endTime = 200;

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

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

var time = 150;var timeDif = endTime - startTimevar 分数 = (startTime - time)/timeDif;//标准化时间var valueDif = endVal - startVal;var valueOffset = valueDif * 分数;var theValue = startVal + valueOffset;

或更简洁.

//nt 是标准化时间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));//钳位nvar nn = Math.pow(n, pow);返回 (nn/( nn + Math.pow(1 - n, pow)))}

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

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

//nt 是标准化时间var nt = (startTime - time)/(endTime - startTime);nt = easeInOut(nt,2);//开始慢速加速,结束慢速var theValue = startVal + (endVal - startVal) * nt;

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

一个出色的缓动功能页面缓动示例和代码和另一个页面快速视觉缓动参考

非确定性缓动

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

const ACCELERATION_COEFFICIENT = 0.3;常数 DRAG_COEFFICIENT = 0.99;var currentVal = 100;var 目标值 = 200;var currentSpeed = 0;

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

var accel = destinationVal - currentVal;//获取加速度加速度 *= ACCELERATION_COEFFICIENT;//修改它,这样我们就不会立即出现当前速度 += 加速度;//把它加到速度上currentSpeed *= DRAG_COEFFICIET;//在接近目的地时添加一些阻力以进一步简化功能currentVal += currentSpeed;//将速度添加到当前值

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

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

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

这应该可以帮助你放松.

更多信息有关更多信息,请参阅这些答案 我如何制作动画 和 如何在两点之间进行缩放

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

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

var canvas = document.getElementById('canvas');var ctx = canvas.getContext('2d');canvas.width = window.innerWidth;canvas.height = window.innerHeight;常量 ACCELERATION_COEFFICIENT = 0.15;常量 DRAG_COEFFICIENT = 0.5;类圈{构造函数(选项){this.cx = 选项.x;this.cy = 选项.y;this.radius = options.radius;this.color = options.color;this.angle = options.angle;this.angleSpeed = 0;this.currentAngle = this.angle;this.binding();}捆绑() {常量自我 = 这个;window.addEventListener('mousemove', (e) => {self.calculateAngle(e);});}计算角度(e){如果(!e)返回;让 rect = canvas.getBoundingClientRect(),vx = e.clientX - this.cx,vy = e.clientY - this.cy;this.angle = Math.atan2(vy, vx);}渲染眼(){//这应该在一个单独的函数中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);让 eyeRadius = this.radius/3;ctx.beginPath();ctx.arc(this.radius/2, 0, eyeRadius, 0, Math.PI * 2);ctx.fill();}使成为() {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();}渲染消息(){ctx.font = "18px 衬线";ctx.strokeStyle = '黑色';ctx.fillText('角度:' + this.angle, 30, canvas.height - 40);}}var 旋转圆 = 新圆({×:320,y: 160,半径:40,颜色:黑色',角度:Math.random() * Math.PI * 2});函数动画(){旋转圆.render();requestAnimationFrame(动画);}动画();

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

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>

Updated with possible solution:

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)

Deterministic easing.

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.

For example

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

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.

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;

or more concise.

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

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)))
}

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

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

Non-deterministic easing

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

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.

More info For more info see these answers How would I animate and How to scale between two points

Non-deterministic easing applied to your example

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>

相关文章