A Pen by Stan Williams on CodePen.
Created
December 16, 2025 08:33
-
-
Save stanwmusic/62b546358d4df31686d3eb0de214309d to your computer and use it in GitHub Desktop.
Googly Eyes mouse tracking!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> | |
| --> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } | |
| }); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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