Search code examples
javascripthtml5-canvascurvebeziercubic-bezier

finding a point and its angle on a cubic Bezier curve in JavaScript


I need to find a point and its angle on a cubic Bezier curve that can be dynamically changed using JavaScript. I asked ChatGPT about this, to which it generated the following code, but the angle is not calculated correctly, where am I or is ChatGPT wrong?

      // Initialize with some initial control points
      let points = [
        { x: 50, y: 100 }, // Start point
        { x: 150, y: 50 }, // First control point
        { x: 250, y: 150 }, // Second control point
        { x: 350, y: 100 } // End point
      ];

      function deCasteljau(points, t) {
        if (points.length === 1) {
          return points[0];
        }

        const newPoints = [];
        for (let i = 0; i < points.length - 1; i++) {
          const x = (1 - t) * points[i].x + t * points[i + 1].x;
          const y = (1 - t) * points[i].y + t * points[i + 1].y;
          newPoints.push({ x, y });
        }

        return deCasteljau(newPoints, t);
      }

      function cubicBezierDerivative(points, t) {
        const derivativePoints = [];
        const n = points.length - 1;
        for (let i = 0; i < n; i++) {
            const dx = n * (points[i + 1].x - points[i].x);
            const dy = n * (points[i + 1].y - points[i].y);
            derivativePoints.push({ x: dx, y: dy });
        }
        return derivativePoints;
      }


      function bezierAngle(points, t) {
        const dPoints = cubicBezierDerivative(points, t);
        const point = deCasteljau(points, t);
        const dx = dPoints[0].x;
        const dy = dPoints[0].y;
        const radian = Math.atan2(dy, dx);
        //const angle = radian*180/Math.PI;
        return radian;
      }
      const point = deCasteljau(points, 0.9);
          
      const angle = bezierAngle(points, 0.9);


live demo:

const canvas = document.getElementById('splineCanvas');
const ctx = canvas.getContext('2d');

let points = []; // Array to hold control points
let selectedPointIndex = -1; // Index of the currently selected control point

// Event listener for mouse down to select control point
canvas.addEventListener('mousedown', function(event) {
    const rect = canvas.getBoundingClientRect();
    const mouseX = event.clientX - rect.left;
    const mouseY = event.clientY - rect.top;

    // Check if mouse is over any control point
    for (let i = 0; i < points.length; i++) {
        const dx = points[i].x - mouseX;
        const dy = points[i].y - mouseY;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < 6) { // 6 is the radius for selecting control point
            selectedPointIndex = i;
            canvas.addEventListener('mousemove', onMouseMove);
            canvas.addEventListener('mouseup', onMouseUp);
            break;
        }
    }
});

// Event listener for mouse move to update control point position
function onMouseMove(event) {
    const rect = canvas.getBoundingClientRect();
    const mouseX = event.clientX - rect.left;
    const mouseY = event.clientY - rect.top;

    points[selectedPointIndex].x = mouseX;
    points[selectedPointIndex].y = mouseY;
    drawSpline();
}

// Event listener for mouse up to stop updating control point position
function onMouseUp() {
    canvas.removeEventListener('mousemove', onMouseMove);
    canvas.removeEventListener('mouseup', onMouseUp);
    selectedPointIndex = -1;
}

let testAngle = 65;

// Draw spline function
function drawSpline() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.beginPath();
    ctx.moveTo(points[0].x, points[0].y);

    for (let i = 1; i < points.length - 2; i+=3) {
        ctx.bezierCurveTo(
          points[i].x, 
          points[i].y,
          points[i+1].x,
          points[i+1].y,
          points[i+2].x,
          points[i+2].y,
        );
    }

    ctx.stroke();

    // Draw control points
    for (const point of points) {
        ctx.beginPath();
        ctx.arc(point.x, point.y, 6, 0, Math.PI * 2);
        ctx.fillStyle = "#ff0000";
        ctx.fill();
        ctx.closePath();
    }

    const point = deCasteljau(points, 0.9);
    //console.log('point = ', point);
    const angle = bezierAngle(points, 0.9);
    ctx.save();
    ctx.translate(point.x, point.y);
    ctx.rotate(angle);
    ctx.translate(-point.x, -point.y);

    ctx.fillStyle = "green";
    ctx.fillRect(point.x-5, point.y-5, 10, 10);
    ctx.restore();
}

// Initialize with some initial control points
points = [
  { x: 50, y: 100 }, // Start point
  { x: 150, y: 50 }, // First control point
  { x: 250, y: 150 }, // Second control point
  { x: 350, y: 100 } // End point
];

function deCasteljau(points, t) {
  if (points.length === 1) {
    return points[0];
  }

  const newPoints = [];
  for (let i = 0; i < points.length - 1; i++) {
    const x = (1 - t) * points[i].x + t * points[i + 1].x;
    const y = (1 - t) * points[i].y + t * points[i + 1].y;
    newPoints.push({ x, y });
  }

  return deCasteljau(newPoints, t);
}

function cubicBezierDerivative(points, t) {
  const derivativePoints = [];
  const n = points.length - 1;
  for (let i = 0; i < n; i++) {
      const dx = n * (points[i + 1].x - points[i].x);
      const dy = n * (points[i + 1].y - points[i].y);
      derivativePoints.push({ x: dx, y: dy });
  }
  return derivativePoints;
}


function bezierAngle(points, t) {
  const dPoints = cubicBezierDerivative(points, t);
  const point = deCasteljau(points, t);
  const dx = dPoints[0].x;
  const dy = dPoints[0].y;
  const radian = Math.atan2(dy, dx);
  //const angle = radian*180/Math.PI;
  return radian;
}

drawSpline();
<canvas id="splineCanvas" width="600" height="300"></canvas>


Solution

  • Just from a math perspective I question what you mean by "finding a point and its angle" points in 2D space, like you represented in the points array, are just {x, y} there is no angle ...


    What we can do is calculate the angle between two points, and looks like that is what the functions cubicBezierDerivative & bezierAngle attempted to do, I'm assuming that is what you need/ask, I would base my code below on that, also I'm going to assume that the point returned by function deCasteljau is correct, I'm not going to spend any research time into how is that doing what is doing.

    So we can modify the function bezierAngle to return the angle between two given points, the current point and and what I call "next" point:

        const point = deCasteljau(points, x);
        const next = deCasteljau(points, x + 0.01);
        const angle = bezierAngle([point, next]);
    

    with the angle between those two the square we draw is "facing" the right way.

    In the code below you will see a new function drawRect that is where we draw the squares, and since it is now a function we can have multiple square with different colors

    const canvas = document.getElementById('splineCanvas');
    const ctx = canvas.getContext('2d');
    
    let points = [
      { x: 50,  y: 100 }, // Start point
      { x: 170, y: 20  }, // First control point
      { x: 240, y: 170 }, // Second control point
      { x: 350, y: 10  }  // End point
    ]
    let selectedPointIndex = -1;
    
    // Event listener for mouse down to select control point
    canvas.addEventListener('mousedown', function(event) {
        const rect = canvas.getBoundingClientRect();
        const mouseX = event.clientX - rect.left;
        const mouseY = event.clientY - rect.top;
    
        // Check if mouse is over any control point
        for (let i = 0; i < points.length; i++) {
            const dx = points[i].x - mouseX;
            const dy = points[i].y - mouseY;
            const dist = Math.sqrt(dx * dx + dy * dy);
            if (dist < 6) { // 6 is the radius for selecting control point
                selectedPointIndex = i;
                canvas.addEventListener('mousemove', onMouseMove);
                canvas.addEventListener('mouseup', onMouseUp);
                break;
            }
        }
    });
    
    // Event listener for mouse move to update control point position
    function onMouseMove(event) {
        const rect = canvas.getBoundingClientRect();
        const mouseX = event.clientX - rect.left;
        const mouseY = event.clientY - rect.top;
    
        points[selectedPointIndex].x = mouseX;
        points[selectedPointIndex].y = mouseY;
        drawSpline();
    }
    
    // Event listener for mouse up to stop updating control point position
    function onMouseUp() {
        canvas.removeEventListener('mousemove', onMouseMove);
        canvas.removeEventListener('mouseup', onMouseUp);
        selectedPointIndex = -1;
    }
    
    let testAngle = 65;
    
    // Draw spline function
    function drawSpline() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.beginPath();
        ctx.moveTo(points[0].x, points[0].y);
    
        ctx.bezierCurveTo(
          points[1].x, points[1].y,
          points[2].x, points[2].y,
          points[3].x, points[3].y,
        );
        ctx.stroke();
    
        // Draw control points
        for (const point of points) {
            ctx.beginPath();
            ctx.arc(point.x, point.y, 6, 0, Math.PI * 2);
            ctx.fillStyle = "#ff0000";
            ctx.fill();
            ctx.closePath();
        }
       drawRect(0.2, "cyan")
       drawRect(0.5, "blue")
       drawRect(0.8, "green")
    }
    
    function drawRect(x, color) {
        const point = deCasteljau(points, x);
        const next = deCasteljau(points, x + 0.01);
        const angle = bezierAngle([point, next]);
        ctx.save();
        ctx.globalAlpha = 0.6
        ctx.translate(point.x, point.y);
        ctx.rotate(angle);
        ctx.translate(-point.x, -point.y);
    
        ctx.fillStyle = color;
        ctx.fillRect(point.x-15, point.y-15, 30, 30);
        //ctx.fillStyle = "black";
        //ctx.fillText(angle, point.x, point.y);
        ctx.restore();
    }
    
    function deCasteljau(points, t) {
      if (points.length === 1) {
        return points[0];
      }
    
      const newPoints = [];
      for (let i = 0; i < points.length - 1; i++) {
        const x = (1 - t) * points[i].x + t * points[i + 1].x;
        const y = (1 - t) * points[i].y + t * points[i + 1].y;
        newPoints.push({ x, y });
      }
    
      return deCasteljau(newPoints, t);
    }
    
    function bezierAngle(points) {
      const dx = points[1].x - points[0].x;
      const dy = points[1].y - points[0].y;
      const radian = Math.atan2(dy, dx);
      return radian;
    }
    
    drawSpline();
    <canvas id="splineCanvas" width="400" height="180"></canvas>