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

 

Share this post


Link to post
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

Share this post


Link to post
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)

Share this post


Link to post
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

Share this post


Link to post
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

Share this post


Link to post
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 (
    <>
      ...
    </>
  )
}

 

 

Share this post


Link to post
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

Share this post


Link to post
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

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...

  • Recently Browsing   0 members

    No registered users viewing this page.

×