skip to Main Content

I am trying to make the eye pupil of the svg to follow cursor using this tutorial:

https://dev.to/anomaly3108/make-svg-follow-cursor-using-css-and-js-2okp

We have 4 divs:

  1. eyeball_left
  2. eyeball_right
  3. pupil_left
  4. pupil_right

looks like the JS is working, but the angle is not really accurate. the pupils are going too high and they do not stay in the correct position.

let eyeball_left = document.querySelector("#eyeball_left"),
  pupil_left = document.querySelector("#pupil_left"),
  eyeArea_left = eyeball_left.getBoundingClientRect(),
  pupil_leftArea = pupil_left.getBoundingClientRect(),
  R_left = eyeArea_left.width / 2,
  r_left = pupil_leftArea.width / 2,
  centerX_left = eyeArea_left.left + R_left,
  centerY_left = eyeArea_left.top + R_left;
console.log(centerX_left)
console.log(centerY_left)
let eyeball_right = document.querySelector("#eyeball_right"),
  pupil_right = document.querySelector("#pupil_right"),
  eyeArea_right = eyeball_right.getBoundingClientRect(),
  pupil_rightArea = pupil_right.getBoundingClientRect(),
  R_right = eyeArea_right.width / 2,
  r_right = pupil_rightArea.width / 2,
  centerX_right = eyeArea_right.left + R_right,
  centerY_right = eyeArea_right.top + R_right;
console.log(centerX_right)
console.log(centerY_right)
document.addEventListener("mousemove", (e) => {
  let x_left = e.clientX - centerX_left,
    y_left = e.clientY - centerY_left,
    theta_left = Math.atan2(y_left, x_left),
    angle_left = (theta_left * 180) / Math.PI + 360;
  let x_right = e.clientX - centerX_right,
    y_right = e.clientY - centerY_right,
    theta_right = Math.atan2(y_right, x_right),
    angle_right = (theta_right * 180) / Math.PI + 360;
  pupil_left.style.transform = `translateX(${
      R_left - r_left + "px"
    }) rotate(${angle_left + "deg"})`;
  pupil_left.style.transformOrigin = `${r_left + "px"} center`;
  pupil_right.style.transform = `translateX(${
      R_right - r_right + "px"
    }) rotate(${angle_right + "deg"})`;
  pupil_right.style.transformOrigin = `${r_right + "px"} center`;
});
#monster {
  height: 100px;
  width: 400px;
}
<div id="monster">
  <svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="168.88 0 290.9 400.77">
                <g>
                  <title>Layer 1</title>
                  <path
                    id="svg_1"
                    fill="#6c63ff"
                    d="m296.30999,388.65991l0,-67.88825l-22,0l0,67.88825a13.98286,13.98286 0 0 0 -7,12.11175l36,0a13.98286,13.98286 0 0 0 -7,-12.11175z"
                  />
                  <path
                    id="svg_2"
                    fill="#6c63ff"
                    d="m355.30999,388.65991l0,-67.88825l-22,0l0,67.88825a13.98286,13.98286 0 0 0 -7,12.11175l36,0a13.98286,13.98286 0 0 0 -7,-12.11175z"
                  />
                  <circle
                    id="svg_3"
                    fill="#6c63ff"
                    r="145.45113"
                    cy="238.54887"
                    cx="314.33362"
                  />
                  <ellipse
                    id="svg_4"
                    fill="#fff"
                    ry="19.21053"
                    rx="57.63158"
                    cy="311.43609"
                    cx="314.33362"
                  />
                  <circle
                    id="svg_5"
                    fill="#fff"
                    r="24.69925"
                    cy="205.61654"
                    cx="262.19076"
                  />
                  <circle
                    id="svg_6"
                    fill="#fff"
                    r="24.69925"
                    cy="205.61654"
                    cx="366.47648"
                  />

                  {/* eyebol */}
                  <circle
                    id="eyeball_left"
                    fill="#3f3d56"
                    r="19.21053"
                    cy="205.31579"
                    cx="262.67948"
                  />
                  <circle
                    id="eyeball_right"
                    fill="#3f3d56"
                    r="19.21053"
                    cy="205.31579"
                    cx="366.73212"
                  />
                  {/* eyebol */}

                  <ellipse
                    id="svg_9"
                    fill="#3f3d56"
                    ry="74.09774"
                    rx="96.05263"
                    cy="87.09774"
                    cx="314.33362"
                  />
                  <ellipse
                    id="svg_10"
                    fill="#3f3d56"
                    ry="18"
                    rx="38"
                    cy="18"
                    cx="314.33362"
                  />
                  <path
                    id="svg_11"
                    fill="#3f3d56"
                    d="m315.39428,259.75517c6.323,-6.40629 16.04713,-6.53419 24.2561,-4.42458c9.786,2.51489 18.116,8.57423 27.17791,12.79851a49.55555,49.55555 0 0 0 14.58024,4.54776a38.27945,38.27945 0 0 0 36.63871,-17.0858a38.7584,38.7584 0 0 0 4.54212,-30.91717a1.50128,1.50128 0 0 0 -2.89283,0.79752a35.70693,35.70693 0 0 1 -3.34417,27.11259a35.29669,35.29669 0 0 1 -35.30417,17.03843a49.62651,49.62651 0 0 1 -14.22886,-4.81212c-8.76148,-4.28973 -16.98465,-10.00419 -26.54935,-12.41745c-9.21411,-2.32481 -19.9481,-1.90083 -26.997,5.241c-1.35753,1.37543 0.76245,3.4981 2.12132,2.12132l-0.00002,-0.00001z"
                  />
                  <path
                    id="svg_12"
                    fill="#3f3d56"
                    d="m315.39428,257.63384c-6.22928,-6.31139 -15.3898,-7.36984 -23.77027,-5.92682c-9.6154,1.65567 -17.88675,6.88869 -26.379,11.36988c-8.6772,4.57879 -17.92825,8.08187 -27.8912,6.48578a35.20905,35.20905 0 0 1 -23.1751,-14.039a35.77653,35.77653 0 0 1 -5.208,-30.05228a1.50128,1.50128 0 0 0 -2.89283,-0.79752a38.80889,38.80889 0 0 0 2.82291,27.89016a37.47231,37.47231 0 0 0 20.97865,18.1838c9.41409,3.348 19.35061,2.63 28.52089,-1.11613c9.42621,-3.85066 17.77515,-10.13661 27.45644,-13.36827c8.93708,-2.98324 20.2603,-3.75844 27.41619,3.49176c1.3583,1.37619 3.47944,-0.7453 2.12132,-2.12132l0,-0.00004z"
                  />
                  <circle
                    id="svg_13"
                    fill="#3f3d56"
                    r="11"
                    cy="258.5"
                    cx="314.36371"
                  />
                  {/* PUPIL */}
                  <circle
                    id="pupil_left"
                    fill="#fff"
                    r="4"
                    cy="198.77165"
                    cx="254.31"
                  />
                  <circle
                    id="pupil_right"
                    fill="#fff"
                    r="4"
                    cy="198.77165"
                    cx="376.31"
                  />
                  {/* PUPIL */}
                </g>
              </svg>

2

Answers


  1. The basic idea here, is that I use a line element to decide the rotation/direction of the eye. A line can have a marker in both ends and in the middle. In this example the eye ball is a marker and then I update the end of the line based on the position of the mouse.

    First a simple example with outlines and then the full example:

    let l1 = document.querySelector("#l1");
    let l2 = document.querySelector("#l2");
    let svg1 = document.querySelector("#svg1");
    
    const toSVGPoint = (svg, x, y) => {
      let p = new DOMPoint(x, y);
      return p.matrixTransform(svg.getScreenCTM().inverse());
    };
    
    document.addEventListener('mousemove', e => {
      let p = toSVGPoint(svg1, e.clientX, e.clientY);
      l1.setAttribute('x2', p.x);
      l1.setAttribute('y2', p.y);
      l2.setAttribute('x2', p.x);
      l2.setAttribute('y2', p.y);
    });
    <svg id="svg1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 50">
      <circle cx="75" cy="25" r="20" fill="none" stroke="blue" />
      <circle cx="125" cy="25" r="20" fill="none" stroke="blue" />
      <line marker-start="url(#pupil)" id="l1" x1="75" y1="25" stroke="red" />
      <line marker-start="url(#pupil)" id="l2" x1="125" y1="25" stroke="red" />
      <defs>
        <marker id="pupil" viewBox="0 0 10 10" refX="16" refY="5" markerWidth="10"
          markerHeight="10" orient="auto-start-reverse">
          <rect width="10" height="10" fill="none" stroke="red"/>
          <circle fill="none" stroke="green" r="4" cy="5" cx="5" />
        </marker>
      </defs>
    </svg>
    let l1 = document.querySelector("#l1");
    let l2 = document.querySelector("#l2");
    let svg1 = document.querySelector("#svg1");
    
    const toSVGPoint = (svg, x, y) => {
      let p = new DOMPoint(x, y);
      return p.matrixTransform(svg.getScreenCTM().inverse());
    };
    
    document.addEventListener('mousemove', e => {
      let p = toSVGPoint(svg1, e.clientX, e.clientY);
      l1.setAttribute('x2', p.x);
      l1.setAttribute('y2', p.y);
      l2.setAttribute('x2', p.x);
      l2.setAttribute('y2', p.y);
    });
    #monster {
      height: 100px;
      width: 400px;
    }
    <div id="monster">
      <svg id="svg1" xmlns="http://www.w3.org/2000/svg" viewBox="168.88 0 290.9 400.77">
                    <g>
                      <title>Layer 1</title>
                      <path
                        id="svg_1"
                        fill="#6c63ff"
                        d="m296.30999,388.65991l0,-67.88825l-22,0l0,67.88825a13.98286,13.98286 0 0 0 -7,12.11175l36,0a13.98286,13.98286 0 0 0 -7,-12.11175z"
                      />
                      <path
                        id="svg_2"
                        fill="#6c63ff"
                        d="m355.30999,388.65991l0,-67.88825l-22,0l0,67.88825a13.98286,13.98286 0 0 0 -7,12.11175l36,0a13.98286,13.98286 0 0 0 -7,-12.11175z"
                      />
                      <circle
                        id="svg_3"
                        fill="#6c63ff"
                        r="145.45113"
                        cy="238.54887"
                        cx="314.33362"
                      />
                      <ellipse
                        id="svg_4"
                        fill="#fff"
                        ry="19.21053"
                        rx="57.63158"
                        cy="311.43609"
                        cx="314.33362"
                      />
                      <circle
                        id="svg_5"
                        fill="#fff"
                        r="24.69925"
                        cy="205.61654"
                        cx="262.19076"
                      />
                      <circle
                        id="svg_6"
                        fill="#fff"
                        r="24.69925"
                        cy="205.61654"
                        cx="366.47648"
                      />
    
                      {/* eyebol */}
                      <circle
                        id="eyeball_left"
                        fill="#3f3d56"
                        r="19.21053"
                        cy="205.31579"
                        cx="262.67948"
                      />
                      <circle
                        id="eyeball_right"
                        fill="#3f3d56"
                        r="19.21053"
                        cy="205.31579"
                        cx="366.73212"
                      />
                      {/* eyebol */}
    
                      <ellipse
                        id="svg_9"
                        fill="#3f3d56"
                        ry="74.09774"
                        rx="96.05263"
                        cy="87.09774"
                        cx="314.33362"
                      />
                      <ellipse
                        id="svg_10"
                        fill="#3f3d56"
                        ry="18"
                        rx="38"
                        cy="18"
                        cx="314.33362"
                      />
                      <path
                        id="svg_11"
                        fill="#3f3d56"
                        d="m315.39428,259.75517c6.323,-6.40629 16.04713,-6.53419 24.2561,-4.42458c9.786,2.51489 18.116,8.57423 27.17791,12.79851a49.55555,49.55555 0 0 0 14.58024,4.54776a38.27945,38.27945 0 0 0 36.63871,-17.0858a38.7584,38.7584 0 0 0 4.54212,-30.91717a1.50128,1.50128 0 0 0 -2.89283,0.79752a35.70693,35.70693 0 0 1 -3.34417,27.11259a35.29669,35.29669 0 0 1 -35.30417,17.03843a49.62651,49.62651 0 0 1 -14.22886,-4.81212c-8.76148,-4.28973 -16.98465,-10.00419 -26.54935,-12.41745c-9.21411,-2.32481 -19.9481,-1.90083 -26.997,5.241c-1.35753,1.37543 0.76245,3.4981 2.12132,2.12132l-0.00002,-0.00001z"
                      />
                      <path
                        id="svg_12"
                        fill="#3f3d56"
                        d="m315.39428,257.63384c-6.22928,-6.31139 -15.3898,-7.36984 -23.77027,-5.92682c-9.6154,1.65567 -17.88675,6.88869 -26.379,11.36988c-8.6772,4.57879 -17.92825,8.08187 -27.8912,6.48578a35.20905,35.20905 0 0 1 -23.1751,-14.039a35.77653,35.77653 0 0 1 -5.208,-30.05228a1.50128,1.50128 0 0 0 -2.89283,-0.79752a38.80889,38.80889 0 0 0 2.82291,27.89016a37.47231,37.47231 0 0 0 20.97865,18.1838c9.41409,3.348 19.35061,2.63 28.52089,-1.11613c9.42621,-3.85066 17.77515,-10.13661 27.45644,-13.36827c8.93708,-2.98324 20.2603,-3.75844 27.41619,3.49176c1.3583,1.37619 3.47944,-0.7453 2.12132,-2.12132l0,-0.00004z"
                      />
                      <circle
                        id="svg_13"
                        fill="#3f3d56"
                        r="11"
                        cy="258.5"
                        cx="314.36371"
                      />
                    </g>
                    {/* PUPIL */}
                    <line marker-start="url(#pupil)" id="l1" x1="262.67948" y1="205.31579" stroke="none" />
                    <line marker-start="url(#pupil)" id="l2" x1="366.73212" y1="205.31579" stroke="none" />
                    {/* PUPIL */}
                    <defs>
                      <marker id="pupil" viewBox="0 0 10 10" refX="16" refY="5" markerWidth="10"
                        markerHeight="10" orient="auto-start-reverse">
                        <circle fill="#fff" r="4" cy="5" cx="5" />
                      </marker>
                    </defs>
                  </svg>
                </div>
    Login or Signup to reply.
  2. Alternative: update <circle> cx and cy attributes

    This approach requires to calculate

    1. the angle between cursor coordinates and the eyeball’s center
    2. the new point position on the circle (based on angle, radius

    Demo example

    const svg = document.getElementById('svg')
    document.addEventListener("mousemove", (e) => {
      movePupils(e);
    });
    
    function movePupils(e) {
      let eyes = svg.querySelectorAll('.eye');
      eyes.forEach(eye=>{
        let eyeball = eye.querySelector('.eyeball');
        let pupil = eye.querySelector('.pupil');
        
        // get center cx/cy and radius
        let pCenter = {x: +eyeball.getAttribute('cx'), y:+eyeball.getAttribute('cy') };
        let rEyeball = +eyeball.getAttribute('r');
        let rPupil = +pupil.getAttribute('r');
        
        // translate cursor HTML DOM coordinates to SVG DOM units
        let pCursor = new DOMPoint(e.clientX, e.clientY);
        pCursor = pCursor.matrixTransform(svg.getScreenCTM().inverse());
    
        // get angle between cursor and eyeball center;
        let angle = (Math.atan2(pCursor.y - pCenter.y, pCursor.x - pCenter.x) * 180) / Math.PI;
            
        //get distance between cursor and eyeball center
        let a = pCursor.x - pCenter.x;
        let b = pCursor.y - pCenter.y;
        let distance =  Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
        
        // adjust pupil movement inside eyeball boundaries
        let offset = distance<rEyeball ? 1/rEyeball*distance : 1;
        let radiusOuter = (rEyeball-rPupil)*offset;
        
        let pMoved = {
          x: pCenter.x + Math.cos((angle * Math.PI) / 180) * radiusOuter,
          y: pCenter.y + Math.sin((angle * Math.PI) / 180) * radiusOuter
        }
        // update attributes
        pupil.setAttribute('cx', pMoved.x)
        pupil.setAttribute('cy', pMoved.y)
            
      })
    
    }
    body{
      margin:5em;
    }
    
    svg{
      width:20em;
      overflow:visible;
      border:1px solid #ccc;
    }
    <svg id="svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 100">
    
      <g class="eye">
        <circle class="eyeball" cx="30" cy="50" r="25" fill="none" stroke="#000" />
        <circle class="pupil" cx="30" cy="50" r="5" fill="#000" />
      </g>
    
      <g class="eye">
        <circle class="eyeball" cx="75" cy="25" r="20" fill="none" stroke="#000" />
        <circle class="pupil" cx="75" cy="25" r="5" fill="#000" />
      </g>
      
      <g class="eye">
        <circle class="eyeball" cx="120" cy="50" r="25" fill="none" stroke="#000" />
        <circle class="pupil" cx="120" cy="50" r="5" fill="#000" />
      </g>
      
      <g class="eye">
        <circle class="eyeball" cx="75" cy="75" r="20" fill="none" stroke="#000" />
        <circle class="pupil" cx="75" cy="75" r="5" fill="#000" />
      </g>
      
    </svg>

    The above script can be applied by wrapping all eyeballs and pupils in a group with a class "eye" like so :

      <g class="eye">
        <circle class="eyeball" cx="120" cy="50" r="25" />
        <circle class="pupil" cx="120" cy="50" r="5" />
      </g>
    

    Like in @chrwahl’s example we need to convert HTML DOM coordinates to SVG user units.

    let pCursor = new DOMPoint(e.clientX, e.clientY);
    pCursor = pCursor.matrixTransform(svg.getScreenCTM().inverse());
    

    Calculate angles

    let pCenter = {x: +eyeball.getAttribute('cx'), y:+eyeball.getAttribute('cy') };
    let angle = (Math.atan2(pCursor.y - pCenter.y, pCursor.x - pCenter.x) * 180) / Math.PI;
    

    Fine tune pupil positioning within eyeball area

    Calculating the distance between cursor and eyeball center, allows us to further adjust the pupil movement: If the cursor is within the eyeball, the pupil will be centered around the current mouse coordinates.

    let a = pCursor.x - pCenter.x;
    let b = pCursor.y - pCenter.y;
    let distance =  Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
    let offset = distance<rEyeball ? 1/rEyeball*distance : 1;
    let radiusOuter = (rEyeball-rPupil)*offset;  
    

    Point on circle

    let pOnCircle = {
      x: pCenter.x + Math.cos((angle * Math.PI) / 180) * radiusOuter,
      y: pCenter.y + Math.sin((angle * Math.PI) / 180) * radiusOuter
    }
    

    Sinc we want the circle to be placed within the eyball’s boundaries we need to use a decreased radius for this calculation (according to the pupil’s radius).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search