skip to Main Content

I have A SVG with two eyes where each pupil tracks the mouse position.

The issue is, the two eyes acts individually and not as a group. For example if you position the cursor between both eyes, the pupil from the left goes in the right direction and the pupil from the right goes to the left direction.

What I would like to achieve is, when the cursor is positioned between (or close to) both eyes, the pupils stay in the center of the eye.

    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);
    });
    .container{
      width: 20em;
      height: 20em;
      margin: 200px auto;
    }
    <div class="container">
      <svg id="svg1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
        <ellipse cx="80" cy="45" rx="20" ry="22" stroke-width="10" stroke="#fdd176"/>
        <ellipse cx="120" cy="45" rx="20" ry="22" stroke-width="10" stroke="#fdd176"/>
        <line marker-start="url(#pupil)" id="l1" x1="80" y1="45" />
        <line marker-start="url(#pupil)" id="l2" x1="120" y1="45" />
        <defs>
          <marker id="pupil" viewBox="0 0 18 18" refX="10" refY="5" markerWidth="37" markerHeight="37" orient="auto-start-reverse">
            <circle fill="#fdd176" r="4" cy="5" cx="5" />
          </marker>
        </defs>
      </svg>
    </div>



 

Here is my codepen where you can see it live.

And another codepen where you can see the wanted result.

Thank you.

3

Answers


  1. The following solution initially places the pupils in the center of the eye and then translates them by one tenth of the distance that the mouse pointer has from the center of the SVG viewBox (which I chose to be (0, 0) for simplicity). The factor of one tenth is chosen so that when the mouse reaches the edge of the viewBox, the pupils reach the edge of the eye.

    Both pupils always move in parallel, like in the "wanted result" codepen.

    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);
      g.setAttribute("transform", `translate(${p.x/10}, ${p.y/10})`);
    });
    <svg id="svg1" xmlns="http://www.w3.org/2000/svg" viewBox="-100 -100 200 200">
      <ellipse cx="-20" cy="0" rx="20" ry="22" stroke-width="10" stroke="#fdd176"/>
      <ellipse cx="20" cy="0" rx="20" ry="22" stroke-width="10" stroke="#fdd176"/>
      <g id="g">
        <circle fill="#fdd176" r="6" cy="0" cx="-20" />
        <circle fill="#fdd176" r="6" cy="0" cx="20" />
      </g>
    </svg>
    Login or Signup to reply.
  2. With the technique, using the a marker, you can adjust the refX of the marker element. As a default value it is 10, but when you point in between the eyes the value need to be smaller.

    So, here I try to do a calculation based on the position on the x axis. I think it could be done better somehow, but this is all for now.

    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);
      
      let diff_l1x = l1.x2.baseVal.value - l1.x1.baseVal.value;
      let diff_l2x = l2.x1.baseVal.value - l2.x2.baseVal.value;
      let refx = 10;
      if(diff_l1x > 0 & diff_l1x <= 20) refx = 10 - diff_l1x / 20 * 4;
      if(diff_l2x > 0 & diff_l2x < 20) refx = 10 - diff_l2x / 20 * 4;
      document.querySelector("#pupil").setAttribute('refX', refx);
    });
    .container {
      width: 20em;
      height: 20em;
      margin: 200px auto;
    }
    <div class="container">
      <svg id="svg1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
        <ellipse cx="80" cy="45" rx="20" ry="22" stroke-width="10" stroke="#fdd176"/>
        <ellipse cx="120" cy="45" rx="20" ry="22" stroke-width="10" stroke="#fdd176"/>
        <line marker-start="url(#pupil)" id="l1" x1="80" y1="45" />
        <line marker-start="url(#pupil)" id="l2" x1="120" y1="45" />
        <defs>
          <marker id="pupil" viewBox="0 0 18 18" refX="10" refY="5" markerWidth="37" markerHeight="37" orient="auto-start-reverse">
            <circle fill="#fdd176" r="4" cy="5" cx="5" />
          </marker>
        </defs>
      </svg>
    </div>
    Login or Signup to reply.
  3. Enhancement of Heiko his answer

    • avoiding Global variables
    • removing Listener
    • needs more tweaking…
    <moving-eyes></moving-eyes>
    <moving-eyes color="lightblue"></moving-eyes>
    <moving-eyes color="sandybrown"></moving-eyes>
    
    <script>
      customElements.define("moving-eyes", class extends HTMLElement {
        connectedCallback() {
          let color = this.getAttribute("color") || "#fdd176";
          let id = crypto.randomUUID(); // every SVG needs unique IDs
          this.innerHTML = `
    <svg viewBox="-100 -100 200 200" style="height:150px;background:beige">
      <ellipse id="eye${id}" cx="-20" cy="0" rx="20" ry="22" stroke-width="10" stroke="${color}"/>
      <use href="#eye${id}" x="40"/>
      <g>
        <circle id="pupil${id}" fill="${color}" r="6" cy="0" cx="-20" />
        <use href="#pupil${id}" x="40" />
      </g>
    </svg>`;
          document.addEventListener('mousemove', e => this.mousemove(e));
        }
        mousemove(e) {
          let svg = this.querySelector("svg");
          let p = (new DOMPoint(e.clientX, e.clientY)).matrixTransform(svg.getScreenCTM().inverse());
          this.querySelector("g").setAttribute("transform", `translate(${p.x/10}, ${p.y/10})`);
        }
        disconnectedCallback() {
          removeEventListener("mousemove", this.mousemove);
        }
      });
    </script>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search