Install
Terminal · npx$
npx skills add https://github.com/martinholovsky/claude-skills-generator --skill gsapWorks with Paperclip
How Gsap fits into a Paperclip company.
Gsap drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.
S
SaaS FactoryPaired
Pre-configured AI company — 18 agents, 18 skills, one-time purchase.
$27$59
Explore packSource file
SKILL.md684 linesExpandCollapse
---name: gsapdescription: GSAP animations for JARVIS HUD transitions and effectsmodel: sonnetrisk_level: LOWversion: 1.0.0--- # GSAP Animation Skill > **File Organization**: This skill uses split structure. See `references/` for advanced patterns. ## 1. Overview This skill provides GSAP (GreenSock Animation Platform) expertise for creating smooth, professional animations in the JARVIS AI Assistant HUD. **Risk Level**: LOW - Animation library with minimal security surface **Primary Use Cases**:- HUD panel entrance/exit animations- Status indicator transitions- Data visualization animations- Scroll-triggered effects- Complex timeline sequences ## 2. Core Responsibilities ### 2.1 Fundamental Principles 1. **TDD First**: Write animation tests before implementation2. **Performance Aware**: Use transforms/opacity for GPU acceleration, avoid layout thrashing3. **Cleanup Required**: Always kill animations on component unmount4. **Timeline Organization**: Use timelines for complex sequences5. **Easing Selection**: Choose appropriate easing for HUD feel6. **Accessibility**: Respect reduced motion preferences7. **Memory Management**: Avoid memory leaks with proper cleanup ## 2.5 Implementation Workflow (TDD) ### Step 1: Write Failing Test First ```typescript// tests/animations/panel-animation.test.tsimport { 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) })})``` ### Step 2: Implement Minimum to Pass ```typescript// components/HUDPanel.vue - implement animation logicconst 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()})``` ### Step 3: Refactor Following Patterns ```typescript// Extract to composable for reusabilityexport 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 }}``` ### Step 4: Run Full Verification ```bash# Run animation testsnpm test -- --grep "Animation" # Check for memory leaksnpm run test:memory # Verify 60fps performancenpm run test:performance``` ## 3. Technology Stack & Versions ### 3.1 Recommended Versions | Package | Version | Notes ||---------|---------|-------|| gsap | ^3.12.0 | Core library || @gsap/vue | ^3.12.0 | Vue integration || ScrollTrigger | included | Scroll effects | ### 3.2 Vue Integration ```typescript// plugins/gsap.tsimport { gsap } from 'gsap'import { ScrollTrigger } from 'gsap/ScrollTrigger' export default defineNuxtPlugin(() => { gsap.registerPlugin(ScrollTrigger) return { provide: { gsap, ScrollTrigger } }})``` ## 4. Implementation Patterns ### 4.1 Panel Entrance Animation ```vue<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 unmountonUnmounted(() => { animation?.kill()})</script> <template> <div ref="panelRef" class="hud-panel"> <slot /> </div></template>``` ### 4.2 Status Indicator Animation ```typescript// composables/useStatusAnimation.tsimport { 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 Data Visualization Animation ```vue<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 Timeline Sequence ```typescript// Create complex HUD startup sequenceexport 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 Scroll-Triggered Animation ```vue<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. Quality Standards ### 5.1 Performance ```typescript// ✅ GOOD - Use transforms for GPU accelerationgsap.to(element, { x: 100, y: 50, rotation: 45, scale: 1.2}) // ❌ BAD - Triggers layout recalculationgsap.to(element, { left: 100, top: 50, width: '120%'})``` ### 5.2 Accessibility ```typescript// ✅ Respect reduced motionconst 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. Performance Patterns ### 6.1 will-change Property Usage ```typescript// Good: Apply will-change before animationconst 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-changeconst animatePanelBad = (element: HTMLElement) => { element.style.willChange = 'transform, opacity' // Memory leak! gsap.to(element, { x: 100, opacity: 0.8 })}``` ### 6.2 Transform vs Layout Properties ```typescript// 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 Timeline Reuse ```typescript// Good: Reuse timeline instanceconst timeline = gsap.timeline({ paused: true })timeline .to(element, { opacity: 1, duration: 0.3 }) .to(element, { y: -20, duration: 0.5 }) // Play/reverse as neededconst show = () => timeline.play()const hide = () => timeline.reverse() // Bad: Creating new timeline each timeconst showBad = () => { gsap.timeline() .to(element, { opacity: 1, duration: 0.3 }) .to(element, { y: -20, duration: 0.5 })}``` ### 6.4 ScrollTrigger Batching ```typescript// Good: Batch ScrollTrigger animationsScrollTrigger.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 elementdocument.querySelectorAll('.animate-item').forEach(item => { gsap.to(item, { scrollTrigger: { trigger: item, start: 'top 80%' }, opacity: 1, y: 0 })})``` ### 6.5 Lazy Initialization ```typescript// Good: Initialize animations only when neededlet 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 mountonMounted(() => { // 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. Testing & Quality ### 7.1 Animation Testing ```typescriptdescribe('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. Common Mistakes & Anti-Patterns ### 8.1 Critical Anti-Patterns #### Never: Skip Cleanup ```typescript// ❌ MEMORY LEAKonMounted(() => { gsap.to(element, { x: 100, duration: 1 })}) // ✅ PROPER CLEANUPlet tween: gsap.core.Tween onMounted(() => { tween = gsap.to(element, { x: 100, duration: 1 })}) onUnmounted(() => { tween?.kill()})``` #### Never: Animate Layout Properties ```typescript// ❌ BAD - Causes layout thrashinggsap.to(element, { width: 200, height: 100 }) // ✅ GOOD - Use transformsgsap.to(element, { scaleX: 2, scaleY: 1 })``` ## 13. Pre-Implementation Checklist ### Phase 1: Before Writing Code - [ ] Write failing tests for animation behavior- [ ] Define animation timing and easing requirements- [ ] Identify elements that need will-change hints- [ ] Plan cleanup strategy for all animations- [ ] Check if reduced motion support is needed ### Phase 2: During Implementation - [ ] Use transforms/opacity only (no layout properties)- [ ] Store animation references for cleanup- [ ] Apply will-change before, remove after animation- [ ] Use timelines for sequences- [ ] Batch ScrollTrigger animations- [ ] Implement lazy initialization for complex animations ### Phase 3: Before Committing - [ ] All tests pass (npm test -- --grep "Animation")- [ ] All animations cleaned up on unmount- [ ] Reduced motion preference respected- [ ] No memory leaks (check with DevTools)- [ ] 60fps maintained (test with performance monitor)- [ ] ScrollTrigger instances properly killed ## 14. Summary GSAP provides professional animations for JARVIS HUD: 1. **Cleanup**: Always kill animations on unmount2. **Performance**: Use transforms and opacity only3. **Accessibility**: Respect reduced motion preference4. **Organization**: Use timelines for sequences **Remember**: Every animation must be cleaned up to prevent memory leaks. --- **References**:- `references/advanced-patterns.md` - Complex animation patternsRelated skills