Search code examples
htmlanimationcanvasbezier

Canvas, make an image follow a Bezier curve


I want to create an animation with canvas of an image following a semi circle. At first I've tried with a canvas arc but I've found it simpler with a bezier curve.

enter image description here

But now I am facing a problem because since it's not on a circle I can't find a way to make it rotate according to it's position, like a watch pointer. This is my code so far.

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>

What is the proper way to do so?


Solution

  • You can use the same function to get another point slightly before the current one on the curve, then use Math.atan2 to get the angle between the two points.

    Then, you'll need to use ctx.translate() and ctx.rotate() to mutate the transformation matrix instead of setting the position in the .drawImage() call. (The .setTransform() call at the start of the animation method resets the matrix for each frame.)

    I also added an "onion skin" effect here, so the motion is better seen.

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