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>
CSS
canvas {
background-color: #A15A22;
}
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();
You will achieve something like this:
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();
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);
}
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.
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:
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;
}
}
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:
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
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);
}
}
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.
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);
}
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.
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);
}
}
/* ... */
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)
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.
Thanks by the way... I will give this a try.
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.
IKR! I have started learning three js recently.