Skip to content

Instantly share code, notes, and snippets.

@stanwmusic
Created December 16, 2025 08:33
Show Gist options
  • Select an option

  • Save stanwmusic/62b546358d4df31686d3eb0de214309d to your computer and use it in GitHub Desktop.

Select an option

Save stanwmusic/62b546358d4df31686d3eb0de214309d to your computer and use it in GitHub Desktop.
Googly Eyes mouse tracking!
<svg id="eyes" width="128" height="128" viewBox="0 0 128 128" role="img" aria-label="googly eyes emoji that track the mouse cursor">
<defs>
<linearGradient id="a" x1="0" x2="0" y1="46.676" y2="82.083" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#424242"/>
<stop offset="1" style="stop-color:#212121"/>
</linearGradient>
<g id="eye-shape">
<path style="fill:#fafafa" d="M34.16 106.51C18.73 106.51 6.19 87.44 6.19 64s12.55-42.51 27.97-42.51S62.13 40.56 62.13 64s-12.55 42.51-27.97 42.51"/>
<path style="fill:#b0bec5" d="M34.16 23.49c6.63 0 12.98 4 17.87 11.27 5.22 7.75 8.1 18.14 8.1 29.24s-2.88 21.49-8.1 29.24c-4.89 7.27-11.24 11.27-17.87 11.27s-12.98-4-17.87-11.27C11.06 85.49 8.19 75.1 8.19 64s2.88-21.49 8.1-29.24c4.89-7.27 11.23-11.27 17.87-11.27m0-4C17.61 19.49 4.19 39.42 4.19 64s13.42 44.51 29.97 44.51S64.13 88.58 64.13 64 50.71 19.49 34.16 19.49"/>
</g>
<path id="pupil-shape" style="fill:url(#a)" d="M25.63 59.84c-2.7-2.54-2.1-7.58 1.36-11.26.18-.19.36-.37.55-.54-1.54-.87-3.23-1.36-5.01-1.36-7.19 0-13.02 7.93-13.02 17.7s5.83 17.7 13.02 17.7 13.02-7.93 13.02-17.7c0-1.75-.19-3.45-.54-5.05-3.24 2.33-7.11 2.64-9.38.51"/>
</defs>
<use href="#eye-shape" id="eye-left"/>
<use href="#pupil-shape" id="pupil-left"/>
<g transform="translate(59.68 0)">
<use href="#eye-shape" id="eye-right" />
<use href="#pupil-shape" id="pupil-right"/>
</g>
</svg>
<p><a target="_top" href="https://dbushell.com/2025/07/11/croissant-no-framework-web-app/">What’s this about?</a></p>
<!--
<svg id="eyes" width="128" height="128" viewBox="0 0 128 128">
<defs>
<linearGradient id="a" x1="0" x2="0" y1="46.676" y2="82.083" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#424242"/>
<stop offset="1" style="stop-color:#212121"/>
</linearGradient>
</defs>
<g id="eye-left">
<path style="fill:#fafafa" d="M34.16 106.51C18.73 106.51 6.19 87.44 6.19 64s12.55-42.51 27.97-42.51S62.13 40.56 62.13 64s-12.55 42.51-27.97 42.51"/>
<path style="fill:#b0bec5" d="M34.16 23.49c6.63 0 12.98 4 17.87 11.27 5.22 7.75 8.1 18.14 8.1 29.24s-2.88 21.49-8.1 29.24c-4.89 7.27-11.24 11.27-17.87 11.27s-12.98-4-17.87-11.27C11.06 85.49 8.19 75.1 8.19 64s2.88-21.49 8.1-29.24c4.89-7.27 11.23-11.27 17.87-11.27m0-4C17.61 19.49 4.19 39.42 4.19 64s13.42 44.51 29.97 44.51S64.13 88.58 64.13 64 50.71 19.49 34.16 19.49"/>
</g>
<path id="pupil-left" style="fill:url(#a)" d="M25.63 59.84c-2.7-2.54-2.1-7.58 1.36-11.26.18-.19.36-.37.55-.54-1.54-.87-3.23-1.36-5.01-1.36-7.19 0-13.02 7.93-13.02 17.7s5.83 17.7 13.02 17.7 13.02-7.93 13.02-17.7c0-1.75-.19-3.45-.54-5.05-3.24 2.33-7.11 2.64-9.38.51"/>
<g id="eye-right">
<path style="fill:#fafafa" d="M93.84 106.51c-15.42 0-27.97-19.07-27.97-42.51s12.55-42.51 27.97-42.51S121.81 40.56 121.81 64s-12.54 42.51-27.97 42.51"/>
<path style="fill:#b0bec5" d="M93.84 23.49c6.63 0 12.98 4 17.87 11.27 5.22 7.75 8.1 18.14 8.1 29.24s-2.88 21.49-8.1 29.24c-4.89 7.27-11.24 11.27-17.87 11.27s-12.98-4-17.87-11.27c-5.22-7.75-8.1-18.14-8.1-29.24s2.88-21.49 8.1-29.24c4.89-7.27 11.24-11.27 17.87-11.27m0-4c-16.55 0-29.97 19.93-29.97 44.51s13.42 44.51 29.97 44.51S123.81 88.58 123.81 64s-13.42-44.51-29.97-44.51"/>
</g>
<path id="pupil-right" style="fill:url(#a)" d="M85.31 59.84c-2.7-2.54-2.1-7.58 1.36-11.26.18-.19.36-.37.55-.54-1.54-.87-3.23-1.36-5.01-1.36-7.19 0-13.02 7.93-13.02 17.7s5.83 17.7 13.02 17.7 13.02-7.93 13.02-17.7c0-1.75-.19-3.45-.54-5.05-3.23 2.33-7.11 2.64-9.38.51"/>
</svg>
-->
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);
}
});
});
#eyes {
--size: clamp(64px, 30vi, 256px);
block-size: var(--size);
inline-size: var(--size);
}
body {
background: wheat;
block-size: 100vb;
display: grid;
grid-template-rows: 2fr 1fr;
place-items: center;
text-align: center;
}
a {
font-family: sans-serif;
font-size: 2rem;
color: #333;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment