GSAPHUD动画开发技能Skill gsap

本技能提供GSAP(GreenSock动画平台)专业知识,用于在JARVIS AI助手HUD中创建平滑、专业的动画。主要应用包括HUD面板入场/出场动画、状态指示器过渡、数据可视化动画、滚动触发效果和复杂时间线序列。强调测试驱动开发、性能优化、内存管理、可访问性和高质量代码实践,适用于前端开发场景。关键词:GSAP, 动画, JARVIS, HUD, 前端开发, Vue.js, 性能优化, TDD, 可访问性。

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

name: gsap description: 用于JARVIS HUD过渡和效果的GSAP动画 model: sonnet risk_level: 低 version: 1.0.0

GSAP动画技能

文件组织:本技能使用分割结构。高级模式请见references/

1. 概述

本技能提供GSAP(GreenSock动画平台)专业知识,用于在JARVIS AI助手HUD中创建平滑、专业的动画。

风险等级:低——动画库,安全面小

主要使用场景

  • HUD面板入场/出场动画
  • 状态指示器过渡动画
  • 数据可视化动画
  • 滚动触发效果
  • 复杂时间线序列

2. 核心职责

2.1 基本原则

  1. 测试驱动开发优先:在实现前编写动画测试
  2. 性能意识:使用变换/不透明度进行GPU加速,避免布局抖动
  3. 清理要求:组件卸载时始终终止动画
  4. 时间线组织:复杂序列使用时间线
  5. 缓动选择:为HUD感觉选择合适的缓动
  6. 可访问性:尊重减少动画偏好
  7. 内存管理:通过适当清理避免内存泄漏

2.5 实现工作流(测试驱动开发)

步骤1:先编写失败测试

// tests/animations/panel-animation.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { gsap } from 'gsap'
import HUDPanel from '~/components/HUDPanel.vue'

describe('HUD面板动画', () => {
  beforeEach(() => {
    // 模拟减少动画
    Object.defineProperty(window, 'matchMedia', {
      writable: true,
      value: vi.fn().mockImplementation(query => ({
        matches: false,
        media: query
      }))
    })
  })

  afterEach(() => {
    // 验证清理
    gsap.globalTimeline.clear()
  })

  it('动画面板入场具有正确属性', async () => {
    const wrapper = mount(HUDPanel)

    // 等待动画完成
    await new Promise(resolve => setTimeout(resolve, 600))

    const panel = wrapper.find('.hud-panel')
    expect(panel.exists()).toBe(true)
  })

  it('卸载时清理动画', async () => {
    const wrapper = mount(HUDPanel)
    const childCount = gsap.globalTimeline.getChildren().length

    await wrapper.unmount()

    // 所有动画应被终止
    expect(gsap.globalTimeline.getChildren().length).toBeLessThan(childCount)
  })

  it('尊重减少动画偏好', async () => {
    // 模拟减少动画启用
    window.matchMedia = vi.fn().mockImplementation(() => ({
      matches: true
    }))

    const wrapper = mount(HUDPanel)
    const panel = wrapper.find('.hud-panel').element

    // 应无动画直接设置最终状态
    expect(gsap.getProperty(panel, 'opacity')).toBe(1)
  })
})

步骤2:实现最小通过代码

// components/HUDPanel.vue - 实现动画逻辑
const animation = ref<gsap.core.Tween | null>(null)

onMounted(() => {
  if (!panelRef.value) return

  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    gsap.set(panelRef.value, { opacity: 1 })
    return
  }

  animation.value = gsap.from(panelRef.value, {
    opacity: 0,
    y: 20,
    duration: 0.5
  })
})

onUnmounted(() => {
  animation.value?.kill()
})

步骤3:遵循模式重构

// 提取到可组合中以便重用
export function usePanelAnimation(elementRef: Ref<HTMLElement | null>) {
  const animation = ref<gsap.core.Tween | null>(null)

  const animate = () => {
    if (!elementRef.value) return

    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      gsap.set(elementRef.value, { opacity: 1 })
      return
    }

    animation.value = gsap.from(elementRef.value, {
      opacity: 0,
      y: 20,
      duration: 0.5,
      ease: 'power2.out'
    })
  }

  onMounted(animate)
  onUnmounted(() => animation.value?.kill())

  return { animation }
}

步骤4:运行完整验证

# 运行动画测试
npm test -- --grep "Animation"

# 检查内存泄漏
npm run test:memory

# 验证60fps性能
npm run test:performance

3. 技术栈和版本

3.1 推荐版本

版本 备注
gsap ^3.12.0 核心库
@gsap/vue ^3.12.0 Vue集成
ScrollTrigger 包含 滚动效果

3.2 Vue集成

// plugins/gsap.ts
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'

export default defineNuxtPlugin(() => {
  gsap.registerPlugin(ScrollTrigger)

  return {
    provide: {
      gsap,
      ScrollTrigger
    }
  }
})

4. 实现模式

4.1 面板入场动画

<script setup lang="ts">
import { gsap } from 'gsap'
import { onMounted, onUnmounted, ref } from 'vue'

const panelRef = ref<HTMLElement | null>(null)
let animation: gsap.core.Tween | null = null

onMounted(() => {
  if (!panelRef.value) return

  // ✅ 检查减少动画偏好
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    gsap.set(panelRef.value, { opacity: 1 })
    return
  }

  animation = gsap.from(panelRef.value, {
    opacity: 0,
    y: 20,
    scale: 0.95,
    duration: 0.5,
    ease: 'power2.out'
  })
})

// ✅ 卸载时清理
onUnmounted(() => {
  animation?.kill()
})
</script>

<template>
  <div ref="panelRef" class="hud-panel">
    <slot />
  </div>
</template>

4.2 状态指示器动画

// composables/useStatusAnimation.ts
import { gsap } from 'gsap'

export function useStatusAnimation(element: Ref<HTMLElement | null>) {
  const timeline = ref<gsap.core.Timeline | null>(null)

  const animateStatus = (status: string) => {
    if (!element.value) return

    timeline.value?.kill()

    timeline.value = gsap.timeline()

    switch (status) {
      case 'active':
        timeline.value
          .to(element.value, {
            scale: 1.2,
            duration: 0.2,
            ease: 'power2.out'
          })
          .to(element.value, {
            scale: 1,
            duration: 0.3,
            ease: 'elastic.out(1, 0.3)'
          })
        break

      case 'warning':
        timeline.value.to(element.value, {
          backgroundColor: '#f59e0b',
          boxShadow: '0 0 10px #f59e0b',
          duration: 0.3,
          repeat: 2,
          yoyo: true
        })
        break

      case 'error':
        timeline.value.to(element.value, {
          x: -5,
          duration: 0.05,
          repeat: 5,
          yoyo: true
        })
        break
    }
  }

  onUnmounted(() => {
    timeline.value?.kill()
  })

  return { animateStatus }
}

4.3 数据可视化动画

<script setup lang="ts">
import { gsap } from 'gsap'

const props = defineProps<{
  data: number[]
}>()

const barsRef = ref<HTMLElement[]>([])
let animations: gsap.core.Tween[] = []

watch(() => props.data, (newData) => {
  // 终止之前动画
  animations.forEach(a => a.kill())
  animations = []

  // 动画每个柱状图
  newData.forEach((value, index) => {
    const bar = barsRef.value[index]
    if (!bar) return

    const tween = gsap.to(bar, {
      height: `${value}%`,
      duration: 0.5,
      delay: index * 0.05,
      ease: 'power2.out'
    })

    animations.push(tween)
  })
}, { immediate: true })

onUnmounted(() => {
  animations.forEach(a => a.kill())
})
</script>

<template>
  <div class="flex items-end h-40 gap-1">
    <div
      v-for="(_, index) in data"
      :key="index"
      ref="barsRef"
      class="w-4 bg-jarvis-primary"
    />
  </div>
</template>

4.4 时间线序列

// 创建复杂HUD启动序列
export function createStartupSequence(elements: {
  logo: HTMLElement
  panels: HTMLElement[]
  status: HTMLElement
}): gsap.core.Timeline {
  const tl = gsap.timeline({
    defaults: { ease: 'power2.out' }
  })

  // 徽标揭示
  tl.from(elements.logo, {
    opacity: 0,
    scale: 0,
    duration: 0.8,
    ease: 'back.out(1.7)'
  })

  // 面板交错进入
  tl.from(elements.panels, {
    opacity: 0,
    x: -30,
    stagger: 0.1,
    duration: 0.5
  }, '-=0.3')

  // 状态指示器
  tl.from(elements.status, {
    opacity: 0,
    y: 10,
    duration: 0.3
  }, '-=0.2')

  return tl
}

4.5 滚动触发动画

<script setup lang="ts">
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'

const sectionRef = ref<HTMLElement | null>(null)

onMounted(() => {
  if (!sectionRef.value) return

  gsap.from(sectionRef.value.querySelectorAll('.animate-item'), {
    scrollTrigger: {
      trigger: sectionRef.value,
      start: 'top 80%',
      end: 'bottom 20%',
      toggleActions: 'play none none reverse'
    },
    opacity: 0,
    y: 30,
    stagger: 0.1,
    duration: 0.5
  })
})

onUnmounted(() => {
  ScrollTrigger.getAll().forEach(trigger => trigger.kill())
})
</script>

5. 质量标准

5.1 性能

// ✅ 良好 - 使用变换进行GPU加速
gsap.to(element, {
  x: 100,
  y: 50,
  rotation: 45,
  scale: 1.2
})

// ❌ 不良 - 触发布局重新计算
gsap.to(element, {
  left: 100,
  top: 50,
  width: '120%'
})

5.2 可访问性

// ✅ 尊重减少动画
const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches

if (prefersReducedMotion) {
  gsap.set(element, { opacity: 1 })
} else {
  gsap.from(element, { opacity: 0, duration: 0.5 })
}

6. 性能模式

6.1 will-change属性使用

// 良好:动画前应用will-change
const animatePanel = (element: HTMLElement) => {
  element.style.willChange = 'transform, opacity'

  gsap.to(element, {
    x: 100,
    opacity: 0.8,
    duration: 0.5,
    onComplete: () => {
      element.style.willChange = 'auto'
    }
  })
}

// 不良:从不移除will-change
const animatePanelBad = (element: HTMLElement) => {
  element.style.willChange = 'transform, opacity' // 内存泄漏!
  gsap.to(element, { x: 100, opacity: 0.8 })
}

6.2 变换与布局属性

// 良好:使用变换(GPU加速)
gsap.to(element, {
  x: 100,           // translateX
  y: 50,            // translateY
  scale: 1.2,       // scale
  rotation: 45,     // rotate
  opacity: 0.5      // opacity
})

// 不良:布局触发属性(CPU,导致回流)
gsap.to(element, {
  left: 100,        // 触发布局
  top: 50,          // 触发布局
  width: '120%',    // 触发布局
  height: 200,      // 触发布局
  margin: 10        // 触发布局
})

6.3 时间线重用

// 良好:重用时间线实例
const timeline = gsap.timeline({ paused: true })
timeline
  .to(element, { opacity: 1, duration: 0.3 })
  .to(element, { y: -20, duration: 0.5 })

// 按需播放/反转
const show = () => timeline.play()
const hide = () => timeline.reverse()

// 不良:每次创建新时间线
const showBad = () => {
  gsap.timeline()
    .to(element, { opacity: 1, duration: 0.3 })
    .to(element, { y: -20, duration: 0.5 })
}

6.4 ScrollTrigger批处理

// 良好:批处理ScrollTrigger动画
ScrollTrigger.batch('.animate-item', {
  onEnter: (elements) => {
    gsap.to(elements, {
      opacity: 1,
      y: 0,
      stagger: 0.1,
      overwrite: true
    })
  },
  onLeave: (elements) => {
    gsap.to(elements, {
      opacity: 0,
      y: -20,
      overwrite: true
    })
  }
})

// 不良:每个元素单独ScrollTrigger
document.querySelectorAll('.animate-item').forEach(item => {
  gsap.to(item, {
    scrollTrigger: {
      trigger: item,
      start: 'top 80%'
    },
    opacity: 1,
    y: 0
  })
})

6.5 懒初始化

// 良好:仅在需要时初始化动画
let panelAnimation: gsap.core.Timeline | null = null

const getPanelAnimation = () => {
  if (!panelAnimation) {
    panelAnimation = gsap.timeline({ paused: true })
      .from('.panel', { opacity: 0, y: 20 })
      .from('.panel-content', { opacity: 0, stagger: 0.1 })
  }
  return panelAnimation
}

const showPanel = () => getPanelAnimation().play()
const hidePanel = () => getPanelAnimation().reverse()

// 不良:挂载时初始化所有动画
onMounted(() => {
  // 即使从未使用也创建时间线
  const animation1 = gsap.timeline().to('.panel1', { x: 100 })
  const animation2 = gsap.timeline().to('.panel2', { y: 100 })
  const animation3 = gsap.timeline().to('.panel3', { scale: 1.2 })
})

7. 测试与质量

7.1 动画测试

describe('面板动画', () => {
  it('卸载时清理', async () => {
    const wrapper = mount(HUDPanel)
    await wrapper.unmount()

    // 应无活动GSAP动画剩余
    expect(gsap.globalTimeline.getChildren().length).toBe(0)
  })
})

8. 常见错误和反模式

8.1 关键反模式

永不:跳过清理

// ❌ 内存泄漏
onMounted(() => {
  gsap.to(element, { x: 100, duration: 1 })
})

// ✅ 适当清理
let tween: gsap.core.Tween

onMounted(() => {
  tween = gsap.to(element, { x: 100, duration: 1 })
})

onUnmounted(() => {
  tween?.kill()
})

永不:动画布局属性

// ❌ 不良 - 导致布局抖动
gsap.to(element, { width: 200, height: 100 })

// ✅ 良好 - 使用变换
gsap.to(element, { scaleX: 2, scaleY: 1 })

13. 实施前检查清单

阶段1:编写代码前

  • [ ] 编写动画行为的失败测试
  • [ ] 定义动画时间和缓动需求
  • [ ] 识别需要will-change提示的元素
  • [ ] 计划所有动画的清理策略
  • [ ] 检查是否需要减少动画支持

阶段2:实施期间

  • [ ] 仅使用变换/不透明度(无布局属性)
  • [ ] 存储动画引用以便清理
  • [ ] 动画前应用will-change,动画后移除
  • [ ] 序列使用时间线
  • [ ] 批处理ScrollTrigger动画
  • [ ] 复杂动画实施懒初始化

阶段3:提交前

  • [ ] 所有测试通过(npm test – --grep “Animation”)
  • [ ] 卸载时所有动画已清理
  • [ ] 尊重减少动画偏好
  • [ ] 无内存泄漏(用DevTools检查)
  • [ ] 保持60fps(用性能监视器测试)
  • [ ] ScrollTrigger实例适当终止

14. 总结

GSAP为JARVIS HUD提供专业动画:

  1. 清理:卸载时始终终止动画
  2. 性能:仅使用变换和不透明度
  3. 可访问性:尊重减少动画偏好
  4. 组织:序列使用时间线

记住:每个动画都必须清理以防止内存泄漏。


参考

  • references/advanced-patterns.md - 复杂动画模式