Animating the Eyes of the Go Gopher

Scalable Vector Graphics (SVGs) are XML based images that can scale, with built-in support for interaction and animation. You can create them with programs like Illustrator & Inkscape, or by typing the definitions or manually.

I thought it would be fun to animate the eyes of the Go gopher, as you can see below (move your mouse or click/tap on the page).

I started with an SVG gopher by tenntenn. Then, using Chrome developer tools, I hovered over the eyes and found the part of the SVG that defined the eyes and made a few changes:

  • Added class="pupil" and class="glare" to the relevant ellipses. I use these classes later to grab the element to animate.
  • Added an extra invisible white circle behind the pupil because the outline of the eye is a freeform path without an easy to reference radius to stay within (I wanted the entire pupil to stay visible.)
  • Added class="eye" to the g of each eye. We can use this class later to loop through the all eyes on the page.
  • Finally, I moved the pupil and glare to the center of the eye to make the animations easy later.

The final SVG for the eyes looks like this:

<g class="eye">
    <circle class="eyeball" fill-rule="evenodd" clip-rule="evenodd" cx="131" cy="92" r="42" fill="white"/>
    <ellipse class="pupil" fill-rule="evenodd" clip-rule="evenodd" cx="131" cy="92" rx="14.829" ry="16.062"/>
    <ellipse class="glare" fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" cx="131" cy="92" rx="3.496" ry="4.082"/>
</g>
<g class="eye">
    <circle class="eyeball" fill-rule="evenodd" clip-rule="evenodd" cx="258" cy="87" r="42" fill="white"/>
    <ellipse class="pupil" fill-rule="evenodd" clip-rule="evenodd" cx="258" cy="87" rx="14.582" ry="16.062"/>
    <ellipse class="glare" fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" cx="258" cy="87" rx="3.438" ry="4.082"/>
</g>

You can grab the eyes in JavaScript using document.querySelectorAll:

let eyes = document.querySelectorAll(".eye");

Once you have an SVG element, you can modify it using a CSS transform. A transform manipulates the element, translating (moving), rotating, scaling, etc.

You can get the current position of the mouse using the onmousemove event.

Given the location of the eye and the location of the mouse, I calculated the angle between the two using arctan. Given the ratio of “opposite” over “adjacent,” arctan gives you the corresponding angle.

gopher-eyes/arctan.svg

The goal is to rotate the pupil and glare of the eyes this same angle, toward the mouse.

The transformation first moves the element (either the pupil or the glare) the correct distance along the positive x-axis, then rotates it toward the mouse.

gopher-eyes/transform.svg

We can use the distance between the eye and the mouse to decide how far away from the center of the eye we want to move the pupil and glare. I capped mine at 200px, meaning once the mouse is 200px away, the pupil and glare will be at the far edge of the eye.

Now, given a mouse event e, we can loop through all eyes on the page, calculate the angle between the current eye and the mouse, and apply the transformations:

function animate(e) {
    eyes.forEach(function(eye) {
        let eyeBall = eye.getElementsByClassName("eyeball")[0],
            pupil = eye.getElementsByClassName("pupil")[0],
            glare = eye.getElementsByClassName("glare")[0],
            eyeR = eyeBall.r.baseVal.value,
            pupilR = pupil.rx.baseVal.value,
            glareR = glare.rx.baseVal.value,
            bound = eyeBall.getBoundingClientRect(),
            cx = bound.left + eyeR,
            cy = bound.bottom - eyeR,
            x = e.clientX - cx,
            y = e.clientY - cy,
            d = Math.sqrt(x*x + y*y),
            theta = Math.atan2(y,x),
            angle = theta*180/Math.PI + 360;
    
        let max = 200.0
        if (d > max) d = max;
    
        let t  = d / max * (eyeR - pupilR),
            t2 = d / max * (eyeR - glareR);
    
        pupil.style.transform = `translate(${t + "px"}) rotate(${angle + "deg"})`;
        pupil.style.transformOrigin = `${eyeBall.cx.baseVal.value - t +"px"} ${eyeBall.cy.baseVal.value +"px"}`;
    
        glare.style.transform = `translate(${t2 + "px"}) rotate(${angle + "deg"})`;
        glare.style.transformOrigin = `${eyeBall.cx.baseVal.value - t2 +"px"} ${eyeBall.cy.baseVal.value +"px"}`;
    });
}

Then, I connected the animate function to the mouse events I care about (onmousemove for following the mouse and document.onclick for devices without a mouse):

document.addEventListener("mousemove", animate);
document.onclick = animate;

Note: I use getBoundingClientRect() to get the location of the eye, rather than the .cx value of it to make sure I get the location on the page, not the location within the SVG. The MDN page on SVG Positions describes more about SVG coordinate systems.

And now we have a gopher with animated eyes! 👀