DEV Community

Pujaa M
Pujaa M

Posted on

Eye tracking dog

The eyes of the dog track the direction of the cursor when moved.

In this tutorial, we'll explore the intricate process of synchronizing a virtual dog's eyes with the movement of your cursor.

1: Create a canvas in HTML

Using the canvas tag, create a canvas and setup the width, height and color of the canvas.

HTML

<canvas></canvas>
Enter fullscreen mode Exit fullscreen mode

CSS

canvas {
    background-color: #A15A22;
}
Enter fullscreen mode Exit fullscreen mode

JS

const canvas = document.querySelector("canvas");
const pen = canvas.getContext("2d");
let radius = 50;
let trackerRadius = 30;

function setupCanvas() {
    canvas.width = 500;
    canvas.height = 500;
}

setupCanvas();
Enter fullscreen mode Exit fullscreen mode

You will achieve something like this:

Canvas Setup

2: Draw eyeball(circle) at the center of the canvas

We can draw a circle on a canvas using the arc() method.

JS

/* ... */
let radius = 125;
/* ... */

function draw() {
    let centerX1 = canvas.width * 0.5;
    let centerY1 = canvas.height * 0.5;

    pen.beginPath();
    pen.arc(centerX1, centerY1, radius, 0, 2 * Math.PI);
    pen.fillStyle = "white";
    pen.fill();
}

draw();
Enter fullscreen mode Exit fullscreen mode

Eyeball

This circle that we have drawn will be the eyeball of the dog later on...

3: Draw iris the moves along with the cursor.

We will be using the arc() method again to draw a dot. Then we shall fetch the cursor position using EventListeners to update the position of the dot.

JS

let trackerRadius = 30;

let cursorPointer = {
    x: canvas.width * 0.5,
    y: canvas.height * 0.5
};

/* ... */

function updateCursorPointer(pointerX, pointerY) {
    let boundary = canvas.getBoundingClientRect();
    cursorPointer.x = pointerX - boundary.left;
    cursorPointer.y = pointerY - boundary.top;
}

document.addEventListener("mousemove", function(event) {
    updateCursorPointer(event.clientX, event.clientY);
});

function draw() {
    pen.clearRect(0, 0, canvas.width, canvas.height);
/* ... */

    pen.beginPath();
    pen.moveTo(cursorPointer.x, cursorPointer.y);
    pen.arc(cursorPointer.x, cursorPointer.y, trackerRadius, 0, 2 * Math.PI);
    pen.fillStyle = "#03c2fc";
    pen.fill();

/* ... */

    requestAnimationFrame(draw);
}
Enter fullscreen mode Exit fullscreen mode

The requestAnimationFrame() method schedules the draw() method for the next frame. The clearRect() method is used so that the canvas is completely cleared before painting the next frame.

Iris

We see that the dot, which will be the iris of the eyeball, moves along with the cursor all over the canvas but not bounded within the eyeball.

3: Bound the iris within the eyeball.

As we can observe from the image that the eyeball is actually a circle. So, in order to bound the iris within the eyeball, we need to check whether the points read from the cursor pointer is actually present within the circle or not.

Let us consider the equation of a circle:

Equation of circle

Using the above equation, we can easily check whether the point lies within the cirlce or not.

JS

function updateCursorPointer(pointerX, pointerY) {
let boundary = canvas.getBoundingClientRect();
    let tempX = pointerX - boundary.left;
    let tempY = pointerY - boundary.top;

    let h = canvas.width * 0.5;
    let k = canvas.height * 0.5;

    let boundaryRadius = radius - trackerRadius;
    let u = tempX - h;
    let v = tempY - k;
    let eq = (u * u) + (v * v);

    if(eq <= (boundaryRadius * boundaryRadius)) {
            cursorPointer.x = tempX;
            cursorPointer.y = tempY;
    }
}
Enter fullscreen mode Exit fullscreen mode

Iris bounded within Eyeball

4: Move iris wherever the cursor is within the window

As of now, the iris moves along to the cursor only when the cursor is inside the eyeball. In order to move the iris no matter where the cursor is on the window, we will be using the method to find the point of intersection between a line and a circle.

Refer this link for detailed explanation and examples on how to find points of intersection between a line and a circle.

So, when substituting the y obtained from the equation of line in the equation of circle, we get the following equation:

Equation after substituting y

In our case, the circle obviously is the eyeball and the line is obtained from two points - the center of eyeball and cursor pointer's position.

We will find the slope m using the two points, find c from the equation of line y = mx + c and r being the radius of the eyeball.

Let us solve the above quadratic equation using

Equation for solving a quadratic equation

JS

function lineEquation(x, slope, constant) {
   return (slope * x) + constant;
}

function discriminant(a, b, c) {
  return (b * b) - (4 * a * c);
}

function isPointBetween(x, y, x1, y1, x2, y2) {
  return (x >= Math.min(x1, x2) && x <= Math.max(x1, x2) &&
          y >= Math.min(y1, y2) && y <= Math.max(y1, y2));
}

function findIntersectionPoint(slope, h, k, point1X, point1Y, constant, circleRadius) {
    var a = 1 + (slope * slope);
      var b = -2 * (h + slope * (k - constant));
      var c = (h * h) + (k * k) - (circleRadius * circleRadius) + (constant * (constant - (2 * k)));

    var disc = discriminant(a, b, c);

    if (disc >= 0) {
      const x1 = (-b + Math.sqrt(disc)) / (2 * a);
      const x2 = (-b - Math.sqrt(disc)) / (2 * a);

      const y1 = lineEquation(x1, slope, constant);
      const y2 = lineEquation(x2, slope, constant);

            if(isPointBetween(x1, y1, point1X, point1Y, h, k)) {
                    cursorPointer.x = x1;
                    cursorPointer.y = y1;
            }
            else if(isPointBetween(x2, y2, point1X, point1Y, h, k)) {
                    cursorPointer.x = x2;
                    cursorPointer.y = y2;
            }
        }
}

function findCursorPointerPosition(h, k, tempX, tempY) {
    let boundaryRadius = radius - trackerRadius;
    let u = tempX - h;
    let v = tempY - k;
    let eq = (u * u) + (v * v);

    if(eq <= (boundaryRadius * boundaryRadius)) {
            cursorPointer.x = tempX
            cursorPointer.y = tempY;
    }
    else {
        let x1 = h;
        let y1 = k;
        let x2 = tempX;
        let y2 = tempY;

        let slope = (y2 - y1) / (x2 - x1);

        let constant = y2 - (slope * x2);

        findIntersectionPoint(slope, x1, y1, x2, y2, constant, boundaryRadius);
    }
}
Enter fullscreen mode Exit fullscreen mode

Since the result gives out two points, we need to find the point that is between the center of the eyeball and the cursor pointer.

Iris movement to cursor

5: Create another eye

Since our dog needs two eyes to see, we will be repeating the same process for the other eye too. But this time, we will be relocating the center of the circles for the left and the right eyes.

JS

/* ... */

let radius = 50;

/* ... */

function setupCanvas() {
    canvas.width = 250;
    canvas.height = 150;
}

let cursorPointerLeft = {
    x: canvas.width * 0.25,
    y: canvas.height * 0.5
}

let cursorPointerRight = {
    x: canvas.width * 0.75,
    y: canvas.height * 0.5
}

/* ... */

function updateCursorPointer(pointerX, pointerY) {
    let boundary = canvas.getBoundingClientRect();
    let tempX = pointerX - boundary.left;
    let tempY = pointerY - boundary.top;

    let h1 = canvas.width * 0.25;
    let k1 = canvas.height * 0.5;
    let h2 = canvas.width * 0.75;
    let k2 = canvas.height * 0.5;

    findCursorPointerPosition(h1, k1, tempX, tempY, cursorPointerLeft);
    findCursorPointerPosition(h2, k2, tempX, tempY, cursorPointerRight); // Add an additional parameter, cursorPointer, in findCursorPointerPosition() and findIntersectionPoint()
}

/* ... */

function draw() {
    pen.clearRect(0, 0, canvas.width, canvas.height);
    let centerX1 = canvas.width * 0.25;
    let centerY1 = canvas.height * 0.5;
    let centerX2 = canvas.width * 0.75;
    let centerY2 = canvas.height * 0.5;

    pen.beginPath();
    pen.arc(centerX1, centerY1, radius, 0, 2 * Math.PI);
    pen.fillStyle = "white";
    pen.fill();

    pen.beginPath();
    pen.arc(centerX2, centerY2, radius, 0, 2 * Math.PI);
    pen.fillStyle = "white";
    pen.fill();

    pen.beginPath();
    pen.moveTo(cursorPointerLeft.x, cursorPointerLeft.y);
    pen.arc(cursorPointerLeft.x, cursorPointerLeft.y, trackerRadius, 0, 2 * Math.PI);
    pen.fillStyle = "#03c2fc";
    pen.fill();

    pen.beginPath();
    pen.moveTo(cursorPointerLeft.x, cursorPointerLeft.y);
    pen.arc(cursorPointerLeft.x, cursorPointerLeft.y, trackerRadius * 0.75, 0, 2 * Math.PI);
    pen.fillStyle = "black";
    pen.fill();

    pen.beginPath();
    pen.moveTo(cursorPointerRight.x, cursorPointerRight.y);
    pen.arc(cursorPointerRight.x, cursorPointerRight.y, trackerRadius, 0, 2 * Math.PI);
    pen.fillStyle = "#03c2fc";
    pen.fill();

    pen.beginPath();
    pen.moveTo(cursorPointerRight.x, cursorPointerRight.y);
    pen.arc(cursorPointerRight.x, cursorPointerRight.y, trackerRadius * 0.75, 0, 2 * Math.PI);
    pen.fillStyle = "black";
    pen.fill();
    requestAnimationFrame(draw);
}
Enter fullscreen mode Exit fullscreen mode

Two Eyeballs

We have almost achieved what we require but the eyes look like the dog has got its fifth shot of Benedryl in a day >_< !

6: Correcting the eye movement

So instead of having two different centers for the eye we will gonna be having one, common for both.

Imaginary circle

Let us imagine there is a circle with the same radius as the eyeballs between the two eyeballs. We will be moving the irises according to that imaginary circle. Once we calculated the position according to that circle we will adjust the x-axes of the irises according to the eyeballs.

JS

/* ... */

function findIntersectionPoint(slope, h, k, point1X, point1Y, constant, circleRadius) {
    var a = 1 + (slope * slope);
      var b = -2 * (h + slope * (k - constant));
      var c = (h * h) + (k * k) - (circleRadius * circleRadius) + (constant * (constant - (2 * k)));

    var disc = discriminant(a, b, c);

    if (disc >= 0) {
      const x1 = (-b + Math.sqrt(disc)) / (2 * a);
      const x2 = (-b - Math.sqrt(disc)) / (2 * a);

      const y1 = lineEquation(x1, slope, constant);
      const y2 = lineEquation(x2, slope, constant);

            if(isPointBetween(x1, y1, point1X, point1Y, h, k)) {
                    cursorPointerLeft.x = x1 - canvas.width * 0.25;
                    cursorPointerLeft.y = y1;
                    cursorPointerRight.x = x1 + canvas.width * 0.25;
                    cursorPointerRight.y = y1;
            }
            else if(isPointBetween(x2, y2, point1X, point1Y, h, k)) {
                    cursorPointerLeft.x = x2 - canvas.width * 0.25;
                    cursorPointerLeft.y = y2;
                    cursorPointerRight.x = x2 + canvas.width * 0.25;
                    cursorPointerRight.y = y2;
            }
        }
}

function updateCursorPointer(pointerX, pointerY) {
    let boundary = canvas.getBoundingClientRect();
    let tempX = pointerX - boundary.left;
    let tempY = pointerY - boundary.top;

    let h = canvas.width * 0.5;
    let k = canvas.height * 0.5;

    findCursorPointerPosition(h, k, tempX, tempY);
}

function findCursorPointerPosition(h, k, tempX, tempY) {
    let boundaryRadius = radius - trackerRadius;
    let u = tempX - h;
    let v = tempY - k;
    let eq = (u * u) + (v * v);

    if(eq <= (boundaryRadius * boundaryRadius)) {
            cursorPointerLeft.x = tempX - canvas.width * 0.25;
            cursorPointerLeft.y = tempY;
            cursorPointerRight.x = tempX + canvas.width * 0.25;
            cursorPointerRight.y = tempY;
    }
    else {
        let x1 = h;
        let y1 = k;
        let x2 = tempX;
        let y2 = tempY;

        let slope = (y2 - y1) / (x2 - x1);

        let constant = y2 - (slope * x2);

        findIntersectionPoint(slope, x1, y1, x2, y2, constant, boundaryRadius);
    }
}

/* ... */
Enter fullscreen mode Exit fullscreen mode

Corrected eyeball movement

To finish off, add your dog face(in my case I designed the dog face in Figma and exported the SVG). And to give a dramatic touch, I reduced the height of the canvas.

Hope you enjoyed this tutorial. Have a nice day!!!

Top comments (4)

Collapse
 
moopet profile image
Ben Sinclair

This is cool from a technical point of view. I notice however that it looks... funny, I guess, when you have the curson between the eyes. Compare it to, say, xeyes, where the eyes converge on the cursor. I think the effect would look better if the eyes were constrained to the same Y position but independent on the X-axis.

Collapse
 
pujaam profile image
Pujaa M

Thanks by the way... I will give this a try.

Collapse
 
rijultp profile image
Rijul Rajesh

Cool stuff, i have done a similar one using three js, I created a 3d model of the logo and made it follow the mouse. Three js is pretty cool and really levels things up.

Collapse
 
pujaam profile image
Pujaa M

IKR! I have started learning three js recently.