mcpField
G
gsap

gsap

github.com/martinholovsky设计与 UI481.3k 次安装47 周前更新
代码仓库

关于 gsap

gsap 是面向 JARVIS AI Assistant HUD 动画开发的 GSAP 技能,适用于需要实现界面动效的开发者。它强调测试先行、动画清理、性能优化与减少动态效果支持。

主要功能

  1. 01HUD 面板进出场动画
  2. 02状态与数据可视化动效
  3. 03滚动触发效果与时间线
  4. 04动画测试和卸载清理
  5. 05遵循减少动态效果偏好

适用场景

  1. 01为 HUD 面板添加进出场效果
  2. 02制作状态指示器过渡
  3. 03创建滚动触发的页面动画

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/martinholovsky/claude-skills-generator --skill gsap

安装后在 Claude Code 中输入/gsap确认已加载。

附带文件references/advanced-patterns.md

详细介绍

GSAP 动画技能

文件组织:此技能采用拆分式结构。高级模式请参阅 references/。

1. 概述

此技能提供 GSAP(GreenSock Animation Platform)专业知识,用于在 JARVIS AI Assistant HUD 中创建流畅、专业的动画。

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

主要用例:

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

2. 核心职责

2.1 基本原则
  1. 优先采用 TDD:先编写动画测试,再进行实现
  2. 关注性能:使用 transform/opacity 实现 GPU 加速,避免布局抖动
  3. 必须清理:组件卸载时,始终终止动画
  4. 组织时间轴:复杂序列使用时间轴
  5. 选择缓动效果:选择适合 HUD 风格的缓动效果
  6. 无障碍性:尊重减少动态效果偏好
  7. 内存管理:通过适当清理避免内存泄漏

2.5 实现工作流(TDD)

步骤 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('HUDPanel Animation', () => {
  beforeEach(() => {
    // Mock reduced motion
    Object.defineProperty(window, 'matchMedia', {
      writable: true,
      value: vi.fn().mockImplementation(query => ({
        matches: false,
        media: query
      }))
    })
  })

  afterEach(() => {
    // Verify cleanup
    gsap.globalTimeline.clear()
  })

  it('animates panel entrance with correct properties', async () => {
    const wrapper = mount(HUDPanel)

    // Wait for animation to complete
    await new Promise(resolve => setTimeout(resolve, 600))

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

  it('cleans up animations on unmount', async () => {
    const wrapper = mount(HUDPanel)
    const childCount = gsap.globalTimeline.getChildren().length

    await wrapper.unmount()

    // All animations should be killed
    expect(gsap.globalTimeline.getChildren().length).toBeLessThan(childCount)
  })

  it('respects reduced motion preference', async () => {
    // Mock reduced motion enabled
    window.matchMedia = vi.fn().mockImplementation(() => ({
      matches: true
    }))

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

    // Should set final state immediately without animation
    expect(gsap.getProperty(panel, 'opacity')).toBe(1)
  })
})
步骤 2:实现足以通过测试的最少代码
// components/HUDPanel.vue - implement animation logic
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:按照模式重构
// Extract to composable for reusability
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:运行完整验证
# Run animation tests
npm test -- --grep "Animation"

# Check for memory leaks
npm run test:memory

# Verify 60fps performance
npm run test:performance

3. 技术栈与版本

3.1 推荐版本
软件包版本说明
gsap^3.12.0核心库
@gsap/vue^3.12.0Vue 集成
ScrollTriggerincluded滚动效果
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

  // ✅ Check reduced motion preference
  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'
  })
})

// ✅ Cleanup on unmount
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) => {
  // Kill previous animations
  animations.forEach(a => a.kill())
  animations = []

  // Animate each bar
  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 时间轴序列
// Create complex HUD startup sequence
export function createStartupSequence(elements: {
  logo: HTMLElement
  panels: HTMLElement[]
  status: HTMLElement
}): gsap.core.Timeline {
  const tl = gsap.timeline({
    defaults: { ease: 'power2.out' }
  })

  // Logo reveal
  tl.from(elements.logo, {
    opacity: 0,
    scale: 0,
    duration: 0.8,
    ease: 'back.out(1.7)'
  })

  // Panels stagger in
  tl.from(elements.panels, {
    opacity: 0,
    x: -30,
    stagger: 0.1,
    duration: 0.5
  }, '-=0.3')

  // Status indicator
  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 性能
// ✅ GOOD - Use transforms for GPU acceleration
gsap.to(element, {
  x: 100,
  y: 50,
  rotation: 45,
  scale: 1.2
})

// ❌ BAD - Triggers layout recalculation
gsap.to(element, {
  left: 100,
  top: 50,
  width: '120%'
})
5.2 无障碍性
// ✅ Respect reduced motion
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 属性的使用
// Good: Apply will-change before animation
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'
    }
  })
}

// Bad: Never removing will-change
const animatePanelBad = (element: HTMLElement) => {
  element.style.willChange = 'transform, opacity' // Memory leak!
  gsap.to(element, { x: 100, opacity: 0.8 })
}
6.2 transform 属性与布局属性对比
// Good: Use transforms (GPU accelerated)
gsap.to(element, {
  x: 100,           // translateX
  y: 50,            // translateY
  scale: 1.2,       // scale
  rotation: 45,     // rotate
  opacity: 0.5      // opacity
})

// Bad: Layout-triggering properties (CPU, causes reflow)
gsap.to(element, {
  left: 100,        // Triggers layout
  top: 50,          // Triggers layout
  width: '120%',    // Triggers layout
  height: 200,      // Triggers layout
  margin: 10        // Triggers layout
})
6.3 时间轴复用
// Good: Reuse timeline instance
const timeline = gsap.timeline({ paused: true })
timeline
  .to(element, { opacity: 1, duration: 0.3 })
  .to(element, { y: -20, duration: 0.5 })

// Play/reverse as needed
const show = () => timeline.play()
const hide = () => timeline.reverse()

// Bad: Creating new timeline each time
const showBad = () => {
  gsap.timeline()
    .to(element, { opacity: 1, duration: 0.3 })
    .to(element, { y: -20, duration: 0.5 })
}
6.4 ScrollTrigger 批处理
// Good: Batch ScrollTrigger animations
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
    })
  }
})

// Bad: Individual ScrollTrigger per element
document.querySelectorAll('.animate-item').forEach(item => {
  gsap.to(item, {
    scrollTrigger: {
      trigger: item,
      start: 'top 80%'
    },
    opacity: 1,
    y: 0
  })
})
6.5 延迟初始化
// Good: Initialize animations only when needed
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()

// Bad: Initialize all animations on mount
onMounted(() => {
  // Creates timeline even if never used
  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('Panel Animation', () => {
  it('cleans up on unmount', async () => {
    const wrapper = mount(HUDPanel)
    await wrapper.unmount()

    // No active GSAP animations should remain
    expect(gsap.globalTimeline.getChildren().length).toBe(0)
  })
})

8. 常见错误与反模式

8.1 关键反模式
切勿:跳过清理
// ❌ MEMORY LEAK
onMounted(() => {
  gsap.to(element, { x: 100, duration: 1 })
})

// ✅ PROPER CLEANUP
let tween: gsap.core.Tween

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

onUnmounted(() => {
  tween?.kill()
})
切勿:对布局属性应用动画
// ❌ BAD - Causes layout thrashing
gsap.to(element, { width: 200, height: 100 })

// ✅ GOOD - Use transforms
gsap.to(element, { scaleX: 2, scaleY: 1 })

13. 实现前检查清单

阶段 1:编写代码前
  • 为动画行为编写失败的测试
  • 明确动画时长和缓动效果要求
  • 确定需要 will-change 提示的元素
  • 为所有动画制定清理策略
  • 检查是否需要支持减少动态效果偏好
阶段 2:实现期间
  • 仅使用 transform/opacity(不使用布局属性)
  • 保存动画引用以便清理
  • 动画前应用 will-change,动画后移除
  • 序列使用时间轴
  • 对 ScrollTrigger 动画进行批处理
  • 为复杂动画实现延迟初始化
阶段 3:提交前
  • 所有测试均通过(npm test -- --grep "Animation")
  • 组件卸载时所有动画均已清理
  • 尊重减少动态效果偏好
  • 无内存泄漏(使用 DevTools 检查)
  • 保持 60fps(使用性能监视器测试)
  • 正确终止 ScrollTrigger 实例

14. 总结

GSAP 为 JARVIS HUD 提供专业动画:

  1. 清理:组件卸载时,始终终止动画
  2. 性能:仅使用 transform 和 opacity
  3. 无障碍性:尊重减少动态效果偏好
  4. 组织:序列使用时间轴

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


参考资料:

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

评论

支持 Markdown