Eye Mouse Tracking

In your HTML, be sure to have the following IDs:

  • eyes
  • eye-left
  • eye-right
  • pupil-left
  • pupil-right

Source Code

CSS

#pupil-left, #pupil-right {
transition: transform 0.1s ease-out;
}

JavaScript

const eyesSVG = document.querySelector('#eyes');
const eyes = [
{
eye: eyesSVG.querySelector('#eye-left'),
pupil: eyesSVG.querySelector('#pupil-left'),
offsetX: 0
},
{
eye: eyesSVG.querySelector('#eye-right'),
pupil: eyesSVG.querySelector('#pupil-right'),
offsetX: 0
}
];

const updateEye = (ev, {eye, pupil, offsetX}) => {
const eyeRect = eye.getBoundingClientRect();
const centerX = eyeRect.left + eyeRect.width / 2;
const centerY = eyeRect.top + eyeRect.height / 2;

const distX = ev.clientX - centerX;
const distY = ev.clientY - centerY;

const pupilRect = pupil.getBoundingClientRect();
const maxDistX = pupilRect.width / 2;
const maxDistY = pupilRect.height / 2;

const angle = Math.atan2(distY, distX);

const newPupilX = offsetX + Math.min(maxDistX, Math.max(-maxDistX, Math.cos(angle) * maxDistX));
const newPupilY = Math.min(maxDistY, Math.max(-maxDistY, Math.sin(angle) * maxDistY));

const svgCTM = eyesSVG.getScreenCTM();
const scaledPupilX = newPupilX / svgCTM.a;
const scaledPupilY = newPupilY / svgCTM.d;

pupil.setAttribute('transform', `translate(${scaledPupilX}, ${scaledPupilY})`);
}
// Pupil position starts off-centre on the X axis
const calcOffset = () => {
for (const props of eyes) {
props.pupil.removeAttribute('transform');
const eyeRect = props.eye.getBoundingClientRect();
const pupilRect = props.pupil.getBoundingClientRect();
props.offsetX = ((eyeRect.right - pupilRect.right) - (pupilRect.left - eyeRect.left)) / 2;
}
}
calcOffset();

globalThis.addEventListener('resize', () => {
calcOffset();
});

let frame = 0;
globalThis.addEventListener('mousemove', (ev) => {
cancelAnimationFrame(frame);
frame = requestAnimationFrame(() => {
for (const eye of eyes) {
updateEye(ev, eye);
}
});
});

// added by albinoblacksheep.com
const handleTouchEvent = (ev) => {
// ev.preventDefault(); // Prevent scrolling
const touch = ev.touches[0]; // Get the first touch point
cancelAnimationFrame(frame);
frame = requestAnimationFrame(() => {
for (const eye of eyes) {
updateEye(touch, eye);
}
});
};

globalThis.addEventListener('touchmove', handleTouchEvent);
globalThis.addEventListener('touchstart', handleTouchEvent);

Based off of this Codepen by David Bushell and then modified to work on touchscreens and allow smooth transitioning.