Canvas,使图像遵循贝塞尔曲线

Canvas, make an image follow a Bezier curve

我想用 canvas 图像创建半圆后的动画。起初我尝试使用 canvas arc 但我发现使用 bezier curve.

更简单

但现在我遇到了一个问题,因为它不在圆上,所以我找不到一种方法让它根据它的位置旋转,就像手表指针一样。到目前为止,这是我的代码。

var c = document.getElementById('myCanvas');
var ctx = c.getContext('2d');

var statue = new Image();
statue.src = 'https://i.ibb.co/3TvCH8n/liberty.png';

function getQuadraticBezierXYatPercent(startPt, controlPt, endPt, percent) {
  var x =
    Math.pow(1 - percent, 2) * startPt.x +
    2 * (1 - percent) * percent * controlPt.x +
    Math.pow(percent, 2) * endPt.x;
  var y =
    Math.pow(1 - percent, 2) * startPt.y +
    2 * (1 - percent) * percent * controlPt.y +
    Math.pow(percent, 2) * endPt.y;
  return { x: x, y: y };
}

const startPt = { x: 600, y: 200 };
const controlPt = { x: 300, y: 100 };
const endPt = { x: 0, y: 200 };

var percent = 0;

statue.addEventListener('load', () => {
  animate();
});

function animate() {
  //console.log(percent);
  ctx.clearRect(0, 0, c.width, c.height);
  percent > 1 ? (percent = 0) : (percent += 0.003);
  var point = getQuadraticBezierXYatPercent(startPt, controlPt, endPt, percent);

  ctx.drawImage(
    statue,
    0,
    0,
    statue.width,
    statue.height,
    point.x - 50,
    point.y - 50,
    100,
    100
  );
  //ctx.fillRect(point.x, point.y, 10, 10);
  requestAnimationFrame(animate);
}
<!DOCTYPE html>
<html>
<body>

<canvas id="myCanvas" width="600" height="200" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>

<script>

</script> 

</body>
</html>

这样做的正确方法是什么?

您可以使用相同的函数在曲线上的当前点之前获取另一个点,然后使用 Math.atan2 获取两点之间的角度。

然后,您需要使用 ctx.translate()ctx.rotate() 来改变转换矩阵,而不是在 .drawImage() 调用中设置位置。 (动画方法开始时的 .setTransform() 调用会重置每一帧的矩阵。)

我这里还加了个“洋葱皮”效果,这样动起来效果更好看。

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

var statue = new Image();
statue.src = "https://i.ibb.co/3TvCH8n/liberty.png";

function getQuadraticBezierXYatPercent(startPt, controlPt, endPt, percent) {
  var x =
    Math.pow(1 - percent, 2) * startPt.x +
    2 * (1 - percent) * percent * controlPt.x +
    Math.pow(percent, 2) * endPt.x;
  var y =
    Math.pow(1 - percent, 2) * startPt.y +
    2 * (1 - percent) * percent * controlPt.y +
    Math.pow(percent, 2) * endPt.y;
  return { x: x, y: y };
}

const startPt = { x: 600, y: 200 };
const controlPt = { x: 300, y: 100 };
const endPt = { x: 0, y: 200 };

var percent = 0;

statue.addEventListener("load", () => {
  ctx.clearRect(0, 0, c.width, c.height);
  animate();
});

function animate() {
  ctx.setTransform(1, 0, 0, 1, 0, 0);
  // "Onion skin" effect so the last frame is slightly retained to better show the motion.
  ctx.fillStyle = "rgba(255,255,255,0.1)";
  ctx.fillRect(0, 0, c.width, c.height);
  
  percent = (percent + 0.003) % 1;
  
  var point = getQuadraticBezierXYatPercent(startPt, controlPt, endPt, percent);
  var lastPoint = getQuadraticBezierXYatPercent(startPt, controlPt, endPt, percent - 0.003);
  var angle = Math.atan2(lastPoint.y - point.y, lastPoint.x - point.x);
  
  // Debug pointer line
  ctx.beginPath();
  ctx.moveTo(point.x, point.y);
  ctx.lineTo(point.x + Math.cos(angle) * 50, point.y + Math.sin(angle) * 50);
  ctx.stroke();
  
  // Actual drawing
  ctx.translate(point.x, point.y);
  ctx.rotate(angle);
  ctx.drawImage(statue, 0, 0, statue.width, statue.height, -50, -50, 100, 100);
  requestAnimationFrame(animate);
}
<canvas id="myCanvas" width="600" height="400" style="border:1px solid #d3d3d3;">
 

无论如何都使用一个圆圈,方式更容易,但不要试图将东西放在坐标上,相反:设置坐标system到正确的偏移量和角度,这样你就可以在相同的坐标上绘制相同的东西。

我们观察到我们正在处理以 (300,600) 为原点、半径为 500、起始角(以弧度为单位)为 0.9272951769 和结束角(以弧度为单位)为 2.21429922 的圆形路径, 所以我们可以移动我们的坐标系,使 (0,0) “真的”是 (300,600),然后当我们围绕新的 (0,0) 旋转坐标系任意角度时,我们需要做的就是绘制在 (radius,0)

处的东西

因为坐标系为我们做旋转,我们不需要实际计算任何点。我们已经知道我们想要的东西在哪里:距离原点 radius 的距离。

cvs.width = 600;
cvs.height = 200;
const ctx = cvs.getContext('2d');
const statue = new Image();
statue.onload = () => animate();
statue.src = 'https://i.ibb.co/3TvCH8n/liberty.png';
const sWidth = sHeight = 80;

// circle values matching your path:
const origin = {x: 300, y: 600 };
const radius = 500;
const start = 0.9272951769;
const end = 2.21429922;

// animation values
let step = 0;
const totalSteps = 120;
const stepSize = (end - start)/totalSteps;

// And our drawing function
function animate() {
  ctx.resetTransform();
  ctx.clearRect(0, 0, cvs.width, cvs.height);

  if (step === totalSteps) step = 0;
  const angle = start + step++ * stepSize;

  // first, change the coordinate system so that we don't need
  // to compute *anything* to draw it in the right place:
  ctx.translate(origin.x, origin.y);
  ctx.rotate(-angle);
  
  // Then we draw the debug line:
  ctx.beginPath();
  ctx.moveTo(0,0);
  ctx.lineTo(radius, 0);
  ctx.stroke();
  
  // And then we draw the image. However, the image is upright
  // when (radius,0) lines up with the x-axis, which means it's
  // actually going to look rotated compared to our line. So:
  // again, we update the coordinate system to do the work for us.
  // we update it so that (radius,0) becomes (0,0), we then rotate
  // it a quarter turn, and then we draw our image at (0,0).
  ctx.translate(radius, 0);
  ctx.rotate(Math.PI/2);

  // of course, (0,0) is the image's top-left corner, so if we
  // want to center it, we can do one more translation:
  ctx.translate(-sWidth/2, -sHeight/2);
  ctx.drawImage(statue, 0, 0, sWidth, sHeight);

  // and move on to the next frame
  requestAnimationFrame(animate);
}
<canvas id="cvs"></canvas>

这样,我们就不会编写 任何 代码来计算“我们需要在哪里绘制东西,以什么方向”,而是我们只使用固定点,并且让 canvas2d 上下文处理所有 translations/rotations.

还有一个鲜为人知的事实:所有浏览器都使具有 id 属性的 HTML 元素可供 JS 使用该 id 作为其变量名。所以在这种情况下我们有一个 <canvas id="cvs">,这意味着在 JS 端有一个名为 cvs 的变量指向我们的 canvas 元素。