GSAP动画最佳实践 gsap-animations

这份指南提供了使用GSAP(GreenSock动画平台)在网页设计中实现专业、可访问且性能优越的动画的最佳实践,包括滚动触发器、性能优化、可访问性、响应式动画和测试集成。

前端开发 0 次安装 0 次浏览 更新于 3/3/2026

name: GSAP动画最佳实践 description: GSAP动画最佳实践,用于网页设计 - 滚动触发器、性能优化、可访问性、响应式动画和测试集成。在WordPress或任何Web项目中实现或审查动画时使用。 allowed-tools: 阅读、写作、编辑、Bash、Glob、Grep

GSAP动画最佳实践

使用GSAP(GreenSock动画平台)实现专业、可访问且性能优越的动画的全面指南。

核心原则

1. 性能优先

  • 仅动画transformopacity(GPU加速)
  • 避免动画widthheighttopleftmarginpadding
  • 谨慎使用will-change
  • 目标是在所有设备上达到60fps

2. 始终可访问

  • 尊重prefers-reduced-motion
  • 确保内容在没有JavaScript的情况下可见
  • 不要将关键内容隐藏在动画后面
  • 为长时间动画提供跳过/暂停控制

3. 渐进增强

  • 内容必须在没有动画的情况下工作
  • 动画增强而不是替代功能
  • 在禁用动画时进行测试

GSAP设置

安装

<!-- CDN(WordPress推荐)-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>

<!-- 可选插件 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollSmoother.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/SplitText.min.js"></script>

WordPress Enqueue

function theme_enqueue_gsap() {
    // GSAP核心
    wp_enqueue_script(
        'gsap',
        'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js',
        array(),
        '3.12.5',
        true
    );

    // ScrollTrigger
    wp_enqueue_script(
        'gsap-scrolltrigger',
        'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js',
        array('gsap'),
        '3.12.5',
        true
    );

    // 主题动画
    wp_enqueue_script(
        'theme-animations',
        get_theme_file_uri('/assets/js/animations.js'),
        array('gsap', 'gsap-scrolltrigger'),
        filemtime(get_theme_file_path('/assets/js/animations.js')),
        true
    );
}
add_action('wp_enqueue_scripts', 'theme_enqueue_gsap');

动画模式

1. 滚动时淡入

// 基本淡入
gsap.from('.fade-in', {
    opacity: 0,
    y: 50,
    duration: 1,
    stagger: 0.2,
    scrollTrigger: {
        trigger: '.fade-in',
        start: 'top 80%',
        toggleActions: 'play none none none'
    }
});

2. 交错元素

// 卡片逐个出现
gsap.from('.card', {
    opacity: 0,
    y: 100,
    duration: 0.8,
    stagger: {
        amount: 0.6,
        from: 'start'
    },
    ease: 'power2.out',
    scrollTrigger: {
        trigger: '.cards-container',
        start: 'top 75%'
    }
});

3. 平行视差效果

// 图片上的微妙平行视差
gsap.to('.parallax-image', {
    yPercent: -20,
    ease: 'none',
    scrollTrigger: {
        trigger: '.parallax-section',
        start: 'top bottom',
        end: 'bottom top',
        scrub: true
    }
});

4. 文本揭示(逐行)

// 需要SplitText插件(Club GreenSock)
// 或使用CSS替代方案

// CSS替代方案 - 将每一行包裹在span中
gsap.from('.reveal-line', {
    opacity: 0,
    y: '100%',
    duration: 0.8,
    stagger: 0.1,
    ease: 'power3.out',
    scrollTrigger: {
        trigger: '.text-reveal',
        start: 'top 80%'
    }
});

5. 幕布/遮罩揭示

// 通过滑动遮罩揭示图像
gsap.to('.curtain-mask', {
    scaleX: 0,
    transformOrigin: 'right center',
    duration: 1.2,
    ease: 'power4.inOut',
    scrollTrigger: {
        trigger: '.curtain-container',
        start: 'top 70%'
    }
});

6. 英雄动画时间线

// 复杂的英雄序列
const heroTL = gsap.timeline({
    defaults: { ease: 'power3.out' }
});

heroTL
    .from('.hero-bg', { scale: 1.2, duration: 1.5 })
    .from('.hero-title', { opacity: 0, y: 100, duration: 1 }, '-=1')
    .from('.hero-subtitle', { opacity: 0, y: 50, duration: 0.8 }, '-=0.5')
    .from('.hero-cta', { opacity: 0, y: 30, duration: 0.6 }, '-=0.3');

可访问性

尊重减少运动

// 检查用户偏好
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

// 选项1:禁用所有动画
if (prefersReducedMotion) {
    gsap.globalTimeline.timeScale(0);
    ScrollTrigger.getAll().forEach(st => st.kill());
}

// 选项2:简化动画
const animationConfig = prefersReducedMotion
    ? { duration: 0, stagger: 0 }
    : { duration: 1, stagger: 0.2 };

gsap.from('.element', {
    opacity: 0,
    y: prefersReducedMotion ? 0 : 50,
    ...animationConfig
});

CSS回退

/* 确保没有JS时内容可见 */
.fade-in {
    opacity: 1;
    transform: translateY(0);
}

/* 只有在动画将运行时才隐藏 */
.js .fade-in {
    opacity: 0;
    transform: translateY(50px);
}

/* 在CSS中也尊重减少运动 */
@media (prefers-reduced-motion: reduce) {
    .js .fade-in {
        opacity: 1;
        transform: none;
    }
}

添加JS类到HTML

// 在脚本开始时添加
document.documentElement.classList.add('js');

响应式动画

断点感知动画

// 创建响应式动画
const mm = gsap.matchMedia();

mm.add('(min-width: 1024px)', () => {
    // 桌面动画
    gsap.from('.hero-image', {
        x: 100,
        opacity: 0,
        duration: 1.2
    });

    return () => {
        // 断点变化时清理
    };
});

mm.add('(max-width: 1023px)', () => {
    // 移动动画(更简单)
    gsap.from('.hero-image', {
        opacity: 0,
        duration: 0.8
    });
});

刷新调整大小时

// 调整大小时重新计算ScrollTrigger
let resizeTimer;
window.addEventListener('resize', () => {
    clearTimeout(resizeTimer);
    resizeTimer = setTimeout(() => {
        ScrollTrigger.refresh();
    }, 250);
});

性能优化

1. 仅使用变换属性

// 好 - GPU加速
gsap.to('.element', {
    x: 100,          // transform: translateX
    y: 50,           // transform: translateY
    rotation: 45,    // transform: rotate
    scale: 1.2,      // transform: scale
    opacity: 0.5
});

// 坏 - 引起布局/绘制
gsap.to('.element', {
    left: 100,       // 触发布局
    width: '200px',  // 触发布局
    marginTop: 50    // 触发布局
});

2. 批量相似动画

// 对许多相似元素使用批量
ScrollTrigger.batch('.card', {
    onEnter: batch => gsap.to(batch, {
        opacity: 1,
        y: 0,
        stagger: 0.1
    }),
    start: 'top 85%'
});

3. 杀死未使用的ScrollTriggers

// 导航(SPA)或组件卸载时清理
function cleanup() {
    ScrollTrigger.getAll().forEach(st => st.kill());
    gsap.killTweensOf('*');
}

4. 延迟初始化

// 仅初始化可见部分的动画
const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            initSectionAnimations(entry.target);
            observer.unobserve(entry.target);
        }
    });
}, { rootMargin: '100px' });

document.querySelectorAll('.animated-section').forEach(section => {
    observer.observe(section);
});

ScrollTrigger最佳实践

1. 适当的开始/结束点

// 避免常见错误
ScrollTrigger.create({
    trigger: '.section',
    start: 'top 80%',    // 当触发器顶部击中视口顶部的80%时
    end: 'bottom 20%',   // 当触发器底部击中顶部的20%时
    markers: true,       // 调试仅 - 生产中删除!
});

2. 小心地固定部分

// 固定可能导致布局问题
ScrollTrigger.create({
    trigger: '.pinned-section',
    start: 'top top',
    end: '+=100%',
    pin: true,
    pinSpacing: true,    // 通常希望这是真的
    anticipatePin: 1     // 有助于移动设备
});

3. 处理图像加载

// 图像加载前等待计算位置
ScrollTrigger.config({
    ignoreMobileResize: true
});

window.addEventListener('load', () => {
    ScrollTrigger.refresh();
});

// 或在延迟图像加载后刷新
document.querySelectorAll('img[loading="lazy"]').forEach(img => {
    img.addEventListener('load', () => ScrollTrigger.refresh());
});

测试集成

视觉QA兼容性

要使视觉-qa技能正确捕获动画:

// 暴露函数以立即完成所有动画
window.completeAllAnimations = function() {
    gsap.globalTimeline.progress(1);
    ScrollTrigger.getAll().forEach(st => {
        st.scroll(st.end);
    });
};

// 或跳过动画以进行屏幕截图
if (window.location.search.includes('skip-animations')) {
    gsap.globalTimeline.timeScale(100);
}

Playwright测试

// 在Playwright测试中
await page.evaluate(() => {
    if (window.completeAllAnimations) {
        window.completeAllAnimations();
    }
});
await page.waitForTimeout(500);
await page.screenshot({ path: 'screenshot.png', fullPage: true });

常见动画库

可重用的动画类

// animations.js - 可重用的动画库

const Animations = {
    // 初始化所有动画
    init() {
        if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
            return;
        }

        this.fadeIn();
        this.slideIn();
        this.parallax();
        this.textReveal();
    },

    fadeIn() {
        gsap.utils.toArray('[data-animate="fade-in"]').forEach(el => {
            gsap.from(el, {
                opacity: 0,
                y: 50,
                duration: 0.8,
                scrollTrigger: {
                    trigger: el,
                    start: 'top 85%',
                    once: true
                }
            });
        });
    },

    slideIn() {
        gsap.utils.toArray('[data-animate="slide-left"]').forEach(el => {
            gsap.from(el, {
                opacity: 0,
                x: -100,
                duration: 1,
                scrollTrigger: {
                    trigger: el,
                    start: 'top 80%',
                    once: true
                }
            });
        });

        gsap.utils.toArray('[data-animate="slide-right"]').forEach(el => {
            gsap.from(el, {
                opacity: 0,
                x: 100,
                duration: 1,
                scrollTrigger: {
                    trigger: el,
                    start: 'top 80%',
                    once: true
                }
            });
        });
    },

    parallax() {
        gsap.utils.toArray('[data-parallax]').forEach(el => {
            const speed = el.dataset.parallax || 0.2;
            gsap.to(el, {
                yPercent: -100 * speed,
                ease: 'none',
                scrollTrigger: {
                    trigger: el.parentElement,
                    start: 'top bottom',
                    end: 'bottom top',
                    scrub: true
                }
            });
        });
    },

    textReveal() {
        gsap.utils.toArray('[data-animate="text-reveal"]').forEach(el => {
            const lines = el.querySelectorAll('.line');
            gsap.from(lines, {
                opacity: 0,
                y: '100%',
                duration: 0.8,
                stagger: 0.1,
                scrollTrigger: {
                    trigger: el,
                    start: 'top 80%',
                    once: true
                }
            });
        });
    },

    // 动态内容后刷新
    refresh() {
        ScrollTrigger.refresh();
    },

    // SPA导航清理
    destroy() {
        ScrollTrigger.getAll().forEach(st => st.kill());
        gsap.killTweensOf('*');
    }
};

// 在DOM就绪时初始化
document.addEventListener('DOMContentLoaded', () => Animations.init());

HTML用法

<!-- 淡入 -->
<div data-animate="fade-in">内容</div>

<!-- 从左侧滑入 -->
<div data-animate="slide-left">内容</div>

<!-- 平行视差(0.2 = 20%速度) -->
<img data-parallax="0.3" src="image.jpg">

<!-- 文本揭示(需要行包装) -->
<div data-animate="text-reveal">
    <div class="line">第一行</div>
    <div class="line">第二行</div>
</div>

调试

启用标记

ScrollTrigger.defaults({
    markers: true  // 显示开始/结束标记
});

日志动画事件

gsap.to('.element', {
    x: 100,
    onStart: () => console.log('动画开始'),
    onComplete: () => console.log('动画完成'),
    onUpdate: self => console.log('进度:', self.progress())
});

检查问题

// 列出所有ScrollTriggers
console.log('ScrollTriggers:', ScrollTrigger.getAll());

// 检查元素是否存在
const el = document.querySelector('.animated-element');
if (!el) console.warn('未找到动画目标!');

清单

发布前

  • [ ] 移除所有markers: true
  • [ ] 测试prefers-reduced-motion: reduce
  • [ ] 在移动设备上测试(真实设备,不仅仅是DevTools)
  • [ ] 在DevTools性能选项卡中检查性能
  • [ ] 在目标设备上验证60fps
  • [ ] 没有JavaScript时内容可见
  • [ ] 图像延迟加载前ScrollTrigger刷新
  • [ ] 没有布局抖动(避免动画布局属性)

视觉QA集成

  • [ ] 屏幕截图前完成动画
  • [ ] 全页滚动触发所有动画
  • [ ] 屏幕截图捕获最终动画状态
  • [ ] 在所有视口大小下测试

资源