1

I’d like to rotate the gradient of an svg based on the mouse position. The mechanic should be as followed, where [0,0] is the mouse being in the upper left corder of the window, [100%,0] should be the mouse in the upper right corner of the window etc.

enter image description here

What I have so far is that the angle changes on mouse position (only mouseX) but not based on my desired mechanic: https://codepen.io/magglomag/pen/YzKYLaa

The svg gradient is defined like this:

<defs>
    <linearGradient gradientTransform="rotate( X, 0.5, 0.5 )" id="gradient" gradientUnits="objectBoundingBox">
      <stop offset="0.4" style="stop-color:#33FF8F"/>
      <stop offset="0.6" style="stop-color:#5A33FF"/>
    </linearGradient>
</defs>

The manipulation of the angle is realized by changing the X in the gradientTransform attribute with JS:

$( 'body' ).mousemove( function( e ) {

    mouseX = e.pageX - this.offsetLeft;
    mouseY = e.pageY - this.offsetTop;

    xy = mouseX;

    $( 'svg defs' ).html( '<linearGradient gradientTransform="rotate(' + xy + ', 0.5, 0.5 )"  id="gradient" gradientUnits="objectBoundingBox"><stop offset="0.4" stop-color="#33FF8F"/><stop offset="0.6" stop-color="#5A33FF"/></linearGradient>' );

});

Besides I’d like to add a bit of easing so the change is not that hard. Here’s an example I found which uses easing. Not in combination with a gradient angle change but with a movement but perhaps the underlying code might be helpful: https://www.kirupa.com/canvas/mouse_follow_ease.htm

Any help is much appreciated.

user1706680
  • 1,103
  • 3
  • 15
  • 34

2 Answers2

3

All you need is a bit of math. You need center point of the SVG image and then angle between the mouse and that point:

Relative to <svg>

  // position of mouse
  mouseX = e.pageX - this.offsetLeft;
  mouseY = e.pageY - this.offsetTop;
  // client rect of the gear
  const svgRoot = document.querySelector("#mysvg");
  const rect = svgRoot.getBoundingClientRect();
  // center point is x+width/2 and y+height/2
  const midx = rect.left + (rect.right - rect.left)/2;
  const midy = rect.top + (rect.bottom - rect.top)/2;
  // angle
  const angle = Math.atan2(midy - mouseY, midx - mouseX);
  // The transform uses degrees (0-365), not radians (0 - 2PI)
  const angleDeg = angle* 180 / Math.PI

Demo: https://codepen.io/MXXIV/pen/OJLzEOV

Relative to window

  // position of mouse
  mouseX = e.pageX - this.offsetLeft;
  mouseY = e.pageY - this.offsetTop;
  // center point is x+width/2 and y+height/2
  const midx = window.innerWidth/2;
  const midy = window.innerHeight/2;
  // angle
  const angle = Math.atan2(midy - mouseY, midx - mouseX);
  const angleDeg = angle* 180 / Math.PI

Demo: https://codepen.io/MXXIV/pen/eYOyjdP

Tomáš Zato
  • 50,171
  • 52
  • 268
  • 778
  • Thanks a lot for that! I just realized I was kind of imprecise regarding the mouse position. In your solution the gradient directly reacts to the mouse’s angle towards the gear (which is also a nice mechanic) but I meant the mouse position in relation to the whole viewport. So when the mouse is in the upper left corner the gradient’s angle is 45deg, in the upper right corner of the viewport it is 135deg, in the lower right corner 225deg and in the lower left corner 315deg. – user1706680 Sep 06 '19 at 11:44
  • 1
    Well, all you need is to set `midx = window.innerWidth/2` – Tomáš Zato Sep 06 '19 at 11:50
  • Hi @TomášZato, shouldn't it be `window.innerWidth/rect.width`, as the distances need to be scaled? – shrys Sep 06 '19 at 11:59
  • 1
    Nah. No `rect` needed at all, that was for angle from the svg center. Think about it, the left side of the window is `x=0`, the right one is `x=window.innerWidth` so the middle is `window.innerWidth/2`. Same for height. – Tomáš Zato Sep 06 '19 at 12:02
  • 1
    @shrys Check the updated example to see it work. Note that `innerWidth` is needed, not outer. Otherwise it wouldn't work in iframes and the likes. – Tomáš Zato Sep 06 '19 at 12:05
  • Not that you did not already helped me lots but do you also have a clue about the easing I mentioned in my original post? – user1706680 Sep 06 '19 at 12:23
  • 1
    @user1706680 I'd just extract the code from my answer into some function, then call it from the code copied from the article you linked. Try to play around with it, if it doesn't work, you can ask another question and somebody, maybe me, will help you fix it. But I think you can do it. – Tomáš Zato Sep 06 '19 at 12:30
3

You could use atan2 as follows to get the angle in radians and convert it into degrees by multiplying 180 / Math.PI:

$('body').mousemove(function(e) {
  var {
    left: offsetX,
    top: offsetY
  } = $('svg').offset();
  centerX = $('svg').width() / 2 + offsetX;
  centerY = $('svg').height() / 2 + offsetY;
  mouseX = e.pageX - centerX;
  mouseY = e.pageY - centerY;

  xy = Math.atan2(mouseY, mouseX) * (180 / Math.PI);
  $('svg defs').html('<linearGradient gradientTransform="rotate(' + xy + ', 0.5, 0.5)"  id="gradient" gradientUnits="objectBoundingBox"><stop offset="0.4" stop-color="#33FF8F"/><stop offset="0.6" stop-color="#5A33FF"/></linearGradient>');

});
svg {
  width: 150px;
  height: 150px;
}

body {
  height: 100vw;
  width: 100vw;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<svg x="0px" y="0px" viewBox="0 0 150 150">
  
  <style>
   .st0 { fill:url( #gradient ); }
  </style>

  <defs>
    <linearGradient gradientTransform="rotate( 0, 0.5, 0.5 )" id="gradient" gradientUnits="objectBoundingBox">
   <stop  offset="0.4" style="stop-color:#33FF8F"/>
   <stop  offset="0.6" style="stop-color:#5A33FF"/>
    </linearGradient>
  </defs>
  
  <path class="st0" d="M149.3,89.5l0.7-24.6l-15.7-0.5c-1-5.4-2.8-10.8-5.3-16l12.4-9.7l-15.4-19.3l-12.4,9.7 c-4.4-3.6-9.3-6.6-14.5-8.8L102.2,5L77.9,0l-3.2,15.3c-5.6,0-11.3,0.8-16.8,2.4L50.4,4L28.5,15.6L36,29.4 c-4.4,3.7-8.2,7.9-11.3,12.6L10,36.2L0.8,59.1l14.7,5.8c-1,5.5-1.2,11.2-0.5,16.8L0,86.5l7.8,23.4l15-4.9c2.9,5,6.5,9.4,10.5,13.2 L25,131.5l21.2,12.9l8.3-13.3c5.3,1.9,10.9,3.1,16.6,3.4l2.3,15.4l24.6-3.6L95.7,131c2.6-1,5.2-2.1,7.8-3.5c2.5-1.4,4.9-2.9,7.2-4.5 l11.8,10.4L139,115l-11.8-10.4c2.8-4.9,4.9-10.2,6.3-15.6L149.3,89.5z M90,102.7c-15.4,8.2-34.7,2.5-43.1-12.8 c-8.3-15.3-2.5-34.4,12.9-42.6c15.4-8.2,34.7-2.5,43,12.8C111.2,75.3,105.4,94.4,90,102.7z"/>
</svg>
shrys
  • 5,860
  • 2
  • 21
  • 36
  • Thanks a lot for that! I just realized I was kind of imprecise regarding the mouse position. In your solution the gradient directly reacts to the mouse’s angle towards the gear (which is also a nice mechanic) but I meant the mouse position in relation to the whole viewport. So when the mouse is in the upper left corner the gradient’s angle is 45deg, in the upper right corner of the viewport it is 135deg, in the lower right corner 225deg and in the lower left corner 315deg. – user1706680 Sep 06 '19 at 11:43