Back

Draggable Bezier Curve In Vanilla Javascript

02 Feb 2024

I have always loved making things with HTML Canvas. You can do a lot of cool things with it, like an animation of the rotation of planets around the sun. Something that has always been challenging for me is making objects in the DOM or on a Canvas draggable. So, I decided to try to make a Bezier curve with draggable controls.

Base html code

Let’s start by setting up a canvas. Create an html file with the following content:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Bezier Curve</title>
    <style>
      body {
        margin: 0;
        background-color: #1a1a1a;
      }
    </style>
  </head>
  <body>
    <script src="bezier-curve.js"></script>
  </body>
</html>

Set up the canvas

Next, create a file called bezier-curve.js. Add the following code to it:

const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;
const DPR = window.devicePixelRatio || 1;

const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");

canvas.width = WIDTH * DPR;
canvas.height = HEIGHT * DPR;
context.scale(DPR, DPR);
canvas.style.width = `${WIDTH}px`;
canvas.style.height = `${HEIGHT}px`;

context.lineWidth = 3;
context.lineJoin = "round";
context.lineCap = "round";

const root = document.querySelector("body");
root.append(canvas);

We just created a new canvas, scaled its size according to our device’s pixel ratio (DPR) so our drawings will be sharp and not pixelated, set up our drawing style and added the canvas to the DOM. You should now see blank page with a dark background, but no curve yet. Let’s change that.

Draw the curve

const start = { x: 50, y: HEIGHT / 2 };
const cp1 = { x: WIDTH / 2, y: HEIGHT - 50 };
const cp2 = { x: WIDTH / 2, y: 50 };
const end = { x: WIDTH - 50, y: HEIGHT / 2 };

window.onload = draw;

function draw() {
  context.fillStyle = "#1a1a1a";
  context.fillRect(0, 0, WIDTH, HEIGHT);

  context.strokeStyle = "#e1e1e1";
  context.beginPath();
  context.moveTo(start.x, start.y);
  context.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y);
  context.stroke();
}

You should now see something like this:

image of a bezier curve with a sine wave like form.

This is already starting to look pretty cool! The curve is visible and it is being drawn based on some coordinates. First, we move to the starting point. Then we draw the curve based on three points: control point 1, control point 2 and the end point.Control point 1 controls the curve at the start of the line, and control point 2 controls the curve at the end of the line. Don’t worry too much about that right now, you’ll see it work in a bit. You might already be able to figure out how to make the curve move based on this code. For example, what would happen if we changed these coordinates? What if we used the mouse to drag the control points or start and end point around? Let’s make that happen.

Keep track of the mouse

// earlier code

const mouse = {
  x: 0,
  y: 0,
};
const circleSize = 100;
let = true;
let mouseDown = false;

document.addEventListener("mousemove", (e) => {
  mouse.x = e.clientX;
  mouse.y = e.clientY;
});

document.addEventListener("mousedown", (e) => {
  mouseDown = true;
});

document.addEventListener("mouseup", (e) => {
  mouseDown = false;
});

We added a variable to keep track of the mouse coordinates to compare them to the coordinates of the line points that we want to be able to drag. Also, we added some event handlers to know when the mouse is being pressed. Let’s continue.

Draw control points and lines

function draw() {
  // earlier code

  drawCircle(start);
  drawLine(start, cp1);
  drawCircle(cp1);
  drawCircle(end);
  drawLine(end, cp2);
  drawCircle(cp2);

  if (mouseDown) {
    if (collision(mouse, start)) {
      start.x = mouse.x;
      start.y = mouse.y;
    } else if (collision(mouse, cp1)) {
      cp1.x = mouse.x;
      cp1.y = mouse.y;
    } else if (collision(mouse, cp2)) {
      cp2.x = mouse.x;
      cp2.y = mouse.y;
    } else if (collision(mouse, end)) {
      end.x = mouse.x;
      end.y = mouse.y;
    }
  }
}

Now we can clearly see the start, control and end points, and how they create the curved line. The coolest part is, we can also drag them to move the curve around and turn it into any shape we want! Let’s finish the code to see the magic happen.

function drawCircle(pos) {
  context.fillStyle = "#e1e1e1";
  context.beginPath();
  context.arc(pos.x, pos.y, circleSize / 2, 0, 2 * Math.PI);
  context.fill();
}

function drawLine(from, to) {
  context.strokeStyle = "#e1e1e1";
  context.beginPath();
  context.moveTo(from.x, from.y);
  context.lineTo(to.x, to.y);
  context.stroke();
}

function collision(a, b) {
  const result =
    a.x >= b.x - circleSize &&
    a.x <= b.x + circleSize &&
    a.y >= b.y - circleSize &&
    a.y <= b.y + circleSize;
  return result;
}

a bezier curve drawn on an html canvas with the control points shown.

Look at that! And you can move it around!

I hope you enjoyed reading this post as much as I enjoyed making it! The full code can be found here. Thanks for reading!


Back