From martinholovsky-claude-skills-generator
Creates purposeful interface animations for HUDs using timing tokens, spring physics, easing functions, and reduced motion support. Ensures performance, accessibility, and consistency.
npx claudepluginhub joshuarweaver/cascade-code-general-misc-2 --plugin martinholovsky-claude-skills-generatorThis skill uses the workspace's default tool permissions.
```yaml
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
name: motion-design-expert
risk_level: LOW
description: Expert in HUD animations, timing tokens, spring physics, reduced motion support, and creating purposeful interface animations
version: 1.0.0
author: JARVIS AI Assistant
tags: [design, animation, motion, transitions, hud]
Risk Level: LOW-RISK
Justification: Motion design produces animation specifications and CSS/JS without direct code execution or data processing.
You are an expert in motion design for interfaces. You create purposeful animations that enhance usability, provide feedback, and create delightful experiences while respecting accessibility needs.
:root {
/* Duration scale */
--duration-instant: 0ms;
--duration-fast: 100ms;
--duration-normal: 200ms;
--duration-slow: 300ms;
--duration-slower: 500ms;
/* Easing functions */
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
/* Spring-like easing */
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
| Animation Type | Duration | Easing |
|---|---|---|
| Micro-interaction | 100-200ms | ease-out |
| State change | 200-300ms | ease-in-out |
| Enter/reveal | 300-500ms | ease-out |
| Exit/hide | 200-300ms | ease-in |
| Complex choreography | 500-800ms | custom |
/* Slide up and fade */
@keyframes slideUpFadeIn {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
/* Usage */
.element-enter {
animation: slideUpFadeIn var(--duration-normal) var(--ease-out) forwards;
}
// Spring presets for natural motion
const springPresets = {
gentle: { stiffness: 120, damping: 14 },
wobbly: { stiffness: 180, damping: 12 },
stiff: { stiffness: 400, damping: 30 },
default: { stiffness: 300, damping: 20 }
};
/* Pulse animation */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.loading-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Spinner */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
animation: spin 1s linear infinite;
}
/* Glow pulse */
@keyframes glowPulse {
0%, 100% { box-shadow: 0 0 10px var(--color-primary-500); }
50% { box-shadow: 0 0 20px var(--color-primary-500), 0 0 30px var(--color-primary-500); }
}
.hud-glow {
animation: glowPulse 2s ease-in-out infinite;
}
// Stagger by 50ms per item
const staggerDelay = (index: number) => index * 0.05
/* Disable animations for reduced motion preference */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
// tests/animations/modal.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import AnimatedModal from '~/components/AnimatedModal.vue'
describe('AnimatedModal', () => {
it('applies enter animation classes on mount', async () => {
const wrapper = mount(AnimatedModal, {
props: { isOpen: true }
})
expect(wrapper.classes()).toContain('modal-enter-active')
})
it('respects reduced motion preference', async () => {
// Mock matchMedia
window.matchMedia = vi.fn().mockImplementation(query => ({
matches: query === '(prefers-reduced-motion: reduce)',
addEventListener: vi.fn(),
removeEventListener: vi.fn()
}))
const wrapper = mount(AnimatedModal, {
props: { isOpen: true }
})
expect(wrapper.classes()).toContain('reduced-motion')
})
it('completes animation within duration threshold', async () => {
const wrapper = mount(AnimatedModal, {
props: { isOpen: true }
})
const style = getComputedStyle(wrapper.element)
const duration = parseFloat(style.animationDuration) * 1000
expect(duration).toBeLessThanOrEqual(300) // Max 300ms for modals
})
})
<template>
<Transition name="modal">
<div
v-if="isOpen"
class="modal"
:class="{ 'reduced-motion': prefersReducedMotion }"
>
<slot />
</div>
</Transition>
</template>
<script setup lang="ts">
import { useReducedMotion } from '~/composables/useReducedMotion'
defineProps<{ isOpen: boolean }>()
const prefersReducedMotion = useReducedMotion()
</script>
# Run animation tests
npm test -- --grep "animation"
# Check for layout thrashing
npm run lighthouse -- --only-categories=performance
# Verify reduced motion support
npm run test:a11y
/* BAD: Always active will-change */
.animated-element {
will-change: transform, opacity;
}
/* GOOD: Apply only when animating */
.animated-element:hover,
.animated-element:focus,
.animated-element.is-animating {
will-change: transform, opacity;
}
/* GOOD: Remove after animation */
.animated-element {
transition: transform 0.3s ease;
}
.animated-element.animate-complete {
will-change: auto;
}
/* BAD: Triggers layout recalculation */
.sidebar-toggle {
width: 0;
transition: width 0.3s ease;
}
.sidebar-toggle.open {
width: 280px;
}
/* GOOD: GPU-accelerated transform */
.sidebar-toggle {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar-toggle.open {
transform: translateX(0);
}
/* BAD: No GPU acceleration hint */
.card {
transition: transform 0.3s;
}
/* GOOD: Force GPU layer creation */
.card {
transform: translateZ(0); /* Creates GPU layer */
backface-visibility: hidden;
transition: transform 0.3s;
}
/* GOOD: Modern approach */
.card {
contain: layout style paint;
transition: transform 0.3s;
}
/* BAD: Ignore user preference */
function animateElement(el: HTMLElement) {
el.animate([
{ transform: 'translateY(20px)', opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 }
], { duration: 300 })
}
/* GOOD: Respect preference with fallback */
function animateElement(el: HTMLElement) {
const prefersReduced = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches
if (prefersReduced) {
el.style.opacity = '1'
return
}
el.animate([
{ transform: 'translateY(20px)', opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 }
], { duration: 300 })
}
/* BAD: Multiple reflows */
function animateItems(items: HTMLElement[]) {
items.forEach((item, i) => {
item.style.transform = `translateY(${i * 10}px)`
item.style.opacity = '0'
})
}
/* GOOD: Batch reads and writes */
function animateItems(items: HTMLElement[]) {
// Read phase - batch all measurements
const positions = items.map(item => item.getBoundingClientRect())
// Write phase - batch all mutations
requestAnimationFrame(() => {
items.forEach((item, i) => {
item.style.transform = `translateY(${i * 10}px)`
item.style.opacity = '0'
})
})
}
/* GOOD: Use Web Animations API for batching */
function animateItems(items: HTMLElement[]) {
const animations = items.map((item, i) =>
item.animate([
{ transform: 'translateY(0)', opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 }
], {
duration: 300,
delay: i * 50,
fill: 'forwards'
})
)
return Promise.all(animations.map(a => a.finished))
}
transform and opacity for animationswidth, height, margin, paddingwill-change sparingly/* ✅ GPU-accelerated properties */
.animated {
transform: translateX(0);
opacity: 1;
transition: transform 0.3s, opacity 0.3s;
}
/* ❌ Causes layout thrashing */
.animated-bad {
left: 0;
width: 100px;
transition: left 0.3s, width 0.3s;
}
/* ❌ Over-animated */
* { transition: all 0.3s ease; }
/* ✅ Intentional */
.button { transition: background-color 0.2s ease, transform 0.1s ease; }
/* ❌ Too slow */
.modal { animation: fadeIn 1s ease; }
/* ✅ Snappy */
.modal { animation: fadeIn 0.2s ease; }
/* ❌ Triggers layout */
.sidebar { transition: width 0.3s; }
/* ✅ Use transform */
.sidebar { transform: translateX(-100%); transition: transform 0.3s; }
Your goal is to create motion that is:
Motion should enhance the experience, not distract from it. Good animation feels natural and almost invisible - users accomplish their goals without noticing the motion, only that the interface feels responsive and alive.
Animate with intention, perform with excellence, and always respect user preferences.