Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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>
like image 751
Proton Avatar asked Nov 16 '25 07:11

Proton


1 Answers

Precise tangent at unit position on curve

Example code below gives exact tangent of curve at unit position on bezier.

The example normalizes the tangent to be more useful for various rendering tasks.

Solutions includes quadratic and cubic curves.

To convert the tangent to an angle just use Math.atan2(tangent.y, tangent.x); though there is no need as a matrix can be constructed directly from the tangent (no need to mess with rotations, translations, etc...) E.G. ctx.setTransform(tangent.x, tangent.y, -tangent.y, tangent.x, pos.x, pos.y); where pos is the position on the curve.

const ctx = canvas.getContext("2d");
const TAU = Math.PI * 2;
const Vec2 = (x = 0, y = 0) => ({x, y});
const RotateVec90 = v => Vec2(-v.y, v.x);
const QBez = (p1, cp1, p2) => ({p1, cp1, p2});           // Quadratic bez 3 Vec2 p1, p2 start and end, cp1 control point
const CBez = (p1, cp1, cp2, p2) => ({p1, cp1, cp2, p2}); // Cubic bez 4 Vec2 p1, p2 start and end, cp1, cp2 control points

const Beziers = {
    asArray(bez) {
        return bez.cp2 === undefined ?
            [bez.p1, bez.cp1, bez.p2] :
            [bez.p1, bez.cp1, bez.cp2, bez.p2];
    },
    tangentAt(bez, pos, limit = true, tangent = Vec2()) {  
        if (limit) { pos = Math.min(1, Math.max(0, pos)); }
        if (bez.cp2 === undefined) {   /* is quadratic */
            const a = (1 - pos) * 2;
            const b = pos * 2;
            tangent.x = a * (bez.cp1.x - bez.p1.x) + b * (bez.p2.x - bez.cp1.x);
            tangent.y = a * (bez.cp1.y - bez.p1.y) + b * (bez.p2.y - bez.cp1.y);
        } else {                          /* is cubic */
            const a = (1 - pos)
            const b = 6 * a * pos;        
            const c = a * 3 * a;          
            const d = 3 * pos * pos;      
            tangent.x = -bez.p1.x * c + bez.cp1.x * (c - b) + bez.cp2.x * (b - d) + bez.p2.x * d;
            tangent.y = -bez.p1.y * c + bez.cp1.y * (c - b) + bez.cp2.y * (b - d) + bez.p2.y * d;
        }
        const u = 1.0 / (tangent.x * tangent.x + tangent.y * tangent.y) ** 0.5;
        tangent.x *= u;
        tangent.y *= u;
        return tangent;
    },
    pointAt(bez, pos, limit = true, point = Vec2()) { 
        if (limit) {
            if (pos <= 0) {
                point.x = bez.p1.x;
                point.y = bez.p1.y;
                return point;
            }
            if (pos >= 1) {
                point.x = bez.p2.x;
                point.y = bez.p2.y;
                return point;
            }
        }
        const v1 = Vec2(bez.p1.x, bez.p1.y);
        const v2 = Vec2(bez.cp1.x, bez.cp1.y);
        const c = pos;
        v1.x += (v2.x - v1.x) * c;
        v1.y += (v2.y - v1.y) * c; 
        if (bez.cp2 === undefined) {  /* is quadratic */
            v2.x += (bez.p2.x - v2.x) * c;
            v2.y += (bez.p2.y - v2.y) * c;
            point.x = v1.x + (v2.x - v1.x) * c;
            point.y = v1.y + (v2.y - v1.y) * c;
            return point;
        }
        const v3 = Vec2(bez.cp2.x, bez.cp2.y);
        v2.x += (v3.x - v2.x) * c;
        v2.y += (v3.y - v2.y) * c;
        v3.x += (bez.p2.x - v3.x) * c;
        v3.y += (bez.p2.y - v3.y) * c;
        v1.x += (v2.x - v1.x) * c;
        v1.y += (v2.y - v1.y) * c;
        v2.x += (v3.x - v2.x) * c;
        v2.y += (v3.y - v2.y) * c;
        point.x = v1.x + (v2.x - v1.x) * c;
        point.y = v1.y + (v2.y - v1.y) * c;
        return point;
    },    
};
const Render = {
    draw(bez, width = 2, color = "#000") {
        ctx.lineWidth = width;
        ctx.strokeStyle = color;
        ctx.beginPath();
        ctx.lineTo(bez.p1.x, bez.p1.y);
        if (bez.cp2 === undefined) {   /* is quadratic */
            ctx.quadraticCurveTo(bez.cp1.x, bez.cp1.y, bez.p2.x, bez.p2.y);
        } else {            
            ctx.bezierCurveTo(bez.cp1.x, bez.cp1.y, bez.cp2.x, bez.cp2.y, bez.p2.x, bez.p2.y)
        }
        ctx.stroke();    
    },
    drawPoints(radius, color, ...points) {
        ctx.fillStyle = color;
        ctx.beginPath();
        for (const p of points) {
             ctx.moveTo(p.x + radius, p.y);
             ctx.arc(p.x, p.y, radius, 0, TAU);
        }
        ctx.fill();
    },
    drawVector(pos, unitVec, len, width = 2, color = "#000") {
        ctx.lineWidth = width;
        ctx.strokeStyle = color;
        ctx.beginPath();
        ctx.lineTo(pos.x, pos.y);
        ctx.lineTo(pos.x + unitVec.x * len, pos.y + unitVec.y * len);
        ctx.stroke();
    }
};

const curve = CBez(Vec2(50, 100), Vec2(170, 10), Vec2(260, 270), Vec2(350, 10));
Render.draw(curve);
Render.drawPoints(4, "#A00", ...Beziers.asArray(curve));
var pos = 0.0;
while (pos <= 1.01) {
    Render.drawVector(Beziers.pointAt(curve, pos, true), RotateVec90(Beziers.tangentAt(curve, pos, true)), 20, 1, "#0A0");
    pos += 0.02;
}
<canvas id="canvas" width="400" height="400"></canvas>
like image 104
Blindman67 Avatar answered Nov 17 '25 21:11

Blindman67