Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...
joshahayes

Animating through a lot of SVG elements on hover without any performance-loss

Recommended Posts

Hey again, sorry for what is most likely another rookie-mistake question but

I've got this SVG map made up out of hexagons and I am trying to implement a hover interaction wherein as the mouse moves over the map, the hexagons underneath change in scale depending on the distance between it and the mouse. 

 

I created this function here which goes through the array of SVG polygons, and on hover, checks the distance between it and the mouse and if below a certain number, maps that polygon's distance to a new number between 0.2 and 1.2 which I then just use on a css transform property for the scale. This codepen probably better demonstrates this: 

function hexagonHover() {
    for (i = 0; i < poly.length; i++) {

        poly[i].addEventListener('mouseover', function (event) {

            for (i = 0; i < poly.length; i++) {

                if (polyDist[i] <= 60) {

                    let v = mapRange(polyDist[i], 0, 60, 1.2, 0.2);

                    poly[i].style.transform =
                        "scale(" + v + ")";
                    poly[i].style.transformOrigin =
                        "center";
                    poly[i].style.transformBox = "fill-box";
                    poly[i].style.transition = "transform ease .6s";
                  
                } else {
                  
                    poly[i].style.transform = "scale(1.0)";

                }

            }

        });
    }
}

It works nicely for that example, but in my actual design the map is a lot larger and there are quite a few polygons to go through. It lags a lot, especially on lower-end systems so I'm pretty much trying to find a more optimal way of animating this interaction. I have a feeling most of the reduced-performance is caused by changing the css variables of each polygon in every second through this script so I tried using gsap for this.

 

Here is the GSAP version which still works, but for some reason has even worst performance than before. 

function polygonHover() {
    map.addEventListener('mouseover', function () {
        for (i = 0; i < poly.length; i++) {
            if (polyDist[i] < 60) {

                let v = mapRange(polyDist[i], 0, 60, 1.2, 0.2);

                gsap.to(poly[i], {
                    duration: .5,
                    ease: "power2.inOut",
                    transformOrigin: "50% 50%",
                    scale: v
                });

            } else {

                gsap.to(poly[i], {
                    scale: 1.0
                });

            }

        }
    });
}

 

I'm not sure what to do anymore. 

See the Pen oNwJwZR by joshahayes01 (@joshahayes01) on CodePen

  • Like 1
Link to comment
Share on other sites

Hey @joshahayes

I'll preface this with 'opinion' as maybe someone has a trick up their sleeve for you?

It's not going to be the GSAP tween itself causing perf issues. It's likely that you're just playing around right at the boundaries of what SVG is capable of.

You'd probably have better luck with canvas, although that comes with it's own challenges, mainly in setting up the shapes in the first place and accessibility.
 

  • Like 2
  • Thanks 1
Link to comment
Share on other sites

Hi Josh!

 

Performance is very complicated topic as every situation is unique, so I don't have a silver bullet for you. Just from cursory glance at your code, here are some things that might help.

 

First, maybe SVG isn't the best medium to use. SVG is slower than HTML, which is slower than canvas, which is slower than WebGL. If I have to animate hundreds of objects, I'm probably going to reach for canvas or WebGL first. PixiJS is good 2D WebGL renderer.

 

You're also doing way too much work in your mouseover handler, and mousemove might be a better event to use. Notice in this demo how most of the work is done in the ticker function. The mouse handler just records where the mouse position is.

 

See the Pen WNNNBpo by GreenSock (@GreenSock) on CodePen

 

Also in your mouse handler, you're creating 692 tweens, which is going to kill performance. You should only create a tween if you need to animate it. This might be easier if you made an object for every one of your polygons. 

 

I like to use class objects for this kind of stuff. Some pseudo code.

 

const mapRange = gsap.utils.mapRange(0, 60, 1.2, 0.2);

class Polygon {
  constructor(element) {
    this.element = element;
    this.getProp = gsap.getProperty(element);
    gsap.set(element, {
      transformOrigin: "50% 50%"
    });
  }
  
  update(mouse) {
    
    const dist = ...;
    if (dist < 60) {
      gsap.to(this.element, {
        scale: mapRange(dist)
      });
    } else if (this.getProp("scale") < 1) {
      gsap.to(this.element, {
        scale: 1
      });
    }
  }
}

const mouse = { x: 100, y: 100 };
const polys = gsap.utils.toArray("polygon").map(el => new Polygon(el));

gsap.ticker.add(() => {
  for (let i = 0; i < polys.length; i++) {
    polys[i].update(mouse);
  }
});

 

Further optimization might involve only calculating if the element is within a set radius of the mouse. Using Math.sqrt() can be an expensive operation if you have to do a bunch of them. You can easily eliminate elements that are outside of the radius like this.

 

// assumes x and y are the center of the element
const dx = Math.abs(mouse.x - this.getProp("x"));
const dy = Math.abs(mouse.y - this.getProp("y"));

if (dx > 60 || dy > 60) {
  // not inside 
  // check if we need scale to 1 animation needed
  
  if (this.getProp("scale") < 1) {
    gsap.to(this.element, {
      scale: 1
    });
  }
  
} else {
  // calculate distance
  const dist = Math.sqrt(dx * dx + dy * dy);
  
  if (dist > 60  && this.getProp("scale") < 1) {
    gsap.to(this.element, {
      scale: 1
    });
  } else {
    gsap.to(this.element, {
      scale: mapRange(dist)
    });
  }
}

 

You're also creating a mouse event for each polygon. It would be better to just have single event listener, and then figure out if the mouse is near a polygon. Yes, it requires a lot more work, but is much more optimized. Highly optimized animations will almost always require a lot more code.

 

  • Like 4
  • Thanks 2
Link to comment
Share on other sites

Cheers guys! This is all really helpful, will give the above methods a shot ;)

Link to comment
Share on other sites

Hey @OSUblake

 

I've created a polygon object and I've included the constructor and update() function. I've noticed though that looping through all the polygon objects in the ticker function seems to be causing a lot of lag. For instance, console.log tells me that there are 2514 polygons within the polys array which seems like a lot to loop through every second. I haven't included anything else in the update() function of each object, just a console.log so I'm not sure if that array might be too much to loop through. Here's what my code looks like: 

class Polygon {
    constructor(element) {
        this.element = element;
        this.getProp = gsap.getProperty(element);
        gsap.set(element, {
            transformOrigin: "50% 50%"
        });
    }

    update() {
        console.log("hello");
    }
}

const mousePos = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
const polys = gsap.utils.toArray("polygon").map(el => new Polygon(el));

gsap.ticker.add(() => {
    for (let i = 0; i < polys.length; i++) {
        polys[i].update();
    }
})

I've included an image of the SVG map that I'm using. Is my file just too large to use in this case? 

Capture1.PNG

Link to comment
Share on other sites

You can check if the mouse has moved before going through the loop.

 

let isDirty = true;

// mouse handler
function onMouseMove(event) {
  ...
  isDirty = true;
}

gsap.ticker.add(() => {
  
  if (!isDirty) {
    return;
  }
  
  isDirty = false;
  
  // loop
});

 

But I can't say how much that will help as managing 2500+ objects is a lot, and going way beyond the scope of this forum. With that many objects, you may need to start looking at advanced algorithms like quadtrees or spatial hashes to reduce the number of objects that are checked within the loop.

 

And from my personal experience, rendering 2500 objects is way beyond the limits of SVG and most likely even canvas for 60+ FPS animations. 

 

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×