360 Secure SkillHub
360 安全技能中心

animation-designer

围绕有目的的网页动效,说明性能约束(仅动画 transform 与 opacity、will-change 管理、减少动态偏好)、设计原则(缓动、编排、空间一致性),并给出 CSS、Framer Motion、GSAP、Three.js/R3F 与 Lottie 的代码示例和选型建议。

分类:开发
avinashchby
热度 0.00
最近更新 2026-11-01 23:37:46

安装

发给 Agent
帮我安装这个技能:npx secure-skills-cli add avinashchby/animation-designer/animation-designer
兼容性
通用兼容
联网要求
不联网
数据出网
数据不出本机
Metadata
name
animation-designer
description
Create performant, intentional web animations using CSS (keyframes, transitions, scroll-driven animations, view transitions API), Framer Motion (layout animations, AnimatePresence, gestures, scroll-triggered), GSAP (timelines, ScrollTrigger, morphing, text effects), Three.js / React Three Fiber (3D animations, camera, particles), and Lottie. Covers page transitions, staggered lists, scroll reveals, parallax, hover micro-interactions, loading skeletons, number counters, text animations (typewriter, split-letter, gradient), image reveals, cursor-following effects, and smooth scrolling. Enforces performance rules (transform/opacity only, will-change discipline, prefers-reduced-motion, GPU compositing) and animation design principles (easing over duration, choreography, purposeful motion).

Animation Designer

Core Principle

Every animation must answer: what purpose does this serve? Valid purposes are:

  1. Guide attention — direct the user's eye to what matters next.
  2. Provide feedback — confirm an action was received (button press, form submit).
  3. Establish continuity — connect two states so spatial relationships stay clear.
  4. Communicate hierarchy — staggered entrances show parent-child relationships.

If an animation serves none of these, remove it. Motion without purpose is noise.

Performance Rules

Follow these rules on every animation. Violations cause layout thrashing, dropped frames, and battery drain.

Only Animate Composite Properties

Animate transform and opacity exclusively. These run on the GPU compositor thread and skip layout/paint. Never animate width, height, top, left, margin, or padding — they trigger full layout recalculation.

/* CORRECT — compositor only */
.card-enter {
  transform: translateY(20px) scale(0.98);
  opacity: 0;
  transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease-out;
}

/* WRONG — triggers layout on every frame */
.card-enter {
  margin-top: 20px;
  height: 0;
  transition: margin-top 0.4s, height 0.4s;
}

will-change Discipline

Apply will-change only to elements about to animate, and remove it after. Permanent will-change on many elements wastes GPU memory and can degrade performance.

element.addEventListener('mouseenter', () => {
  element.style.willChange = 'transform';
});
element.addEventListener('transitionend', () => {
  element.style.willChange = 'auto';
});

Never set will-change on more than ~10 elements simultaneously. Never put will-change: transform in a global stylesheet applied to hundreds of elements.

Respect prefers-reduced-motion

Wrap all non-essential animations in a reduced-motion check. This is a hard requirement, not optional polish.

@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 runtime check
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

// Framer Motion
<motion.div
  animate={{ opacity: 1, y: prefersReduced ? 0 : 20 }}
  transition={prefersReduced ? { duration: 0 } : { duration: 0.4 }}
/>

requestAnimationFrame for Custom Animations

Never use setInterval or setTimeout for frame-by-frame animation. Use requestAnimationFrame to sync with the browser's paint cycle.

function animateValue(start: number, end: number, duration: number, onUpdate: (v: number) => void) {
  const startTime = performance.now();
  function tick(now: number) {
    const elapsed = now - startTime;
    const progress = Math.min(elapsed / duration, 1);
    const eased = 1 - Math.pow(1 - progress, 3); // cubic ease-out
    onUpdate(start + (end - start) * eased);
    if (progress < 1) requestAnimationFrame(tick);
  }
  requestAnimationFrame(tick);
}

Design Principles

Easing Over Duration

Duration matters less than easing curve. A 300ms animation with the right easing feels faster than a 200ms linear animation. Default curves for common actions:

Action Easing CSS Value Why
Enter/appear Decelerate cubic-bezier(0.0, 0.0, 0.2, 1) Elements arrive and settle into place
Exit/disappear Accelerate cubic-bezier(0.4, 0.0, 1, 1) Elements pick up speed as they leave
Move/resize Standard cubic-bezier(0.4, 0.0, 0.2, 1) Balanced for repositioning
Bounce/playful Spring cubic-bezier(0.34, 1.56, 0.64, 1) Overshoots target then settles
Micro-interaction Snap cubic-bezier(0.16, 1, 0.3, 1) Quick start, gentle land

Duration guidelines: micro-interactions 100-200ms, standard transitions 200-400ms, complex choreography 400-800ms. Never exceed 1s for UI animations.

Choreography

When multiple elements animate, stagger them 50-80ms apart in reading order (top-to-bottom, left-to-right). This creates a cascade that communicates hierarchy. Never animate everything simultaneously — it looks like a glitch.

Spatial Consistency

Elements should enter from where they logically originate. A dropdown enters from the top. A sidebar slides from the side. A modal scales from its trigger button. Breaking spatial logic confuses users about where elements "live" in the interface.

CSS Animations

@keyframes for Repeating or Multi-Step Animations

Use @keyframes when an animation has more than two states or loops. Use transition for simple A-to-B state changes.

@keyframes shimmer {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

.skeleton {
  position: relative;
  overflow: hidden;
  background: #e2e8f0;
}
.skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
  animation: shimmer 1.5s infinite;
}

Scroll-Driven Animations (CSS Native)

Use the CSS animation-timeline property with scroll() or view() for scroll-linked effects without JavaScript. This runs entirely on the compositor.

.parallax-bg {
  animation: parallax linear both;
  animation-timeline: scroll(root);
}
@keyframes parallax {
  from { transform: translateY(0); }
  to { transform: translateY(-30%); }
}

/* Reveal elements as they enter viewport */
.reveal {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}
@keyframes reveal {
  from { opacity: 0; transform: translateY(40px); }
  to { opacity: 1; transform: translateY(0); }
}

View Transitions API

Use for page-level transitions in SPAs and MPAs. Assign view-transition-name to elements that persist across navigations to morph them smoothly.

.hero-image {
  view-transition-name: hero;
}

::view-transition-old(hero) {
  animation: 0.3s cubic-bezier(0.4, 0, 0.2, 1) fade-out;
}
::view-transition-new(hero) {
  animation: 0.3s cubic-bezier(0.4, 0, 0.2, 1) fade-in;
}
// Trigger transition in SPA
document.startViewTransition(() => {
  updateDOM(); // synchronous DOM update
});

Framer Motion

Reference: references/framer-motion.md

Layout Animations

Use layout prop for automatic FLIP animations when element position or size changes. This avoids manual calculation and always animates transform under the hood.

<motion.div layout layoutId="card-expand" transition={{ type: "spring", stiffness: 300, damping: 30 }}>
  {isExpanded ? <ExpandedContent /> : <CollapsedContent />}
</motion.div>

AnimatePresence for Exit Animations

Wrap lists or conditional elements to animate them out before React unmounts them. Without AnimatePresence, exit animations are impossible because the DOM node is removed immediately.

<AnimatePresence mode="popLayout">
  {items.map((item) => (
    <motion.li
      key={item.id}
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, x: -100 }}
      transition={{ duration: 0.3 }}
    />
  ))}
</AnimatePresence>

Staggered List Entrance

Use staggerChildren in a parent's transition to cascade child animations.

const container = {
  hidden: {},
  show: { transition: { staggerChildren: 0.06 } },
};
const item = {
  hidden: { opacity: 0, y: 20 },
  show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] } },
};

<motion.ul variants={container} initial="hidden" animate="show">
  {items.map((i) => <motion.li key={i.id} variants={item} />)}
</motion.ul>

Scroll-Triggered with useInView

function RevealSection({ children }: { children: React.ReactNode }) {
  const ref = useRef(null);
  const isInView = useInView(ref, { once: true, margin: "-100px" });
  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 40 }}
      animate={isInView ? { opacity: 1, y: 0 } : {}}
      transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
    >
      {children}
    </motion.div>
  );
}

Gesture Animations

<motion.button
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.95 }}
  transition={{ type: "spring", stiffness: 400, damping: 17 }}
/>

GSAP

Reference: references/gsap.md

Timelines for Coordinated Sequences

Use gsap.timeline() when multiple elements must animate in a precise sequence. Timelines provide position parameters for exact overlap control.

const tl = gsap.timeline({ defaults: { ease: "power3.out", duration: 0.6 } });
tl.from(".hero-title", { y: 60, opacity: 0 })
  .from(".hero-subtitle", { y: 40, opacity: 0 }, "-=0.3")   // starts 0.3s before previous ends
  .from(".hero-cta", { scale: 0.9, opacity: 0 }, "-=0.2")
  .from(".hero-image", { x: 100, opacity: 0 }, "<");          // starts at same time as previous

ScrollTrigger

gsap.from(".feature-card", {
  scrollTrigger: {
    trigger: ".features-section",
    start: "top 80%",
    end: "bottom 20%",
    toggleActions: "play none none reverse",
  },
  y: 60,
  opacity: 0,
  stagger: 0.1,
  duration: 0.8,
  ease: "power3.out",
});

Text Split Animation

Split text into spans, then stagger them. GSAP's SplitText plugin handles this, or build manually for fewer dependencies.

// Manual split approach
function splitText(element: HTMLElement) {
  const text = element.textContent ?? '';
  element.innerHTML = text
    .split('')
    .map((char) => `<span class="char" style="display:inline-block">${char === ' ' ? '&nbsp;' : char}</span>`)
    .join('');
  return element.querySelectorAll('.char');
}

const chars = splitText(document.querySelector('.headline')!);
gsap.from(chars, { y: 40, opacity: 0, stagger: 0.03, duration: 0.5, ease: "back.out(1.7)" });

SVG Morphing

Use GSAP's MorphSVG plugin or flubber for path interpolation between SVG shapes.

Three.js / React Three Fiber

Reference: references/threejs-r3f.md for full examples (scenes, particles, cameras, shaders).

Use R3F's useFrame for the animation loop. Use InstancedMesh or <points> for particle systems — never individual meshes per particle. Cap dpr at [1, 2] to prevent GPU overload on high-DPI screens. Set frameloop="demand" for scenes that only change on interaction.

Key rules: keep draw calls under 100, total triangles under 500K (desktop) / 100K (mobile), use power-of-two textures, and mount the Canvas lazily with Intersection Observer.

Lottie

Use Lottie for complex, designer-authored animations exported from After Effects. Do not use Lottie for simple transitions — CSS or Framer Motion is lighter.

import Lottie from 'lottie-react';
import animationData from './success-check.json';

<Lottie animationData={animationData} loop={false} style={{ width: 120, height: 120 }} />

Keep Lottie JSON files under 50KB. Audit them for embedded images (bloat). Use lottie-light for smaller bundle when only SVG rendering is needed.

Animation Patterns

Page Transitions (SPA)

Combine AnimatePresence with route changes. Animate the outgoing page out, then the incoming page in. Use mode="wait" to prevent layout overlap.

<AnimatePresence mode="wait">
  <motion.main
    key={pathname}
    initial={{ opacity: 0, y: 8 }}
    animate={{ opacity: 1, y: 0 }}
    exit={{ opacity: 0, y: -8 }}
    transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
  >
    {children}
  </motion.main>
</AnimatePresence>

Image Reveal with clip-path

.image-reveal {
  clip-path: inset(100% 0 0 0);
  transition: clip-path 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
.image-reveal.visible {
  clip-path: inset(0 0 0 0);
}

Cursor-Following Element

Use requestAnimationFrame with linear interpolation (lerp) to smooth the cursor position. A speed factor of 0.1-0.2 creates a pleasant trailing effect. See references/patterns.md for full implementation.

Smooth Scrolling (Lenis)

Initialize Lenis with a custom easing function and feed it via requestAnimationFrame. When combining with GSAP ScrollTrigger, call ScrollTrigger.update() inside the Lenis scroll callback — without this, they fight over scroll position. See references/patterns.md.

Number Counting Animation

Use requestAnimationFrame with cubic ease-out, triggered by useInView({ once: true }). Apply toLocaleString() for formatted output. See references/patterns.md.

Typewriter Effect

Reveal characters via setInterval at 40-60ms per character. Clean up the interval on unmount. Add a blinking cursor with CSS animation: blink 1s step-end infinite. See references/patterns.md.

Choosing the Right Tool

Need Use Why
Simple hover/focus states CSS transitions Zero JS, zero bundle cost
Scroll-linked parallax CSS scroll-driven animations Compositor-only, no JS scroll listener
Page navigation morphs View Transitions API Browser-native, works across documents
React component enter/exit Framer Motion Handles React lifecycle, unmount animations
Complex multi-element sequences GSAP timeline Precise timing control, position parameters
Scroll-triggered sections GSAP ScrollTrigger or Framer useInView Depends on complexity; ScrollTrigger for pinning/scrub
3D scenes, particles React Three Fiber Declarative Three.js, React integration
Designer-authored complex art Lottie Frame-perfect reproduction from After Effects
Simple loading indicators CSS @keyframes No dependency needed for spinners/shimmers

Anti-Patterns

  • Animating layout properties (width, height, top, left) — causes layout thrashing on every frame.
  • will-change on everything — promotes elements to GPU layers permanently, consuming memory.
  • No reduced-motion support — violates WCAG 2.1 SC 2.3.3, causes vestibular discomfort.
  • Animations longer than 1s for UI elements — users perceive the interface as slow.
  • Animating during page load — blocks first meaningful paint; defer non-critical animations.
  • Using JS for scroll-linked effects that CSS can handle — JS scroll listeners fire on the main thread.
  • Individual mesh per particle in Three.js — use InstancedMesh or Points geometry.
  • Lottie for simple transitions — a 200KB JSON for a fade-in is unjustifiable.