Jump to content
Search Community

Mouse move parallax animation conflict with motionPath animation

Yunus Emre test
Moderator Tag

Go to solution Solved by GreenSock,

Recommended Posts

In a React project I have an animation that I apply a swing animation to each element using motionPath plugin and a parallax animation to each of them on mouse move.

After parallax animation is over, there is a slide problem on each element. I assume the swing animation causing this. 

What's wrong with my approach? How can I apply these two animation working well together?

There is a lot going on but I can create codesandbox if needed.

import { useEffect, useRef } from "react";
import gsap from "gsap";
import { MotionPathPlugin } from "gsap/dist/MotionPathPlugin";


export function Swing({ children }) {
  // select wrapper of elements that will be animated
  const wrapperRef = useRef();

  // function to generate integers to use in the animations
  function getRandom(min, max) {
    return Math.floor(Math.random() * (max - min) + min);

  useEffect(() => {
    const wrapper = wrapperRef.current;

    // grab elements
    const elements = gsap.utils.toArray(wrapper.children);

    // 1) swing animation for each element
    elements.forEach((element) => {
      const size = element.dataset.size;
      const randSize = 100 - size;
      gsap.to(element, {
        motionPath: {
          path: [
            { x: -randSize / 1.5, y: 0 },
            { x: -randSize / 1.5, y: -randSize / 2 },
            { x: 0, y: -randSize / 2 },
            { x: 0, y: 0 },
          curviness: 1,
        duration: getRandom(size / 3, size / 2),
        ease: "none",
        repeat: -1,
        delay: getRandom(0, 4),

    // 2) mouse move parallax animation
    const onMouseMove = (event) => {
      const pageX = event.pageX - wrapper.getBoundingClientRect().width * 0.5;
      const pageY = event.pageY - wrapper.getBoundingClientRect().height * 0.5;

      elements.forEach((element) => {
        // element.dataset.size is an int between 30 - 60
        const speedX = 100 - element.dataset.size;
        const speedY = 100 - element.dataset.size;

        gsap.to(element, {
          x: -((element.offsetLeft + pageX * speedX) * 0.005) / 2,
          y: -(element.offsetTop + pageY * speedY) * 0.005,
          ease: "Expo.easeOut",
          duration: 2,
    document.addEventListener("pointermove", onMouseMove);
  }, []);

  return <div ref={wrapperRef}>{children}</div>;


Here is a short video to show the problem:

By the way the parallax effect is not so smooth. Are there any tips to improve the animation performance? I already use "will-change: transform;" on elements and wrapper itself.

Link to comment
Share on other sites

  • Solution

You can't animate "x" and "y" properties on every element to multiple places at the same time :)


You could, however, wrap each element in another <div> and have the pointer parallax animate that one, and the motionPath can animate the child. 


The way you're handling the parallax pointermove thing is extremely inefficient. On every pointermove event (which can fire over 60 times per second) you're creating a whole new tween for every single element, and you're also calling .getBoundingClientRect() twice. Since you didn't set overwrite: true or overwrite: "auto", it also means you're creating a bunch of conflicting tweens that are fighting with each other for control of the same properties of the same targets. You're also getting the offsetTop and offsetLeft on every element on every pointermove event too which is expensive. You're needlessly duplicating the calculation of speed too (speedX and speedY are identical). 


You could pre-calculate a lot of the values, store them in a data object along with a tween for each element that you then invalidate() and restart() after updating the x/y destination values, sorta like this: 

const data = elements.map(el => {
	return {
		left: el.offsetLeft,
		top: el.offsetTop,
		speed: 100 - el.dataset.size,
		tween: gsap.to(el, {x: "+=0", y: "+=0", ease: "expo", duration: 2, paused: true})

const onMouseMove = (event) => {
	let bounds = wrapper.getBoundingClientRect(),
		pageX = event.pageX - bounds.width * 0.5,
		pageY = event.pageY - bounds.height * 0.5;

	data.forEach(d => {
		d.tween.vars.x = -((d.left + pageX * d.speed) * 0.005) / 2;
		d.tween.vars.y = -(d.top + pageY * d.speed) * 0.005;


The upcoming release is going to have a very cool feature that'll make this even easier and faster, so stay tuned for that :)


Side note: GSAP has a gsap.utils.random() method that you can tap into instead of making your own. 


I'm not even sure you need to use MotionPathPlugin - are you just trying to make it go in a circular path? If so, there may be easier algorithms. Like put it in a wrapper <div>, offset the transformOrigin and then rotate it while rotating the inner element in the opposite direction. Just an idea. 


Have fun!

  • Thanks 1
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.
  • Create New...