skills/top-design/references/animation-patterns.md

14 KiB

Animation Patterns Reference

Table of Contents

  1. Easing Functions
  2. Page Load Sequences
  3. Scroll-Triggered Animations
  4. Micro-Interactions
  5. Page Transitions
  6. Text Animations
  7. Performance Optimization

1. Easing Functions

CSS Custom Easings (Agency Standards)

:root {
  /* Smooth exits - most common for UI */
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
  --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-out-circ: cubic-bezier(0, 0.55, 0.45, 1);
  
  /* Dramatic entrances */
  --ease-in-expo: cubic-bezier(0.7, 0, 0.84, 0);
  --ease-in-quart: cubic-bezier(0.5, 0, 0.75, 0);
  
  /* Symmetric - for toggles, loops */
  --ease-in-out-quint: cubic-bezier(0.83, 0, 0.17, 1);
  --ease-in-out-expo: cubic-bezier(0.87, 0, 0.13, 1);
  
  /* Bouncy/Elastic */
  --ease-out-back: cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-out-elastic: cubic-bezier(0.68, -0.6, 0.32, 1.6);
  
  /* Duration standards */
  --duration-fast: 150ms;
  --duration-normal: 300ms;
  --duration-slow: 500ms;
  --duration-slower: 800ms;
  --duration-slowest: 1200ms;
}

GSAP Equivalent

// Common GSAP easings
gsap.to(element, {
  duration: 1,
  ease: "expo.out"      // Most used for reveals
});

gsap.to(element, {
  duration: 0.8,
  ease: "power4.out"    // Slightly faster settle
});

gsap.to(element, {
  duration: 1.2,
  ease: "power2.inOut"  // Symmetric movement
});

// Custom ease (Locomotive-style)
gsap.registerPlugin(CustomEase);
CustomEase.create("smooth", "0.16, 1, 0.3, 1");

2. Page Load Sequences

Staggered Reveal Pattern

/* CSS-only staggered reveal */
.reveal-item {
  opacity: 0;
  transform: translateY(60px);
  animation: revealUp 0.8s var(--ease-out-expo) forwards;
}

.reveal-item:nth-child(1) { animation-delay: 0.1s; }
.reveal-item:nth-child(2) { animation-delay: 0.2s; }
.reveal-item:nth-child(3) { animation-delay: 0.3s; }
.reveal-item:nth-child(4) { animation-delay: 0.4s; }

@keyframes revealUp {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

GSAP Page Load

// Orchestrated page load
const tl = gsap.timeline({ defaults: { ease: "expo.out" } });

tl.set(".page-content", { visibility: "visible" })
  .from(".hero-title .word", {
    y: 120,
    opacity: 0,
    duration: 1.2,
    stagger: 0.1
  })
  .from(".hero-subtitle", {
    y: 40,
    opacity: 0,
    duration: 0.8
  }, "-=0.6")
  .from(".hero-cta", {
    y: 30,
    opacity: 0,
    duration: 0.6
  }, "-=0.4")
  .from(".nav-item", {
    y: -20,
    opacity: 0,
    duration: 0.5,
    stagger: 0.05
  }, "-=0.8");

Split Text Animation

// Using GSAP SplitText or custom splitting
import { SplitText } from "gsap/SplitText";

const split = new SplitText(".hero-title", { 
  type: "words,chars",
  wordsClass: "word",
  charsClass: "char"
});

gsap.from(split.chars, {
  y: 100,
  opacity: 0,
  duration: 0.8,
  ease: "expo.out",
  stagger: 0.02
});

// CSS approach (manual splitting)
/* HTML: <span class="char">H</span><span class="char">e</span>... */
.char {
  display: inline-block;
  opacity: 0;
  transform: translateY(100%);
  animation: charReveal 0.6s var(--ease-out-expo) forwards;
}

.char:nth-child(1) { animation-delay: calc(0.03s * 1); }
.char:nth-child(2) { animation-delay: calc(0.03s * 2); }
/* ... generate with CSS custom properties or JS */

3. Scroll-Triggered Animations

Intersection Observer (Vanilla)

// Reusable scroll reveal
const observerOptions = {
  root: null,
  rootMargin: '0px 0px -100px 0px',
  threshold: 0.1
};

const revealOnScroll = (entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
      observer.unobserve(entry.target);
    }
  });
};

const observer = new IntersectionObserver(revealOnScroll, observerOptions);

document.querySelectorAll('[data-reveal]').forEach(el => {
  observer.observe(el);
});
/* Accompanying CSS */
[data-reveal] {
  opacity: 0;
  transform: translateY(60px);
  transition: opacity 0.8s var(--ease-out-expo),
              transform 0.8s var(--ease-out-expo);
}

[data-reveal].is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* Stagger children */
[data-reveal="stagger"] > * {
  opacity: 0;
  transform: translateY(40px);
  transition: opacity 0.6s var(--ease-out-expo),
              transform 0.6s var(--ease-out-expo);
}

[data-reveal="stagger"].is-visible > *:nth-child(1) { transition-delay: 0.1s; }
[data-reveal="stagger"].is-visible > *:nth-child(2) { transition-delay: 0.2s; }
[data-reveal="stagger"].is-visible > *:nth-child(3) { transition-delay: 0.3s; }
[data-reveal="stagger"].is-visible > * {
  opacity: 1;
  transform: translateY(0);
}

GSAP ScrollTrigger

import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

// Fade up on scroll
gsap.utils.toArray('.reveal').forEach(el => {
  gsap.from(el, {
    y: 60,
    opacity: 0,
    duration: 1,
    ease: "expo.out",
    scrollTrigger: {
      trigger: el,
      start: "top 85%",
      toggleActions: "play none none none"
    }
  });
});

// Parallax effect
gsap.to('.parallax-bg', {
  yPercent: -30,
  ease: "none",
  scrollTrigger: {
    trigger: '.parallax-section',
    start: "top bottom",
    end: "bottom top",
    scrub: true
  }
});

// Horizontal scroll section
gsap.to('.horizontal-panels', {
  xPercent: -100 * (panels.length - 1),
  ease: "none",
  scrollTrigger: {
    trigger: '.horizontal-container',
    pin: true,
    scrub: 1,
    end: () => "+=" + document.querySelector('.horizontal-panels').offsetWidth
  }
});

// Pin and reveal
gsap.timeline({
  scrollTrigger: {
    trigger: '.pinned-section',
    start: "top top",
    end: "+=200%",
    pin: true,
    scrub: 1
  }
})
.from('.pinned-title', { opacity: 0, y: 100 })
.from('.pinned-content', { opacity: 0, y: 50 }, 0.3);

Lenis Smooth Scroll + GSAP

import Lenis from '@studio-freight/lenis';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

// Initialize Lenis
const lenis = new Lenis({
  duration: 1.2,
  easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
  direction: 'vertical',
  smooth: true,
});

// Connect Lenis to GSAP ScrollTrigger
lenis.on('scroll', ScrollTrigger.update);

gsap.ticker.add((time) => {
  lenis.raf(time * 1000);
});

gsap.ticker.lagSmoothing(0);

4. Micro-Interactions

Button Hover States

/* Magnetic button effect */
.magnetic-btn {
  position: relative;
  transition: transform 0.3s var(--ease-out-expo);
}

/* JS handles the magnetic effect, CSS handles the visual */
.magnetic-btn::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--color-accent);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.5s var(--ease-out-expo);
}

.magnetic-btn:hover::before {
  transform: scaleX(1);
  transform-origin: left;
}

/* Text slide effect */
.text-slide-btn {
  overflow: hidden;
  position: relative;
}

.text-slide-btn span {
  display: block;
  transition: transform 0.5s var(--ease-out-expo);
}

.text-slide-btn span::after {
  content: attr(data-text);
  position: absolute;
  top: 100%;
  left: 0;
}

.text-slide-btn:hover span {
  transform: translateY(-100%);
}
// Magnetic button JS
document.querySelectorAll('.magnetic-btn').forEach(btn => {
  btn.addEventListener('mousemove', (e) => {
    const rect = btn.getBoundingClientRect();
    const x = e.clientX - rect.left - rect.width / 2;
    const y = e.clientY - rect.top - rect.height / 2;
    
    gsap.to(btn, {
      x: x * 0.3,
      y: y * 0.3,
      duration: 0.3,
      ease: "power2.out"
    });
  });
  
  btn.addEventListener('mouseleave', () => {
    gsap.to(btn, {
      x: 0,
      y: 0,
      duration: 0.5,
      ease: "elastic.out(1, 0.5)"
    });
  });
});
/* Slide in underline */
.link-underline {
  position: relative;
}

.link-underline::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.4s var(--ease-out-expo);
}

.link-underline:hover::after {
  transform: scaleX(1);
  transform-origin: left;
}

/* Draw underline */
.link-draw {
  background-image: linear-gradient(currentColor, currentColor);
  background-position: 0 100%;
  background-repeat: no-repeat;
  background-size: 0% 1px;
  transition: background-size 0.4s var(--ease-out-expo);
}

.link-draw:hover {
  background-size: 100% 1px;
}

Card Hover Effects

/* 3D tilt card */
.tilt-card {
  transform-style: preserve-3d;
  transition: transform 0.5s var(--ease-out-expo);
}

.tilt-card:hover {
  transform: perspective(1000px) rotateX(5deg) rotateY(-5deg);
}

.tilt-card::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    135deg,
    rgba(255,255,255,0.1) 0%,
    transparent 50%
  );
  opacity: 0;
  transition: opacity 0.3s;
}

.tilt-card:hover::before {
  opacity: 1;
}

5. Page Transitions

Basic Fade Transition

// Using Barba.js
import barba from '@barba/core';

barba.init({
  transitions: [{
    name: 'fade',
    leave(data) {
      return gsap.to(data.current.container, {
        opacity: 0,
        duration: 0.5,
        ease: "power2.inOut"
      });
    },
    enter(data) {
      return gsap.from(data.next.container, {
        opacity: 0,
        duration: 0.5,
        ease: "power2.inOut"
      });
    }
  }]
});

Overlay Wipe Transition

// Wipe transition with overlay
barba.init({
  transitions: [{
    name: 'wipe',
    leave(data) {
      const tl = gsap.timeline();
      
      tl.to('.transition-overlay', {
        scaleY: 1,
        transformOrigin: 'bottom',
        duration: 0.6,
        ease: "expo.inOut"
      })
      .to(data.current.container, {
        opacity: 0,
        duration: 0.3
      }, 0.3);
      
      return tl;
    },
    enter(data) {
      const tl = gsap.timeline();
      
      tl.set(data.next.container, { opacity: 1 })
        .to('.transition-overlay', {
          scaleY: 0,
          transformOrigin: 'top',
          duration: 0.6,
          ease: "expo.inOut"
        })
        .from('[data-reveal]', {
          y: 60,
          opacity: 0,
          stagger: 0.1,
          duration: 0.8,
          ease: "expo.out"
        }, 0.2);
      
      return tl;
    }
  }]
});

6. Text Animations

Word-by-Word Reveal

// Split and animate
function animateText(element) {
  const text = element.textContent;
  const words = text.split(' ');
  
  element.innerHTML = words.map(word => 
    `<span class="word-wrapper"><span class="word">${word}</span></span>`
  ).join(' ');
  
  gsap.from(element.querySelectorAll('.word'), {
    y: '100%',
    duration: 0.8,
    ease: "expo.out",
    stagger: 0.05
  });
}

Scramble/Decode Effect

// Text scramble effect
class TextScramble {
  constructor(el) {
    this.el = el;
    this.chars = '!<>-_\\/[]{}—=+*^?#________';
    this.update = this.update.bind(this);
  }
  
  setText(newText) {
    const oldText = this.el.innerText;
    const length = Math.max(oldText.length, newText.length);
    const promise = new Promise(resolve => this.resolve = resolve);
    this.queue = [];
    
    for (let i = 0; i < length; i++) {
      const from = oldText[i] || '';
      const to = newText[i] || '';
      const start = Math.floor(Math.random() * 40);
      const end = start + Math.floor(Math.random() * 40);
      this.queue.push({ from, to, start, end });
    }
    
    cancelAnimationFrame(this.frameRequest);
    this.frame = 0;
    this.update();
    return promise;
  }
  
  update() {
    let output = '';
    let complete = 0;
    
    for (let i = 0; i < this.queue.length; i++) {
      let { from, to, start, end, char } = this.queue[i];
      
      if (this.frame >= end) {
        complete++;
        output += to;
      } else if (this.frame >= start) {
        if (!char || Math.random() < 0.28) {
          char = this.chars[Math.floor(Math.random() * this.chars.length)];
          this.queue[i].char = char;
        }
        output += char;
      } else {
        output += from;
      }
    }
    
    this.el.innerText = output;
    
    if (complete === this.queue.length) {
      this.resolve();
    } else {
      this.frameRequest = requestAnimationFrame(this.update);
      this.frame++;
    }
  }
}

7. Performance Optimization

GPU-Accelerated Properties

/* ONLY animate these for 60fps */
.performant-animation {
  /* Good - GPU accelerated */
  transform: translateX(100px);
  transform: translateY(100px);
  transform: scale(1.2);
  transform: rotate(45deg);
  opacity: 0.5;
  
  /* Bad - triggers layout/paint */
  /* left: 100px; */
  /* top: 100px; */
  /* width: 200px; */
  /* height: 200px; */
  /* margin: 20px; */
  /* padding: 20px; */
}

/* Force GPU layer */
.will-animate {
  will-change: transform, opacity;
  transform: translateZ(0);
}

/* Remove will-change after animation */
.animation-complete {
  will-change: auto;
}

Reduce Motion Support

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
// JS check for reduced motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');

if (prefersReducedMotion.matches) {
  // Skip or simplify animations
  gsap.globalTimeline.timeScale(10); // Or skip entirely
}

Lazy Animation Loading

// Only initialize animations when needed
const animationObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      initHeavyAnimation(entry.target);
      animationObserver.unobserve(entry.target);
    }
  });
}, { rootMargin: '200px' });

document.querySelectorAll('[data-heavy-animation]').forEach(el => {
  animationObserver.observe(el);
});