javascriptcubic-bezier

What's wrong on this cubic Bezier curve calculation?


Here's my attempt to draw a cubic bezier curve and get the value at 170x (considering P1 and P2):

<canvas id="myCanvas" width="800" height="200" style="border: 1px solid black;"></canvas>
<div id="result" style="margin-top: 20px;">N/A</div>

<script>
    var canvas = document.getElementById('myCanvas');
    var ctx = canvas.getContext('2d');
    var resultDiv = document.getElementById('result');

    // Function to flip the y-coordinate
    function flipY(y) {
        return canvas.height - y;
    }

    // Class to represent a single Bézier curve block
    class BezierBlock {
        constructor(P0, P1, P2, P3) {
            this.P0 = { x: P0.x, y: flipY(P0.y) }; // Start point
            this.P1 = { x: P1.x, y: flipY(P1.y) }; // First control point
            this.P2 = { x: P2.x, y: flipY(P2.y) }; // Second control point
            this.P3 = { x: P3.x, y: flipY(P3.y) }; // End point

            this.minX = Math.min(this.P0.x, this.P3.x);
            this.maxX = Math.max(this.P0.x, this.P3.x);
        }

        draw() {
            // Draw the cubic Bézier curve
            ctx.setLineDash([]);
            ctx.beginPath();
            ctx.moveTo(this.P0.x, this.P0.y);
            ctx.bezierCurveTo(this.P1.x, this.P1.y, this.P2.x, this.P2.y, this.P3.x, this.P3.y);
            ctx.strokeStyle = 'black';
            ctx.stroke();
            
            // Draw the vertical cursor line at the current slider position
            ctx.setLineDash([5, 5]);
            ctx.beginPath();
            ctx.moveTo(currentX, 0);
            ctx.lineTo(currentX, canvas.height);
            ctx.strokeStyle = 'blue';
            ctx.stroke();
            ctx.setLineDash([]);

            // Draw the control points
            ctx.fillStyle = 'red';
            ctx.fillRect(this.P0.x - 3, this.P0.y - 3, 6, 6);  // P0
            ctx.fillStyle = 'blue';
            ctx.fillRect(this.P1.x - 3, this.P1.y - 3, 6, 6);  // P1
            ctx.fillStyle = 'blue';
            ctx.fillRect(this.P2.x - 3, this.P2.y - 3, 6, 6);  // P2
            ctx.fillStyle = 'red';
            ctx.fillRect(this.P3.x - 3, this.P3.y - 3, 6, 6);  // P3
        }

        // Method to calculate the y value on the curve at a given x position
        getYByX(posX) {
            let t = (posX - this.P0.x) / (this.P3.x - this.P0.x);
            if (t < 0 || t > 1) return null;  // posX is out of bounds for this curve

            let y =
                Math.pow(1 - t, 3) * this.P0.y +
                3 * Math.pow(1 - t, 2) * t * this.P1.y +
                3 * (1 - t) * Math.pow(t, 2) * this.P2.y +
                Math.pow(t, 3) * this.P3.y;

            return flipY(y);
        }
    }

    // Define the points for each block
    const blocks = [
        new BezierBlock({x: 0, y: 0}, {x: 200, y: 0}, {x: 200, y: 0}, {x: 200, y: 180})
    ];

    // Draw all the Bezier blocks
    function drawAll() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        blocks.forEach(block => block.draw());
    }

    // Function to process a given x position and display the corresponding y value
    function process(posX) {
        for (let block of blocks) {
            if (posX >= block.P0.x && posX <= block.P3.x) {
                let posY = block.getYByX(posX);
                if (posY !== null) {
                    resultDiv.innerText = `X=${posX}: Y=${posY.toFixed(2)}`;
                    currentX = posX;
                    return;
                }
            }
        }
    }

    let currentX = 170; // Initialize currentX for the cursor
    drawAll();
    process(currentX); // Process initial position

</script>

Which display as:

enter image description here

But the value from getYByX should be somethings like 20 (as for the plot), not 110. Notice that Y-values are flip (so bottom is 0, top is 200).

Still wrong result.

Where am I wrong on cubic calculation?


Solution

  • You're using X as a linear value not as cubic evolving variable. I took your code and get an X thanks to a linear t (same formula than your Y but with the X axis). I think it's working with my method:

    <canvas id="myCanvas" width="800" height="200" style="border: 1px solid black;"></canvas>
    <div id="result" style="margin-top: 20px;">N/A</div>
    
    <script>
        var canvas = document.getElementById('myCanvas');
        var ctx = canvas.getContext('2d');
        var resultDiv = document.getElementById('result');
    
        // Function to flip the y-coordinate
        function flipY(y) {
            return canvas.height - y;
        }
    
        // Class to represent a single Bézier curve block
        class BezierBlock {
            constructor(P0, P1, P2, P3) {
                this.P0 = { x: P0.x, y: flipY(P0.y) }; // Start point
                this.P1 = { x: P1.x, y: flipY(P1.y) }; // First control point
                this.P2 = { x: P2.x, y: flipY(P2.y) }; // Second control point
                this.P3 = { x: P3.x, y: flipY(P3.y) }; // End point
    
                this.minX = Math.min(this.P0.x, this.P3.x);
                this.maxX = Math.max(this.P0.x, this.P3.x);
            }
    
            draw() {
                // Draw the cubic Bézier curve
                ctx.setLineDash([]);
                ctx.beginPath();
                ctx.moveTo(this.P0.x, this.P0.y);
                ctx.bezierCurveTo(this.P1.x, this.P1.y, this.P2.x, this.P2.y, this.P3.x, this.P3.y);
                ctx.strokeStyle = 'black';
                ctx.stroke();
                
                // Draw the vertical cursor line at the current slider position
                ctx.setLineDash([5, 5]);
                ctx.beginPath();
                ctx.moveTo(t, 0);
                ctx.lineTo(t, canvas.height);
                ctx.strokeStyle = 'blue';
                ctx.stroke();
                ctx.setLineDash([]);
    
                // Draw the control points
                ctx.fillStyle = 'red';
                ctx.fillRect(this.P0.x - 3, this.P0.y - 3, 6, 6);  // P0
                ctx.fillStyle = 'blue';
                ctx.fillRect(this.P1.x - 3, this.P1.y - 3, 6, 6);  // P1
                ctx.fillStyle = 'blue';
                ctx.fillRect(this.P2.x - 3, this.P2.y - 3, 6, 6);  // P2
                ctx.fillStyle = 'red';
                ctx.fillRect(this.P3.x - 3, this.P3.y - 3, 6, 6);  // P3
            }
    
            // Method to calculate the y value on the curve at a given x position
            getYByT(t) {
                let y =
                    Math.pow(1 - t, 3) * this.P0.y +
                    3 * Math.pow(1 - t, 2) * t * this.P1.y +
                    3 * (1 - t) * Math.pow(t, 2) * this.P2.y +
                    Math.pow(t, 3) * this.P3.y;
    
                return flipY(y);
            }
            getXByT(t) {
                let x =
                    Math.pow(1 - t, 3) * this.P0.x +
                    3 * Math.pow(1 - t, 2) * t * this.P1.x +
                    3 * (1 - t) * Math.pow(t, 2) * this.P2.x +
                    Math.pow(t, 3) * this.P3.x;
    
                return x;
            }
        }
    
        // Define the points for each block
        const blocks = [
            new BezierBlock({x: 0, y: 0}, {x: 200, y: 0}, {x: 200, y: 0}, {x: 200, y: 180})
        ];
    
        // Draw all the Bezier blocks
        function drawAll() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            blocks.forEach(block => block.draw());
        }
    
        // Function to process a given x position and display the corresponding y value
        function process(t) {
            for (let block of blocks) {
                if (t >= block.P0.x && t <= block.P3.x) {
                    let posY = block.getYByT(t);
                    let posX = block.getXByT(t);
                    if (posY !== null) {
                        resultDiv.innerText = `X=${posX.toFixed(2)}: Y=${posY.toFixed(2)}`;
                        t = posX;
                        return;
                    }
                }
            }
        }
    
        let t = 0.47 // Average value of t for x ~= 170
        drawAll();
        process(t); // Process initial position
    
    </script>
    
    

    EDIT: I found the value of x with a python script and by resolving the cubic bezier curve equation.

    The original equation:

    Expanded form:

    This rmula is a polynomial equation as:

    with

    scipy module has a function named fsolve that can solve some equation like this one.

    from scipy.optimize import fsolve
    
    x0, x1, x2, x3 = 0, 200, 200, 200  # Your x values
    
    a: int = -x0 + 3 * x1 - 3 * x2 + x3
    b: int = 3 * x0 - 6 * x1 + 3 * x2
    c: int = -3 * x0 + 3 * x1
    d: int = x0
    
    
    def bezier_x(t: float):
        return a * t ** 3 + b * t ** 2 + c * t + d
    
    
    def solve_for_t(expected: float):
        delta = lambda t: bezier_x(t) - expected
        t_initial = 0.5
        t_solution = fsolve(delta, t_initial)
        return t_solution[0]
    
    
    x_target: float = 170.
    t_result = solve_for_t(x_target)
    print(f"The parameter t corresponding to x = {x_target} is approximately {t_result:.4f}")