$npx -y skills add draguris/dragonclaw --skill video-animationBuild agency-quality animated videos (motion graphics, brand showcases, product reveals) using the video-js artifact type with React, Framer Motion, and Tailwind. Use when the user asks to create an animation, showcase video, motion graphic, or brand film.
| 1 | # Video Animation Skill |
| 2 | |
| 3 | Build smooth, cinematic motion graphics for any brand or product. This skill covers the full workflow from setup to scene authoring — brand-agnostic and reusable for any project. |
| 4 | |
| 5 | --- |
| 6 | |
| 7 | ## Setup |
| 8 | |
| 9 | ### 1. Create the artifact |
| 10 | |
| 11 | ```javascript |
| 12 | const result = await createArtifact({ |
| 13 | artifactType: "video-js", |
| 14 | slug: "my-video", |
| 15 | previewPath: "/", |
| 16 | title: "My Brand Showcase" |
| 17 | }); |
| 18 | ``` |
| 19 | |
| 20 | ### 2. Define your brand system in `index.css` |
| 21 | |
| 22 | Replace the placeholder values with your actual brand. This is the only place brand colors and fonts are defined — all scenes reference these variables. |
| 23 | |
| 24 | ```css |
| 25 | /* index.css */ |
| 26 | :root { |
| 27 | /* --- YOUR BRAND COLORS --- */ |
| 28 | --color-primary: #YOUR_PRIMARY; /* main brand color, CTAs, accents */ |
| 29 | --color-secondary: #YOUR_SECONDARY; /* secondary accent */ |
| 30 | --color-bg-dark: #YOUR_BG; /* scene background */ |
| 31 | |
| 32 | --color-text-primary: #ffffff; |
| 33 | --color-text-secondary: #aaaaaa; |
| 34 | --color-text-muted: #666666; |
| 35 | |
| 36 | /* --- YOUR BRAND FONTS --- */ |
| 37 | --font-display: 'YourDisplayFont', sans-serif; /* titles, headings */ |
| 38 | --font-body: 'YourBodyFont', sans-serif; /* body, labels */ |
| 39 | } |
| 40 | ``` |
| 41 | |
| 42 | ### 3. Load fonts in `index.html` |
| 43 | |
| 44 | ```html |
| 45 | <link href="https://fonts.googleapis.com/css2?family=YourDisplayFont&family=YourBodyFont&display=swap" rel="stylesheet"> |
| 46 | ``` |
| 47 | |
| 48 | **Font selection by brand personality:** |
| 49 | | Mood | Display font | Body font | |
| 50 | |---|---|---| |
| 51 | | Tech/Developer | Space Grotesk | JetBrains Mono | |
| 52 | | Bold/Energy | Bungee or Anton | DM Sans | |
| 53 | | Premium/Luxury | Cormorant Garamond | Inter | |
| 54 | | Playful | Nunito | Baloo 2 | |
| 55 | | Editorial | Fraunces | IBM Plex Sans | |
| 56 | |
| 57 | --- |
| 58 | |
| 59 | ## File Structure |
| 60 | |
| 61 | Every video follows this layout inside `artifacts/<slug>/src/`: |
| 62 | |
| 63 | ``` |
| 64 | components/video/ |
| 65 | ├── VideoTemplate.tsx ← scene orchestrator + timing |
| 66 | ├── PersistentLayers.tsx ← background, shapes, accents (OUTSIDE AnimatePresence) |
| 67 | ├── Scene1<Name>.tsx |
| 68 | ├── Scene2<Name>.tsx |
| 69 | └── Scene3<Name>.tsx ← as many scenes as needed |
| 70 | lib/video/ |
| 71 | ├── hooks.ts ← useVideoPlayer (auto-advances scenes, loops) |
| 72 | └── animations.ts ← spring/easing presets |
| 73 | ``` |
| 74 | |
| 75 | --- |
| 76 | |
| 77 | ## VideoTemplate.tsx — Scene Orchestrator |
| 78 | |
| 79 | ```tsx |
| 80 | import { AnimatePresence } from 'framer-motion'; |
| 81 | import { useVideoPlayer } from '@/lib/video'; |
| 82 | import { PersistentLayers } from './PersistentLayers'; |
| 83 | import { Scene1Intro } from './Scene1Intro'; |
| 84 | import { Scene2Features } from './Scene2Features'; |
| 85 | import { Scene3Lockup } from './Scene3Lockup'; |
| 86 | |
| 87 | // Durations in milliseconds. Mix short punchy beats with longer dramatic holds. |
| 88 | const SCENE_DURATIONS = { |
| 89 | intro: 4000, // brand reveal — let it breathe |
| 90 | features: 5000, // staggered content — needs reading time |
| 91 | lockup: 6000, // final impression — hold it longer |
| 92 | }; |
| 93 | |
| 94 | export default function VideoTemplate() { |
| 95 | const { currentScene } = useVideoPlayer({ durations: SCENE_DURATIONS }); |
| 96 | |
| 97 | return ( |
| 98 | <div className="w-full h-screen overflow-hidden relative bg-[var(--color-bg-dark)]"> |
| 99 | {/* Persistent background — lives OUTSIDE AnimatePresence */} |
| 100 | <PersistentLayers currentScene={currentScene} /> |
| 101 | |
| 102 | {/* Scenes swap in/out with AnimatePresence */} |
| 103 | <AnimatePresence mode="wait"> |
| 104 | {currentScene === 0 && <Scene1Intro key="intro" />} |
| 105 | {currentScene === 1 && <Scene2Features key="features" />} |
| 106 | {currentScene === 2 && <Scene3Lockup key="lockup" />} |
| 107 | </AnimatePresence> |
| 108 | </div> |
| 109 | ); |
| 110 | } |
| 111 | ``` |
| 112 | |
| 113 | **Timing rules:** |
| 114 | - First scene: 3–4s (brand reveal needs to land) |
| 115 | - Feature/content scenes: 4–6s (staggered lists need time) |
| 116 | - Final scene: 5–7s (leave impression before loop) |
| 117 | - Never make every scene the same duration — rhythm dies |
| 118 | |
| 119 | --- |
| 120 | |
| 121 | ## PersistentLayers.tsx — Continuous Background |
| 122 | |
| 123 | Elements here **never unmount**. They animate to new states when `currentScene` changes, creating visual continuity across scene cuts instead of hard resets. |
| 124 | |
| 125 | ```tsx |
| 126 | import { motion } from 'framer-motion'; |
| 127 | |
| 128 | export function PersistentLayers({ currentScene }: { currentScene: number }) { |
| 129 | return ( |
| 130 | <> |
| 131 | {/* Gradient that shifts between scenes */} |
| 132 | <motion.div |
| 133 | className="absolute inset-0 z-0" |
| 134 | animate={{ |
| 135 | background: currentScene === 0 |
| 136 | ? 'radial-gradient(ellipse at 30% 40%, rgba(VAR_R,VAR_G,VAR_B,0.25) 0%, #YOUR_BG 70%)' |
| 137 | : currentScene === 1 |
| 138 | ? 'radial-gradient(ellipse at 70% 20%, rgba(VAR_R,VAR_G,VAR_B,0.15) 0%, #YOUR_BG 60%)' |
| 139 | : 'radial-gradient(ellipse at 50% 80%, rgba(VAR_R,VAR_G,VAR_B,0.3) 0%, #YOUR_BG 65%)', |
| 140 | }} |
| 141 | transition={{ duration: 2, ease: 'easeInOut' }} |
| 142 | /> |
| 143 | |
| 144 | {/* Floating shape that drifts and rotates between scenes */} |
| 145 | <motion.div |
| 146 | classNam |