Search code examples
javascriptjquerysvgsnap.svg

jQuery & Snap.svg -> circle not following mouse correctly


I am playing around with Snap.svg and jQuery a little bit, and I am creating this bitmoji and trying to make his eyes follow the mouse cursor.

It's all going pretty well except for the eyes. They are transforming and rotating when moving the cursor but not 100% correctly and I can't figure out why.

Here is my code in a JSFiddle: http://jsfiddle.net/bmp5j4x9/1/

Resize the result box, make it bigger and move around your mouse, I guess you'll see what I mean. Or take a look at http://dante-c.be.

This is the jQuery part:

        var s = Snap(420, 420).attr({ viewBox: "0 0 120 120" });
        $(s.node).appendTo(".button");

        var image = s.paper.image('https://render.bitstrips.com/v2/cpanel/10220069-circle-357822728_5-s4-v1.png?palette=1', 0, 0, 1, 1);
        image = image.pattern().attr({
            patternContentUnits: "objectBoundingBox",
            patternUnits: "",
            width: "100%", height: "100%", viewBox: "" 
        });
        var bitmojiCircle = s.circle(60, 60, 39).attr({ fill: image });

        var circleX = 50, circleY = 63, circleRadius = 4.5;
        var bigEyeCircle = s.circle(circleX, circleY, circleRadius);
        var L1 = s.path("M "+circleX+" "+circleY +"L 0 0").attr({stroke: "blue"});
        bigEyeCircle.attr({
            fill: "#bada55",
            stroke: "#000",
            strokeWidth: 1
        });
        var smallEyeCircle = s.circle(0,0,3.5).attr({ fill: "red" });

        var opacityCircle = s.circle(60, 60, 39).attr({ fill: "rgba(255,255,255,0.7)" });
        var menuButton = s.path("M58.486 56.324H57.19c-.48 0-.866.387-.866.865v1.29c0 .48.387.86.865.86h1.29c.48 0 .86-.39.86-.87v-1.29c0-.48-.39-.87-.87-.87zm-4.324 0h-1.297c-.478 0-.865.387-.865.865v1.29c0 .48.387.86.865.86h1.297c.478 0 .865-.39.865-.87v-1.29c0-.48-.387-.87-.865-.87zM58.486 52H57.19c-.48 0-.866.387-.866.865v1.297c0 .478.387.865.865.865h1.29c.48 0 .86-.387.86-.865v-1.297c0-.478-.39-.865-.87-.865zm-4.324 0h-1.297c-.478 0-.865.387-.865.865v1.297c0 .478.387.865.865.865h1.297c.478 0 .865-.387.865-.865v-1.297c0-.478-.387-.865-.865-.865zm12.973 4.324h-1.297c-.478 0-.865.387-.865.865v1.29c0 .48.387.86.865.86h1.297c.478 0 .865-.39.865-.87v-1.29c0-.48-.387-.87-.865-.87zm-4.324 0h-1.29c-.48 0-.86.387-.86.865v1.29c0 .48.39.86.87.86h1.3c.48 0 .87-.39.87-.87v-1.29c0-.48-.38-.87-.86-.87zM67.14 52h-1.3c-.48 0-.866.387-.866.865v1.297c0 .478.387.865.865.865h1.29c.48 0 .86-.387.86-.865v-1.297c0-.478-.39-.865-.87-.865zm-4.324 0H61.52c-.48 0-.865.387-.865.865v1.297c0 .478.386.865.865.865h1.297c.48 0 .866-.387.866-.865v-1.297c0-.478-.386-.865-.864-.865zM58.49 64.973h-1.3c-.48 0-.866.387-.866.865v1.297c0 .478.387.865.865.865h1.29c.48 0 .86-.387.86-.865v-1.297c0-.478-.39-.865-.87-.865zm-4.325 0h-1.297c-.478 0-.865.387-.865.865v1.297c0 .478.387.865.865.865h1.297c.478 0 .865-.387.865-.865v-1.297c0-.478-.388-.865-.866-.865zm4.324-4.324h-1.3c-.48 0-.87.38-.87.86v1.29c0 .48.38.86.86.86h1.29c.48 0 .86-.39.86-.87V61.5c0-.48-.39-.864-.87-.864zm-4.33 0h-1.3c-.48 0-.87.38-.87.86v1.29c0 .48.38.86.86.86h1.29c.472 0 .86-.39.86-.87V61.5c0-.48-.39-.864-.867-.864zm12.97 4.32h-1.29c-.48 0-.87.38-.87.86v1.29c0 .48.38.86.86.86h1.29c.48 0 .86-.39.86-.87v-1.29c0-.48-.387-.87-.865-.87zm-4.33 0h-1.29c-.48 0-.87.38-.87.86v1.29c0 .48.38.86.86.86h1.3c.48 0 .862-.39.862-.87v-1.29c0-.48-.39-.87-.867-.87zm4.32-4.33h-1.3c-.48 0-.87.38-.87.86v1.3c0 .48.384.86.862.86h1.3c.476 0 .863-.39.863-.87V61.5c0-.48-.388-.864-.866-.864zm-4.33 0H61.5c-.48 0-.864.38-.864.86v1.3c0 .48.387.86.866.86H62.8c.48 0 .87-.39.87-.87V61.5c0-.48-.383-.864-.86-.864z").attr({
            class: "menu-button",
            fill: "#9B9B9B",
            fillRule: "nonzero"
        });

        var c1 = s.circle(60, 60, 53).attr({ stroke: "#9B9B9B", transform: "rotate(90 60 60)" });
        var c2 = s.circle(60, 7, 2).attr({ fill: "#9B9B9B" });
        var c3 = s.circle(60, 113, 2).attr({ fill: "#9B9B9B" });
        var c4 = s.circle(113, 60, 2).attr({ fill: "#9B9B9B" });
        var c5 = s.circle(7, 60, 2).attr({ fill: "#9B9B9B" });

        var outerCircles = s.group(c1, c2, c3, c4, c5).attr({ class: "outer-circle" });
        var fullSVG = s.group(bitmojiCircle, bigEyeCircle, L1, smallEyeCircle, opacityCircle, menuButton, outerCircles).attr({ fill: "none", fillRule: "evenodd" });

        function OnMouseMove(evt) {
            L1.attr({ d: "M "+circleX+" "+circleY +"L "+evt.clientX+" "+evt.clientY });
            var totalLength = L1.getTotalLength();

            if (totalLength < circleRadius) {
                smallEyeCircle.attr({ cx: evt.clientX , cy: evt.clientY });
            } else {
                var PAL = L1.getPointAtLength(circleRadius);
                smallEyeCircle.attr({ cx: PAL.x , cy: PAL.y });
            }
        }
        document.onmousemove = OnMouseMove;

Edit

Tried to throttle/debounce it, as Nikos said, by replacing the OnMouseMove function with the following code:

var pageX = 0,
pageY = 0;

var moveIt = function() {
    L1.attr({ d: "M "+circleX+" "+circleY +"L "+pageX+" "+pageY });
    var totalLength = L1.getTotalLength();

    if (totalLength < circleRadius) {
        smallEyeCircle.attr({ cx: pageX, cy: pageY });
    } else {
        var PAL = L1.getPointAtLength(circleRadius);
        smallEyeCircle.attr({ cx: PAL.x , cy: PAL.y });
    }
    setTimeout(moveIt, 1000/25);
};

$(document).on('mousemove', function(e) {
    pageX = e.pageX;
    pageY = e.pageY;
}).one('mousemove', moveIt);

This does not seem to work.

Update

I found a better solution, but it's still not 100% functional, the area for the eyeball to move in, is too big, but I don't know how to get it smaller. Here is the updated fiddle: http://jsfiddle.net/bmp5j4x9/3/


Solution

  • As I've commented you are detecting the mouse position relative to the document and you are using those coordinates to draw inside an SVG canvas whose size is 120/120. This can not work. Next comes an example (Javascript) where the the line is following the mouse correctly

    let m = {}
    test.addEventListener("mousemove",(e)=>{
    // draw the line on mousemove
      m=oMousePosSVG(e);
      _line.setAttributeNS(null,"x2",m.x);
      _line.setAttributeNS(null,"y2",m.y);
    })
    
    function oMousePosSVG(e) {
    // a function to detect the mouse position inside an SVG
          var p = test.createSVGPoint();
          p.x = e.clientX;
          p.y = e.clientY;
          var ctm = test.getScreenCTM().inverse();
          var p =  p.matrixTransform(ctm);
          return p;
    }
    <svg id="test" viewBox="0 0 120 120" width="100vw" height="100vh">
      <circle cx="60" cy="60" r="20" fill="#d9d9d9" />
      <line id="_line" x1="55" y1="60" stroke="blue" />
    </svg>

    Yet an other solution would be letting things as you have them but recalculating the mouse position in function of the document size:

    let w = window.innerWidth;
    let h = window.innerHeight;
    let m = {}
    
    
    document.addEventListener("mousemove",(e)=>{
      //get the mouse position
      m=oMousePos(e);
      //calculate the x2 and y2 for the line in function of the size of the window
      let x2 = map(m.x, 0, w, 0, 120)
      let y2 = map(m.y, 0, h, 0, 120)
      // set the attributes x2 and y2 for the line
      _line.setAttributeNS(null,"x2",x2);
      _line.setAttributeNS(null,"y2",y2);
    })
    
    function init(){
    // a function to get the size of the window on resize
      w = window.innerWidth;
      h = window.innerHeight;
    }
    
    // you call the init on resize
    setTimeout(function() {
    		init();
    		addEventListener('resize', init, false);
    }, 15);
    
    // a function to get the mouse position
    function oMousePos(evt) {
       return { 
           x: evt.clientX,
           y: evt.clientY
          }
    }
    
    
    function map(n, a, b, _a, _b) {
      let d = b - a;
      let _d = _b - _a;
      let u = _d / d;
      return _a + n * u;
    }
    svg {
      border: 1px solid;
      position: absolute;
      margin: auto;
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
    }
    <svg id="test" viewBox="0 0 120 120" width="240" >
      <circle cx="60" cy="60" r="20" fill="#d9d9d9" />
      <line id="_line" x1="55" y1="60" stroke="blue" />
    </svg>

    I hope it helps.

    Update

    The OP comments that in fact they want the small red circle to follow the mouse. In this case you need to calculate the angle between the center of the eye and the mouse, and you draw the red circle using this angle:

    let m = {}
    let c = {x:55,y:60}// the center of the eye
    let r = whitecircle.getAttribute("r") - redcircle.getAttribute("r") - .5;
    // where .5 is 1/2 stroke-width
    test.addEventListener("mousemove",(e)=>{
    // draw the line on mousemove
      m=oMousePosSVG(e);
      //_line.setAttributeNS(null,"x2",m.x);
      //_line.setAttributeNS(null,"y2",m.y);
      var angle = getAngle(m,c)
      //this are the coordinates for the center of the red circle 
      var x2 = c.x + r * Math.cos(angle);
      var y2 = c.y + r * Math.sin(angle);
    
      redcircle.setAttributeNS(null,"cx",x2);
      redcircle.setAttributeNS(null,"cy",y2);
    })
    
    function oMousePosSVG(e) {
    // a function to detect the mouse position inside an SVG
          var p = test.createSVGPoint();
          p.x = e.clientX;
          p.y = e.clientY;
          var ctm = test.getScreenCTM().inverse();
          var p =  p.matrixTransform(ctm);
          return p;
    }
    
    function getAngle(p1,p2){
      // a function to calculate the angle between two points p1 and p2
      var deltaX = p1.x - p2.x;
      var deltaY = p1.y - p2.y;
      return Math.atan2(deltaY, deltaX);
    }
    <svg id="test" viewBox="0 0 120 120" width="100vw" height="100vh">
      <circle cx="60" cy="60" r="20" fill="#d9d9d9" />
      <circle id="whitecircle" cx="55" cy="60" r="5" fill="#fff" stroke="black" />
      <circle cx="55" cy="60" r="3" fill="#f00" id="redcircle"  />
      <!--<line id="_line" x1="55" y1="60" stroke="blue" />-->
    </svg>