Skip to content

GSAP Animations

animation.ts

ts
type AnimationFunction = (
  timeline: gsap.core.Timeline,
  element: HTMLElement | null,
  options?: AnimationOptions
) => gsap.core.Timeline;

type AnimationOptions = {
  duration?: number;
  ease?: string;
  position?: number | string;
  y0?: string;
  scale0?: number;
  lineHeight?: number;
};

const DEFAULT_DURATION = 0.75;
const DEFAULT_POSITION = "+=0";
const DEFAULT_EASE_FADE = "circ.inOut";
const DEFAULT_EASE_SLIDE = "power1.out";
const DEFAULT_EASE_ZOOM = "power3.inOut";

export const fadeIn: AnimationFunction = (timeline, element, options = {}) => {
  const {
    duration = DEFAULT_DURATION,
    ease = DEFAULT_EASE_FADE,
    position = "<50%",
  } = options;

  if (element) {
    timeline.fromTo(
      element,
      { opacity: 0 },
      { opacity: 1, duration, ease },
      position
    );
  }

  return timeline;
};

export const fadeOut: AnimationFunction = (timeline, element, options = {}) => {
  const {
    duration = DEFAULT_DURATION,
    ease = DEFAULT_EASE_FADE,
    position = "<+50%",
  } = options;
  if (element) {
    timeline.to(element, { opacity: 0, duration, ease }, position);
  }
  return timeline;
};

export const slideUp: AnimationFunction = (timeline, element, options = {}) => {
  const {
    duration = DEFAULT_DURATION,
    ease = DEFAULT_EASE_SLIDE,
    y0 = "50%",
    position = "<50%",
    lineHeight = null,
  } = options;
  if (element) {
    if (lineHeight) {
      timeline.fromTo(
        element,
        { y: y0, opacity: 0, lineHeight: `${lineHeight * 1.5}px` },
        { y: "0%", opacity: 1, lineHeight: `${lineHeight}px`, duration, ease },
        position
      );
    } else {
      timeline.fromTo(
        element,
        { y: y0, opacity: 0 },
        { y: "0%", opacity: 1, duration, ease },
        position
      );
    }
  }
  return timeline;
};

export const slideDown: AnimationFunction = (
  timeline,
  element,
  options = {}
) => {
  const {
    duration = DEFAULT_DURATION,
    ease = DEFAULT_EASE_SLIDE,
    y0 = "100%",
    position = "<+50%",
  } = options;
  if (element) {
    timeline.to(element, { y: y0, opacity: 0, duration, ease }, position);
  }
  return timeline;
};

export const zoomIn: AnimationFunction = (timeline, element, options = {}) => {
  const {
    duration = DEFAULT_DURATION,
    ease = DEFAULT_EASE_ZOOM,
    position = DEFAULT_POSITION,
    scale0 = 0,
  } = options;
  if (element) {
    timeline.fromTo(
      element,
      { scale: scale0, opacity: 0 },
      { scale: 1, opacity: 1, duration, ease },
      position
    );
  }
  return timeline;
};

export const zoomOut: AnimationFunction = (timeline, element, options = {}) => {
  const {
    duration = DEFAULT_DURATION,
    ease = DEFAULT_EASE_ZOOM,
    position = DEFAULT_POSITION,
  } = options;
  if (element) {
    timeline.to(element, { scale: 0, opacity: 0, duration, ease }, position);
  }
  return timeline;
};

export const zoomInWobble: AnimationFunction = (
  timeline,
  element,
  options = {}
) => {
  return zoomIn(timeline, element, {
    ...options,
    duration: 1.5,
    ease: "elastic.out",
  });
};

export const zoomOutWobble: AnimationFunction = (
  timeline,
  element,
  options = {}
) => {
  return zoomOut(timeline, element, {
    ...options,
    duration: 1.5,
    ease: "elastic.out",
  });
};

type FadeSwapFunction = (
  timeline: gsap.core.Timeline,
  element1: HTMLElement | null,
  element2: HTMLElement | null,
  display?: string,
  position?: number | string
) => gsap.core.Timeline;

export const fadeSwap: FadeSwapFunction = (
  timeline,
  element1,
  element2,
  display = "flex",
  position = DEFAULT_POSITION
) => {
  if (element1 && element2) {
    timeline.to(element1, {
      opacity: 0,
      duration: DEFAULT_DURATION,
      onComplete: () => {
        element1.style.display = "none";
        element2.style.display = display;
      },
      position,
    });
  }
  return timeline;
};

type PlayVideoFunction = (
  timeline: gsap.core.Timeline,
  element: React.RefObject<HTMLVideoElement> | null,
  playRange?: [number, number],
  options?: AnimationOptions
) => gsap.core.Timeline;

export const playVideo: PlayVideoFunction = (
  timeline,
  element,
  playRange = [0, 10],
  options = {}
) => {
  const {
    duration = playRange[1] - playRange[0],
    position = DEFAULT_POSITION,
  } = options;
  const video = element?.current;

  if (video) {
    timeline.to(
      video,
      {
        duration,
        onStart: () => {
          video.currentTime = playRange[0];
          video.play().catch(() => {
            console.warn("Failed to play video");
          });
        },
        onComplete: () => video.pause(),
      },
      position
    );
  }

  return timeline;
};

Implementation Example

tsx
import React from "react";
import {
  slideUp,
  playVideo,
  zoomIn,
  fadeIn,
  fadeOut,
} from ".../animation.ts";

const AnimationExample: React.FC = () => {
  const animateIn = () => {
    const tl = gsap.timeline({ paused: true });
    slideUp(tl, refs.background);
    playVideo(tl, video, [0.5, 5]);
    zoomIn(tl, refs.hero, { duration: 1, position: ">-=1" });
    slideUp(tl, refs.content);
    fadeIn(tl, refs.button, { position: 2 });
    return tl;
  };

  const animateOut = () => {
    const tl = gsap.timeline({ paused: true });
    slideDown(tl, refs.background);
    return tl;
  };

  useEffect(() => {
    animateIn().play();
  }, []);

  return (
    <>
      ...
      <button
        ref={setRef('button')}
        onClick={() => animateOut().play()}
      >
        Animate Out
      </button>
    </>
  );
};