3

As requested in the comments, I have made a simpler example that focuses on the question. That being how to correctly calculate the two control points for a bezier curve that follows the pattern pictured below.

enter image description here

The solution I'm looking for needs calculate the variables control2 and control3 so that the curve draw has the same general form for any random pair of start/end points.

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

    let canvasWidth = canvas.width;
    let canvasHeight = canvas.height;
    let zoneWidth = canvas.width*0.2;
    let zoneHeight = canvas.height*0.2;
    let zoneTopLeftX = (canvas.width/2) - (zoneWidth/2);
    let zoneTopLeftY = (canvas.height/2) - (zoneHeight/2);

    let startPoint = {x:zoneTopLeftX+30, y:zoneTopLeftY+(zoneHeight/2)};
    let endPoint = {x:zoneTopLeftX+zoneWidth-30, y:zoneTopLeftY+(zoneHeight/2)};

    // The sign of lineRun and lineRise should tell direction, but 
    // it's not the correct direction because canvas points are 
    // numbered differently from standard graph.  At least it is for y.
    let lineRun = startPoint.x - endPoint.x; // delta along the X-axis
    let lineRise = startPoint.y - endPoint.y; // delta along the Y-axis

    // This math works only in the example case. I need better math,
    // math that will work for any random start/end pair. 
    let cpX = (endPoint.x - lineRun) + (lineRise);
    let cpY = (endPoint.y - lineRise) + (lineRun);
    let control2 = {x:cpX, y:cpY};
    //console.log(cpX, cpY, control2);

    // Again, this math works only in the example case. I need better math,
    // math that will work for any random start/end pair. 
    cpX = startPoint.x;
    cpY = startPoint.y + lineRun + lineRun;
    let control3 = {x:cpX, y:cpY};
    //console.log(cpX, cpY, control3);

    ctx.fillStyle = "#eee"; // lite grey
    ctx.fillRect(0, 0, canvasWidth, canvasHeight);

    ctx.fillStyle = "#aaa"; // grey
    ctx.fillRect(zoneTopLeftX, zoneTopLeftY, zoneWidth, zoneHeight);

    ctx.beginPath();
    // lable everything for demo
    ctx.font = "bold 12px Courier";
    ctx.strokeText("canvas width: "+canvasWidth, 15, 15);
    ctx.strokeText("canvas height: "+canvasHeight, 15, 30);

    ctx.beginPath();
    ctx.arc(startPoint.x, startPoint.y, 5, 0, 2 * Math.PI); // point 1
    ctx.fillStyle = "red";
    ctx.fill();
    ctx.strokeText("1 (" + (startPoint.x + "").substring(0,5) + "," + (startPoint.y + "").substring(0,5) + ")", startPoint.x+5, startPoint.y+15);

    ctx.beginPath();
    ctx.arc(control2.x, control2.y, 5, 0, 2 * Math.PI);     // point 2
    ctx.fillStyle = "#b1e22b"; // yellow-green
    ctx.fill();
    ctx.strokeText("2 (" + (control2.x + "").substring(0,5) + "," + (control2.y + "").substring(0,5) + ")", control2.x+5, control2.y+15);

    ctx.beginPath();
    ctx.arc(control3.x, control3.y, 5, 0, 2 * Math.PI);     // point 3
    ctx.fillStyle = "#8a2be2"; // purple-ish
    ctx.fill();
    ctx.strokeText("3 (" + (control3.x + "").substring(0,5) + "," + (control3.y + "").substring(0,5) + ")", control3.x+5, control3.y+15);

    ctx.beginPath();
    ctx.arc(endPoint.x, endPoint.y, 5, 0, 2 * Math.PI);     // point 4
    ctx.fillStyle = "blue";
    ctx.fill();
    ctx.strokeText("4 (" + (endPoint.x + "").substring(0,5) + "," + (endPoint.y + "").substring(0,5) + ")", endPoint.x+5, endPoint.y+15);

    ctx.moveTo(startPoint.x, startPoint.y);
    ctx.bezierCurveTo(control2.x, control2.y, control3.x, control3.y, endPoint.x, endPoint.y);
    ctx.stroke();
#canvas {
  position: absolute;
}
<canvas id="canvas" width="800" height="800"></canvas>
<div id="results"></div>

Any help is appreciated!

8
  • @trincot the bezier curves generated do not have the pattern expected. They should have a loop and they don't. Commented Feb 28 at 20:42
  • @trincot I did, it's the first image in the post. Commented Feb 28 at 20:51
  • @trincot that is the pattern my program intends to output. While the start and end points are randomly created within a defined range, the general pattern of the curve pictured is what it intends to output. No doubt I am explaining it poorly. Commented Feb 28 at 21:37
  • Are you asking how to create a loop in a svg bezier curve? Commented Feb 28 at 21:59
  • 2
    Please reduce your example to a single thing, having a million curves bouncing around doesn't help clarify your question, and doesn't help anyone answer it. Find a single exemplary case, and show that, with only as much code needed to show that one case. Commented Mar 1 at 1:08

1 Answer 1

2

It looks like you need to translate the two additional control points based on the given two end points. For that you can use basic vector manipulation, so I have defined a few of these as separate functions (like vectorAdd). There is also a vectorTurn in two versions which returns a vector that is perpendicular to the given vector. Depending on the direction of the Y axis you can choose which of the two versions is the one you need.

The snippet below will use a mouse drag as input, and will draw the points and curve as you drag the mouse. The start point is where you start the drag, and the end point is the current mouse position while you drag:

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let mouseDownPos = { x: 0, y: 0};

const getMousePos = (e) => ({ x: e.clientX - e.target.offsetLeft, y: e.clientY - e.target.offsetTop });

canvas.addEventListener("mousedown", function (e) {
    mouseDownPos = getMousePos(e);
    refresh(mouseDownPos, mouseDownPos);
});

canvas.addEventListener("mousemove", function (e) {
    if (e.buttons === 1) refresh(mouseDownPos, getMousePos(e));
});

function setCanvasSize() {
    canvas.setAttribute("width", document.body.clientWidth);
    canvas.setAttribute("height", document.body.clientHeight - 4);
}

window.addEventListener("resize", setCanvasSize);
document.addEventListener("DOMContentLoaded", setCanvasSize);

// Vector helper functions
const sum = (arr, prop) => arr.reduce((sum, obj) => sum + obj[prop], 0);
const vectorAdd = (...vectors) => ({ x: sum(vectors, "x"), y: sum(vectors, "y")});
const vectorSub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
const vectorTurn = v => ({ x: -v.y, y: v.x });
const vectorTurn2 = v => ({ x: v.y, y: -v.x });
const vectorCoords = (vectors) => vectors.flatMap(v => [v.x, v.y]);

function refresh(startPoint, endPoint) {
    const para = vectorSub(endPoint, startPoint);
    const ortho = vectorTurn2(para); // or vectorTurn (if upward Y axis)
    const points = [
        startPoint,
        vectorAdd(endPoint, para, ortho),
        vectorAdd(startPoint, ortho, ortho),
        endPoint
    ];
    draw(points);
}

function draw(points) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    for (const [i, color] of ["red", "#b1e22b", "#8a2be2", "blue"].entries()) {
        plotPoint(points[i], color, i + 1);
    }
    plotBezier(points);
}

function plotPoint({x, y}, color, label) {
    ctx.beginPath();
    ctx.arc(x, y, 5, 0, 2 * Math.PI);
    ctx.fillStyle = color;
    ctx.fill();
    ctx.strokeText(`${label} (${x.toFixed()},${y.toFixed()})`, x+5, y+15);
}

function plotBezier(points) {
    const [x, y, ...rest] = vectorCoords(points);
    ctx.moveTo(x, y);
    ctx.bezierCurveTo(...rest);
    ctx.stroke();
}
canvas { background: #eee }
body { height: 100vh; margin: 0; }
div { position: absolute; left: 0; top: 0; };
<canvas id="canvas" width="600" height="600"></canvas>
<div>Mouse down to set start point and drag to update end point:</div>

Alternative calculation

As a transformation can be described by a transformation matrix, we can do the same as the above, but with matrix-vector multiplication, where the vector has 4 scalars (the coordinates of the start point and the coordinates of the end point), and the product also has 4 scalars: for the coordinates of the second control point and the third control point.

That matrix and product is like this (for an Y-axis that points downward):

( 𝑥₂ )   ( -1 -1  2  1 )   ( 𝑥₁ )
( 𝑦₂ )   (  1 -1 -1  2 )   ( 𝑦₁ )
( 𝑥₃ ) = (  1 -2  0  2 ) × ( 𝑥₄ )
( 𝑦₃ )   (  2  1 -2  0 )   ( 𝑦₄ )

In this implementation points have their coordinates in an array instead of an x/y object:

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let mouseDownPos = [0, 0];

const getMousePos = (e) => [e.clientX - e.target.offsetLeft, e.clientY - e.target.offsetTop];

canvas.addEventListener("mousedown", function (e) {
    mouseDownPos = getMousePos(e);
    refresh(mouseDownPos, mouseDownPos);
});

canvas.addEventListener("mousemove", function (e) {
    if (e.buttons === 1) refresh(mouseDownPos, getMousePos(e));
});

function setCanvasSize() {
    canvas.setAttribute("width", document.body.clientWidth);
    canvas.setAttribute("height", document.body.clientHeight - 4);
}

window.addEventListener("resize", setCanvasSize);
document.addEventListener("DOMContentLoaded", setCanvasSize);

// Matrix helper function
function multiply(matrix, vector) {
    return matrix.map(row =>
        vector.reduce((sum, scalar, k) => sum + row[k] * scalar, 0)
    );
}

const MATRIX = [        // when Y-axis is up:
    [-1, -1,  2,  1],   //  [-1,  1,  2, -1], 
    [ 1, -1, -1,  2],   //  [-1, -1,  1,  2],
    [ 1, -2,  0,  2],   //  [ 1,  2,  0, -2],
    [ 2,  1, -2,  0],   //  [-2,  1,  2,  0],
];

function refresh(startPoint, endPoint) {
    const product = multiply(MATRIX, [...startPoint, ...endPoint]);
    const points = [
        startPoint,
        product.splice(0, 2),
        product,
        endPoint
    ];
    draw(points);
}

function draw(points) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    for (const [i, color] of ["red", "#b1e22b", "#8a2be2", "blue"].entries()) {
        plotPoint(points[i], color, i + 1);
    }
    plotBezier(points);
}

function plotPoint([x, y], color, label) {
    ctx.beginPath();
    ctx.arc(x, y, 5, 0, 2 * Math.PI);
    ctx.fillStyle = color;
    ctx.fill();
    ctx.strokeText(`${label} (${x.toFixed()},${y.toFixed()})`, x+5, y+15);
}

function plotBezier(points) {
    ctx.moveTo(...points[0]);
    ctx.bezierCurveTo(...points.slice(1).flat());
    ctx.stroke();
}
canvas { background: #eee }
body { height: 100vh; margin: 0; }
div { position: absolute; left: 0; top: 0; };
<canvas id="canvas" width="600" height="600"></canvas>
<div>Mouse down to set start point and drag to update end point:</div>

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.