.agent/skills/gsap/SKILL.md
GSAP animations for JARVIS HUD transitions and effects
npx skillsauth add lildibbb/tele-crm-frontend gsapInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
File Organization: This skill uses split structure. See
references/for advanced patterns.
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:
// 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)
})
})
// 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()
})
// 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 }
}
# Run animation tests
npm test -- --grep "Animation"
# Check for memory leaks
npm run test:memory
# Verify 60fps performance
npm run test:performance
| Package | Version | Notes | |---------|---------|-------| | gsap | ^3.12.0 | Core library | | @gsap/vue | ^3.12.0 | Vue integration | | ScrollTrigger | included | Scroll effects |
// plugins/gsap.ts
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
export default defineNuxtPlugin(() => {
gsap.registerPlugin(ScrollTrigger)
return {
provide: {
gsap,
ScrollTrigger
}
}
})
<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>
// 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 }
}
<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>
// 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
}
<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>
// ✅ 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%'
})
// ✅ 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 })
}
// 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 })
}
// 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
})
// 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 })
}
// 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
})
})
// 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 })
})
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)
})
})
// ❌ 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 })
GSAP provides professional animations for JARVIS HUD:
Remember: Every animation must be cleaned up to prevent memory leaks.
References:
references/advanced-patterns.md - Complex animation patternsdevelopment
Tiptap editor API patterns for vmark WYSIWYG development. Use when working with editor commands, node traversal, selection handling, or format operations.
tools
Expert guide for creating effective prompts for Google Stitch AI UI design tool. Use when user wants to design UI/UX in Stitch, create app interfaces, generate mobile/web designs, or needs help cra...
development
Teaches agents to iteratively build websites using Stitch with an autonomous baton-passing loop pattern
development
Provides complete shadcn/ui component library patterns including installation, configuration, and implementation of accessible React components. Use when setting up shadcn/ui, installing components, building forms with React Hook Form and Zod, customizing themes with Tailwind CSS, or implementing UI patterns like buttons, dialogs, dropdowns, tables, and complex form layouts.