Jump to content
GreenSock

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

React hooks + complex timeline

Recommended Posts

Right now I unfortunately don't have a codebox/pen with my latest try at it, but hopefully the example will help a bit until I upload the full code. 

 

https://codesandbox.io/s/smoosh-violet-fk6un (this example is pure js, no react)

 

So I have:

- a Master timeline. 

- smaller timelines added to the master timeline

- 2 labels

- I use a .add(fn) with a condition in it to either play the loop again or go to 'end' label. I would like to use this with a (img) preloader. So whenever loading is finished, I want the loop to go to 'end'. 

- end (infinite) loop

 

The problem I faced so far is that the 'loop' variable, which is state in React, somehow doesn't work in the conditional in the .add function. 

 

So I tried hacking this by adding each timeline to their own state, add them to the master and instead of a conditional for 'loop', I add a 'seekloop'. Then set state of 'loop', rerun the effect and this time remove the 'seekloop' and add a different seek.

 

I hope this makes sense ?

 

 

import React, { useEffect, useRef, useState } from "react"
import { TweenMax, TimelineLite, Power3, Linear, Back } from "gsap"

import {
  Wrap,
  WrapText,
  YesWrapper,
  Yes,
  Problem,
  ProblemText,
  ProblemYes,
  BG,
} from "./styles"

const Intro = () => {
  const [{ w, h }, setWindowSize] = useState({
    w: window.innerWidth,
    h: window.innerHeight,
  })
  const [loop, setLoop] = useState(true)
  const [aspect, setAspect] = useState(true)
  const [tlMaster, setAnimation] = useState(null)
  const [tlIntro, setTLIntro] = useState(null)
  const [tlLoop, setTLLoop] = useState(null)
  const [tlEnd, setTLEnd] = useState(null)

  const hyp = Math.sqrt(w * w + h * h)
  const angle = Math.floor(Math.sin(h / hyp) * (180 / Math.PI))
  const bounce = Back.easeOut.config(0.05)

  const refWrap = useRef(null)
  const refWrapText = useRef(null)
  const refYesWrapper = useRef(null)
  const refProblemText = useRef(null)
  const refProblemYes = useRef(null)
  const refBG = useRef(null)

  // initial effect to load and measure
  useEffect(() => {
    function init() {
      setWindowSize({ w: window.innerWidth, h: window.innerHeight })

      TweenMax.set(refProblemText.current, { yPercent: 100 })
      TweenMax.set(refYesWrapper.current, { y: h })
    }

    window.addEventListener("resize", init)
    return () => window.removeEventListener("resize", init)
  }, [h])

  useEffect(() => {
    setAspect(w / h)

    TweenMax.set(refWrapText.current, {
      xPercent: aspect > 1 ? 5 : 35,
      width: h,
    })
  }, [w, h])

  useEffect(() => {
    setAnimation(new TimelineLite({ repeat: 0, paused: true }))
  }, [])

  useEffect(() => {
    if (!tlMaster) return

    setTLIntro(
      new TimelineLite()
        .fromTo(refBG.current, 0.2, { y: 0 }, { y: h / 2 })
        .to(refWrap.current, 0.2, { rotation: -angle })
        .to(refProblemText.current, 0.2, { yPercent: 0, opacity: 1 })
        .staggerFromTo(
          refYesWrapper.current.childNodes,
          0.2,
          { y: -h },
          { opacity: 1, y: 0, ease: bounce },
          -0.1
        )
    )
  }, [tlMaster])

  useEffect(() => {
    if (!tlMaster) return

    setTLLoop(
      new TimelineLite()
        .to(refProblemText.current, 0.2, { x: w * 1.5 })
        .to(refProblemYes.current, 0.2, { yPercent: 100 })
        .staggerTo(
          refYesWrapper.current.childNodes,
          0.2,
          { yPercent: 100, opacity: 1, y: 0, ease: bounce },
          -0.1
        )
        .set(refProblemText.current, { x: 0, yPercent: 100 })
        .to(refProblemText.current, 0.2, { yPercent: 0 })
        .delay(0.3)
    )
  }, [tlMaster])

  useEffect(() => {
    if (!tlMaster) return

    const completedIntro = () => {
      TweenMax.to(refWrapText.current, 2, {
        y: 100,
        ease: Linear.easeNone,
      }).repeat(-1)
      //y:100 = yesHeight
      // dom.html.classList.remove("js--disable-scroll");
    }

    setTLEnd(
      new TimelineLite({ onComplete: completedIntro })
        .addLabel("start")
        .to(refWrap.current, 0.6, { rotation: -90 }, "start")
        .to(
          refWrap.current,
          0.6,
          { xPercent: 100, ease: Power3.easeOut },
          "start"
        )
        .to(
          refWrapText.current,
          0.6,
          { xPercent: aspect > 1 ? "-=5" : "-=35", ease: Power3.easeOut },
          "start"
        )
    )
  }, [tlMaster])

  useEffect(() => {
    if (!tlIntro || !tlLoop || !tlEnd) return

    tlMaster
      .add(tlIntro)
      .addLabel("loop")
      .add(tlLoop)
      .addLabel("end")
      .add(tlEnd)

    tlMaster.play(0)
  }, [tlIntro, tlLoop, tlEnd])

  useEffect(() => {
    if (!tlMaster || !tlLoop) return

    if (loop) {
      console.log("add")
      tlLoop.add(() => tlMaster.seek("loop"), "seekLoop")
      setLoop(false)
    } else {
      console.log("remove")
      tlMaster.removeLabel("seekLoop")
      tlLoop.add(() => tlMaster.seek("end"))
    }
  }, [tlMaster, tlLoop, loop])

  return (
    <Wrap ref={refWrap}>
      <WrapText ref={refWrapText}>
        <YesWrapper ref={refYesWrapper}>
          <Yes>Yes</Yes>
          <Yes>Yes</Yes>
          <Yes>Yes</Yes>
          <Yes>Yes</Yes>
          <Yes>Yes</Yes>
          <Yes>Yes</Yes>
          <Yes>Yes</Yes>
          <Yes>Yes</Yes>
        </YesWrapper>

        <Problem>
          <ProblemYes ref={refProblemYes}>Yes</ProblemYes>
          <ProblemText ref={refProblemText}>problem</ProblemText>
        </Problem>
      </WrapText>

      <BG ref={refBG} />
    </Wrap>
  )
}

export default Intro

 

Link to comment
Share on other sites

Hey flowen,

 

I'm not very good with React but it seems like you're overcomplicating it to me. I would just do something in the infinite timeline's onRepeat to disable the permanent loop and do a new animation instead. In GSAP 3 that would look like this:

See the Pen mddjeey?editors=0010 by GreenSock (@GreenSock) on CodePen

  • Like 2
Link to comment
Share on other sites

@zachSaucier thx for the pen, that's a useful callback. Would you recommend the Beta of 3 over gsap 2 ?

 

ps: I also wondered.. why the chain of seek, pause and kill (and not just kill for example)

Link to comment
Share on other sites

6 minutes ago, flowen said:

Would you recommend the Beta of 3 over gsap 2 ?

GSAP 3 is no longer in beta! It's been soft-released with the full release coming in a few hours. Yes, I highly recommend GSAP 3 as it's great!

 

7 minutes ago, flowen said:

why the chain of seek, pause and kill (and not just kill for example)

I had just kill initially but there was a visual glitch. AFAIK .kill() doesn't actually immediately stop tweens or timelines. It just frees it to be garbage collected. So there could be some time between when you call .kill() and it being garbage collected, in which case the animation could continue partially. You probably just need a .pause().kill() and can drop the .seek(0).

  • Like 1
Link to comment
Share on other sites

27 minutes ago, ZachSaucier said:

GSAP 3 is no longer in beta! It's been soft-released with the full release coming in a few hours. Yes, I highly recommend GSAP 3 as it's great!

 

I had just kill initially but there was a visual glitch. AFAIK .kill() doesn't actually immediately stop tweens or timelines. It just frees it to be garbage collected. So there could be some time between when you call .kill() and it being garbage collected, in which case the animation could continue partially. You probably just need a .pause().kill() and can drop the .seek(0).

Oh, wow, what a timing! I'll have to upgrade of course :) Thanks for mentioning and thanks for the explanation on the chain!

 

 

 

  • Like 2
Link to comment
Share on other sites

I have a different problem now.. I'll keep it in this topic as the topic is still correct.

 

- I have multiple paused timelines, that I add to a master timeline.

- I pause them, otherwise they're being playing during initialisation.. right?

- I start playing the master timeline once all state is set, but nothing happens. I think because the child timelines are still paused... could this be?

 

Due to the nature of useEffect effects, I think I'm supposed to set them up like this: 

- Check if the timeline exists (first time this fails, so we return)

- timeline is being set so we rerun the effect 

- add them with a setter

 

 

 

 

const Intro = () => {
  const [tlIntroMaster, setTlIntroMaster] = useState(null)
  const [tlIntro, setTlIntro] = useState(null)
  const [tlLoop, setTlLoop] = useState(null)

  useEffect(() => {
    setTlIntro(
      gsap
        .timeline({ defaults: { duration: 0.2, repeat: 0 } })
        ...
    )

    setTlLoop(
      // it's important this one repeats
      gsap
        .timeline({ defaults: { duration: 0.25 }, repeat: -1, paused: true })
        ...
    )
  }, [])

  // here I add them to setTlIntroMaster
  useEffect(() => {
    if (!tlLoop || !tlIntro) return

    setTlIntroMaster(
      gsap
        .timeline()
        .add(tlIntro)
        .add(tlLoop)
    )
  }, [tlLoop, tlIntro])

  // this use effect reruns when the tlIntroMaster has a timeline
  // we play it.. but it won't play. 
  // the children are still in a paused state and I can't seem to run them
  useEffect(() => {
    if (!tlIntroMaster) return
    // I log to see if something is there - yes
    console.log(tlIntroMaster)
    tlIntroMaster.play()
  }, [tlIntroMaster])

  return (
    <>
      ...
    </>
  )
}

 

 

Link to comment
Share on other sites

 I agree with Zach in this, I think you're over-complicating things a bit (something we all have done to be honest ;)).

 

You can easily create an empty timeline using useState() and later in the initial render you can add child instances, labels, callbacks, etc. to it, in order to avoid errors:

const [tl] = useState(gsap.timeline({paused: true));
                                     
useEffect(()=>{
  // here add instances to the timeline
}, []);

You're right about adding paused timelines to a parent one. Normally in that scenario I create the child timelines paused and when I add them to the parent I un-pause them:

const masterTl = gsap.timeline({ paused: true });

const childTl = gsap.timeline({ paused: true });

// add child instance
masterTl.add(childTl.paused(false));

// later on your code you only control the parent Timeline

Finally, avoid adding instances to a timeline because of a state update using useEffect, this will keep adding instances and callbacks to your timeline on every state update and at the end you could end up with a lot of unwanted callbacks and instances inside your timeline. As suggested create the timeline at runtime and then, if possible, add all the instances to the timeline on the initial render. Then control the timeline using useEffect.

 

Here is a simple example of creating and controlling a timeline using Hooks:

 

https://stackblitz.com/edit/gsap-react-hooks-timeline-instance-toggle

 

Happy Tweening!!!

  • Like 4
Link to comment
Share on other sites

The unpause trick did it and definitely gonna simplify the useeffect stuff

 

thanks @Rodrigo for helping me out with React (once again) :)

 

 

  • Like 1
Link to comment
Share on other sites

  • 1 year later...

Hey! I'm new to the GreenSock community, and super excited to try out all the cool features!

 

I'm adding my question to this thread because I think it's relevant. Please let me know if this belongs in a new post.

 

Similar to @flowen, I'm trying to animate multiple timelines (introTimeline, occupationTimeline) by nesting them into the master timeline (masterTimeline). However, the resulting page doesn't trigger the animation, instead rendering the default HTML.

 

export default function Home() {
  // Ref for GSAP animations
  const hiText = useRef();
  const iAmAText = useRef();
  const cursor = useRef();
  const occupation = useRef();

  const masterTimeline = useRef(gsap.timeline({ paused: true }));
  const introTimeline = useRef(gsap.timeline({ paused: true }));
  const occupationTimeline = useRef(gsap.timeline({ paused: true }));

  introTimeline.current
    .from(hiText.current, {
      y: "+=10",
      opacity: 0,
      duration: 1.5,
      ease: "power4.easeInOut",
    })
    .from(iAmAText.current, {
      y: "+=10",
      opacity: 0,
      duration: 1.5,
      ease: "power4.easeInOut",
    })
    .to(cursor.current, {
      opacity: 0,
      ease: "power0",
      repeat: -1,
      yoyo: true,
      repeatDelay: 0.5,
    });

  occupationTimeline.current
    .add(typeOccupation(occupation.current, "a CS Major", 1))
    .add(typeOccupation(occupation.current, "a Software Developer", 1))
    .add(typeOccupation(occupation.current, "a NLP Researcher", 1))
    .add(typeOccupation(occupation.current, "a Computer Science Tutor", 1))
    .repeat();
  
  masterTimeline
    .current.add(introTimeline.current.paused(false))
	.add(occupationTimeline.current.paused(false));

  // Wait until DOM has been rendered
  useLayoutEffect(() => {
    masterTimeline.current.paused(false);
  }, []); 
  
  return (
    <div className="home" id="home">
      <div className="landingText">
        <h1 ref={hiText}>Hi, I'm Seunggun</h1>
        <div ref={iAmAText} id="wrapper">
          <span>I am&nbsp;</span>
          <span ref={occupation}></span>
          <span ref={cursor}>|</span>
        </div>
      </div>
    </div>
  );
}

I tried creating my timeline with (paused: true), as @Rodrigo suggested, but that doesn't seem to be the only problem. Am I creating / adding my timelines in the wrong place? Any help would be appreciated. Thanks!

 

Link to comment
Share on other sites

Welcome to forums @seungguini

 

You should always create your timelines inside a hook. 

 

// only need to use a ref if you are tring to access 
// the master timeline elsewhere
const masterTimeline = useRef();

useLayoutEffect(() => {
  const introTimeline = gsap.timeline()
    .from(hiText.current, {
      y: "+=10",
      opacity: 0,
      duration: 1.5,
      ease: "power4.easeInOut",
    })
    .from(iAmAText.current, {
      y: "+=10",
      opacity: 0,
      duration: 1.5,
      ease: "power4.easeInOut",
    })
    .to(cursor.current, {
      opacity: 0,
      ease: "power0",
      repeat: -1,
      yoyo: true,
      repeatDelay: 0.5,
    });

  const occupationTimeline = gsap.timeline()
    .add(typeOccupation(occupation.current, "a CS Major", 1))
    .add(typeOccupation(occupation.current, "a Software Developer", 1))
    .add(typeOccupation(occupation.current, "a NLP Researcher", 1))
    .add(typeOccupation(occupation.current, "a Computer Science Tutor", 1))
    .repeat();
  
  masterTimeline.current = gsap.timeline({ paused: true })
    .add(introTimeline)
    .add(occupationTimeline);
  
  return () => masterTimeline.current.kill()
}, [])

 

Be sure to check out our React Guide for more details.

 

  • Like 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.
×