【问题标题】:Canvas, make an image follow a Bezier curve画布,使图像遵循贝塞尔曲线
【发布时间】:2021-10-30 09:15:18
【问题描述】:

我想创建一个动画,其中的图像画布遵循一个半圆形。起初我尝试使用画布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>

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

【问题讨论】:

  • 你能解释一下为什么贝塞尔曲线更容易,因为作为一个对贝塞尔曲线有点了解的人:圆弧比贝塞尔曲线容易。
  • 这是因为我可以通过一个不涉及三角函数的简单函数获得我想要的形状(一个稍微变平的半圆)和特定时间的图像位置。
  • 以非线性路径运动和微积分而不是三角函数为代价,这是一个任意差异(二次计算更复杂并且比弧数学更昂贵)

标签: html animation canvas bezier


【解决方案1】:

您可以使用相同的函数获取曲线上稍早于当前点的另一点,然后使用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;">
 

【讨论】:

  • 不计算任何东西,只是让画布完成所有繁重的工作,告诉它如何翻译()/旋转()更好:根本不需要数学。
【解决方案2】:

不管怎样,只要使用一个圆圈,它方式更容易,但与其尝试将事物放置在坐标上,不如反过来:将坐标系设置为正确的偏移和角度,这样你就可以在相同的坐标上绘制相同的东西。

我们观察到我们正在处理以 (300,600) 为原点的圆形路径,半径为 500,起始角(弧度)为 0.9272951769,结束角(弧度)为 2.21429922,所以我们可以移动我们的坐标系,使(0,0)是“真正的”(300,600),然后当我们围绕新的(0,0)将坐标系旋转任意角度时,我们需要做的就是在(半径,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);
}
&lt;canvas id="cvs"&gt;&lt;/canvas&gt;

这样,我们不会编写任何代码来计算“我们需要在哪里绘制东西,以什么方向”,而是我们只使用固定点,并让 canvas2d 上下文照顾所有的翻译/旋转。

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

【讨论】:

  • 嗯,IIRC 二次贝塞尔路径不能精确表示圆?
  • 正确,但最初的帖子是从他们想要一个半圆开始,然后使用二次贝塞尔曲线“因为它更简单”,但事实并非如此,而是让事情变得很多 更难 =)
  • 我认为 OP 也有其他原因切换到贝塞尔曲线。但是,是的,用一点三角函数来绘制圆弧会非常简单......
  • 在完全理解后我最终使用了这个解决方案(比贝塞尔解决方案花费了我更多的成本......)。但是由于@AKX 回答了我遇到的问题,我仍然必须将其保留为“已接受的答案”。
  • 您可能希望切换已接受的答案,但两个答案都依赖于相同的概念:更改坐标系并“原位”绘制事物,而不是保持坐标系原位并更改事物的位置被画=)
猜你喜欢
  • 2023-03-27
  • 1970-01-01
  • 2018-09-09
  • 1970-01-01
  • 2023-03-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-11-30
相关资源
最近更新 更多