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. 01HUDパネルの表示・終了効果の追加
  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. 概要

このスキルでは、JARVIS AI Assistant HUD で滑らかでプロフェッショナルなアニメーションを作成するための GSAP(GreenSock Animation Platform)の専門知識を提供します。

リスクレベル:低 — セキュリティ上の攻撃対象領域が最小限のアニメーションライブラリです。

主なユースケース:

  • HUD パネルの表示/非表示アニメーション
  • ステータスインジケーターの遷移
  • データビジュアライゼーションのアニメーション
  • スクロール連動エフェクト
  • 複雑なタイムラインシーケンス

2. 主な責務

2.1 基本原則
  1. まず TDD:実装前にアニメーションのテストを作成します
  2. パフォーマンスを考慮:GPU アクセラレーションには transform/opacity を使用し、レイアウトのスラッシングを避けます
  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 連携
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

  // ✅ 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 - 複雑なアニメーションパターン

コメント