loopwind
# loopwind
A CLI tool for generating images and videos from JSX templates using Tailwind CSS and Satori. Templates live in a `.loopwind/` directory alongside your codebase.
## Quick Start
Loopwind is a CLI tool for generating images and videos with React and Tailwind CSS. It's designed to be used with AI Agents and Cursor.
### Installation
```bash
curl -fsSL https://loopwind.dev/install.sh | bash
```
This installs loopwind to `~/.loopwind/` and adds the `loopwind` command to your PATH. Requires Node.js 18+.
### Initialize in Your Project
Navigate to any project folder and run:
```bash
loopwind init
```
This creates `.loopwind/loopwind.json` — a configuration file with your project's theme colors.
### Install AI Skill
Give your AI agent expertise in loopwind:
```bash
npx skills add https://loopwind.dev/skill.md
```
This installs a skill that teaches Claude Code (or other AI agents) how to create templates, use animation classes, and render images/videos.
### Use with Claude Code
With the loopwind skill installed, Claude has deep knowledge of template structure, animation classes, and Tailwind CSS patterns for Satori. Just ask:
```
Create an OG image for my blog post about TypeScript tips
```
```
Create an animated intro video for my YouTube channel
```
Claude will create optimized templates and render the final output automatically.
### Install a Template
#### 1. Official Templates
```bash
loopwind add image-template
loopwind add video-template
```
Templates are installed to: `.loopwind/<template>/`
**Benefits:**
- Templates are local to your project
- Version controlled with your project
- Easy to share within your team
### Render a Template
```bash
loopwind render template-name '{"title":"Hello World","subtitle":"Built with loopwind"}'
```
or use a local props file:
```bash
loopwind render template-name props.json
```
## Commands
### `loopwind add <source>`
Install a template from various sources:
```bash
# Official templates
loopwind add image-template
loopwind add video-template
```
These will be downloaded to `.loopwind/<template>/`
### `loopwind list`
List all installed templates:
```bash
loopwind list
```
### `loopwind render <template> <props> [options]`
Render an image or video:
```bash
# Image with inline props
loopwind render banner-hero '{"title":"Hello World"}'
# Video with inline props
loopwind render video-intro '{"title":"Welcome"}'
# Using a props file
loopwind render banner-hero props.json
# Custom output
loopwind render banner-hero '{"title":"Hello"}' --out custom-name.png
# Different format
loopwind render banner-hero '{"title":"Hello"}' --format jpeg
```
Options:
- `--out, -o` - Output filename (default: `<template>.<ext>` in current directory)
- `--format` - Output format: `png`, `jpeg`, `svg` (images only)
- `--quality` - JPEG quality 1-100 (default: 92)
### `loopwind validate <template>`
Validate a template:
```bash
loopwind validate banner-hero
```
Checks:
- Template file exists and is valid React
- `export const meta` exists and is valid
- Required props are defined
- Fonts exist (if specified)
### `loopwind init`
Initialize loopwind in a project:
```bash
loopwind init
```
Creates `.loopwind/loopwind.json` configuration file with your project's design tokens.
## Animation Classes (Video Only)
Use Tailwind-style animation classes - no manual calculations needed:
```tsx
// Fade in: starts at 0ms, lasts 500ms
<h1 style={tw('enter-fade-in/0/500')}>Hello</h1>
// Loop: ping effect every 500ms
<div style={tw('loop-ping/500')} />
// Combined with easing
<h1 style={tw('ease-out enter-bounce-in-up/0/600')}>Title</h1>
```
See [Animation](/animation) for the complete reference.
## Next Steps
- [Templates](/templates)
- [Embedding Images](/images)
- [Animation](/animation)
- [Helpers (QR, Template Composition)](/helpers)
- [Styling with Tailwind & shadcn/ui](/styling)
- [Custom Fonts](/fonts)
- [AI Agent Integration](/agents)
# Templates
Templates are React components that define your images and videos. They use Tailwind CSS for styling and export metadata that loopwind uses for rendering.
## Installing Templates
### Official Templates
```bash
loopwind add image-template
loopwind add video-template
```
Templates are installed to `.loopwind/<template-name>/`.
### Direct URLs
```bash
loopwind add https://example.com/templates/my-template.json
```
### Local Filesystem
```bash
loopwind add ./my-templates/banner-hero
loopwind add /Users/you/templates/social-card
```
---
## Image Templates
### Basic Structure
```tsx
// .loopwind/banner-hero/template.tsx
export const meta = {
name: "banner-hero",
type: "image",
description: "Hero banner with gradient background",
size: { width: 1600, height: 900 },
props: { title: "string", subtitle: "string" }
};
export default function BannerHero({ title, subtitle, tw }) {
return (
<div style={tw('flex flex-col justify-center items-center w-full h-full bg-gradient-to-br from-purple-600 to-blue-500 p-12')}>
<h1 style={tw('text-7xl font-bold text-white mb-4')}>
{title}
</h1>
<p style={tw('text-2xl text-white/80')}>
{subtitle}
</p>
</div>
);
}
```
### Rendering Images
```bash
# Render with inline props
loopwind render banner-hero '{"title":"Hello World","subtitle":"Welcome"}'
# Custom output name
loopwind render banner-hero '{"title":"Hello"}' --out custom-name.png
# Different format
loopwind render banner-hero '{"title":"Hello"}' --format jpeg --quality 95
# Use a props file
loopwind render banner-hero props.json
```
### Output Formats
| Format | Best For |
|--------|----------|
| **PNG** (default) | Transparency, sharp text, logos |
| **JPEG** | Photographs, gradients, smaller files |
| **SVG** | Vector graphics, scalable designs |
---
## Video Templates
### Basic Structure
```tsx
// .loopwind/video-intro/template.tsx
export const meta = {
name: "video-intro",
type: "video",
description: "Animated intro with bounce-in title",
size: { width: 1920, height: 1080 },
video: { fps: 30, duration: 3 },
props: { title: "string" }
};
export default function VideoIntro({ tw, title }) {
return (
<div style={tw('flex items-center justify-center w-full h-full bg-gradient-to-br from-blue-600 to-purple-700')}>
<h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/600')}>
{title}
</h1>
</div>
);
}
```
### Rendering Videos
```bash
# Render with inline props
loopwind render video-intro '{"title":"Welcome!"}' --out intro.mp4
# Faster encoding with FFmpeg
loopwind render video-intro '{"title":"Welcome!"}' --ffmpeg
# Higher quality (lower CRF = better)
loopwind render video-intro '{"title":"Welcome!"}' --crf 18
```
### FPS and Duration
```tsx
video: { fps: 30, duration: 3 } // 90 frames total
```
| FPS | Use Case |
|-----|----------|
| **24** | Cinematic look, smaller files |
| **30** | Standard web video |
| **60** | Smooth animations |
### Video-Specific Props
Templates receive these additional props:
- **`frame`** - Current frame number (0 to totalFrames - 1)
- **`progress`** - Animation progress from 0 to 1
```tsx
export default function MyVideo({ frame, progress }) {
// frame: 0, 1, 2, ... 89 (for 3s @ 30fps)
// progress: 0.0 at start, 0.5 at middle, 1.0 at end
}
```
### Encoding Options
| Encoder | Command | Use Case |
|---------|---------|----------|
| **WASM** (default) | `loopwind render ...` | CI/CD, no dependencies |
| **FFmpeg** | `loopwind render ... --ffmpeg` | Faster, smaller files |
Install FFmpeg: `brew install ffmpeg` (macOS)
---
## Animation Classes
Use Tailwind-style animation classes for videos:
```tsx
// Enter animations: enter-{type}/{delay}/{duration}
<h1 style={tw('enter-fade-in/0/500')}>Fade in at start</h1>
<h1 style={tw('enter-bounce-in-up/300/400')}>Bounce in after 300ms</h1>
// Exit animations: exit-{type}/{start}/{duration}
<div style={tw('exit-fade-out/2500/500')}>Fade out at 2.5s</div>
// Loop animations: loop-{type}/{duration}
<div style={tw('loop-float/1000')}>Continuous floating</div>
<div style={tw('loop-spin/1000')}>Spinning</div>
// Easing
<h1 style={tw('ease-out enter-slide-left/0/500')}>Smooth slide</h1>
```
See the full [Animation documentation](/animation) for all classes.
---
## Common Sizes
### Social Media
- **Twitter/X Card**: 1200x675
- **Facebook/OG**: 1200x630
- **Instagram Post**: 1080x1080
- **LinkedIn Post**: 1200x627
### Web Graphics
- **Hero Banner**: 1920x1080
- **Blog Header**: 1600x900
- **Thumbnail**: 640x360
---
## Example Templates
### Open Graph Image
```tsx
export const meta = {
name: "og-image",
type: "image",
size: { width: 1200, height: 630 },
props: { title: "string", description: "string" }
};
export default function OGImage({ tw, image, title, description }) {
return (
<div style={tw('flex w-full h-full bg-white')}>
<div style={tw('flex-1 flex flex-col justify-between p-12')}>
<img src={image('logo.svg')} style={tw('h-12 w-auto')} />
<div>
<h1 style={tw('text-5xl font-bold text-gray-900 mb-4')}>{title}</h1>
<p style={tw('text-xl text-gray-600')}>{description}</p>
</div>
<p style={tw('text-gray-400')}>yoursite.com</p>
</div>
</div>
);
}
```
### Animated Intro
```tsx
export const meta = {
name: "animated-intro",
type: "video",
size: { width: 1920, height: 1080 },
video: { fps: 60, duration: 3 },
props: { title: "string", subtitle: "string" }
};
export default function AnimatedIntro({ tw, title, subtitle }) {
return (
<div style={tw('flex flex-col items-center justify-center w-full h-full bg-background')}>
<h1 style={tw('text-8xl font-bold text-foreground ease-out enter-bounce-in-up/0/400')}>
{title}
</h1>
<p style={tw('text-2xl text-muted-foreground mt-4 ease-out enter-fade-in-up/300/400')}>
{subtitle}
</p>
</div>
);
}
```
---
## Next Steps
- [Layouts](/layouts) - Wrap templates with reusable layouts
- [Embedding Images](/images) - Using the `image()` helper
- [Animation](/animation) - Full animation reference
- [Styling](/styling) - Tailwind & shadcn/ui integration
- [Fonts](/fonts) - Custom fonts
# Layouts
Layouts let you wrap templates with consistent headers, footers, and styling. A child template specifies a layout in its meta, and the layout receives the child content as a `children` prop.
## Basic Usage
### Layout Template
Create a layout template that receives `children`:
```tsx
// .loopwind/base-layout/template.tsx
export const meta = {
name: 'base-layout',
type: 'image',
size: { width: 1200, height: 630 },
props: {},
};
export default function BaseLayout({ tw, children }) {
return (
<div style={tw('flex flex-col w-full h-full bg-background')}>
{/* Header */}
<div style={tw('flex items-center px-8 py-4 border-b border-border')}>
<span style={tw('text-2xl font-bold text-primary')}>loopwind</span>
</div>
{/* Content slot */}
<div style={tw('flex flex-1')}>
{children}
</div>
{/* Footer */}
<div style={tw('flex items-center justify-between px-8 py-4 border-t border-border')}>
<span style={tw('text-muted-foreground')}>loopwind.dev</span>
</div>
</div>
);
}
```
### Usage in Templates
Reference the layout using a relative path:
```tsx
// .loopwind/blog-post/template.tsx
export const meta = {
name: 'blog-post',
type: 'image',
layout: '../base-layout', // Layout controls size
props: {
title: 'string',
excerpt: 'string',
},
};
export default function BlogPost({ tw, title, excerpt }) {
return (
<div style={tw('flex flex-col justify-center p-12')}>
<h1 style={tw('text-5xl font-bold text-foreground mb-4 text-balance')}>
{title}
</h1>
<p style={tw('text-xl text-muted-foreground leading-relaxed')}>
{excerpt}
</p>
</div>
);
}
```
### Render
```bash
loopwind render blog-post '{"title":"Hello World","excerpt":"My first post"}'
```
The output uses the layout's size (1200x630) with the child content inside.
---
## Key Concepts
### Size
When using a layout, the **layout's size** controls the final output dimensions. The child template doesn't need a `size` property.
### Path Resolution
Use relative paths to reference layouts:
```tsx
layout: '../base-layout' // Sibling directory
layout: './shared/layout' // Subdirectory
layout: '../../layouts/main' // Parent's sibling
```
### Props Flow
The layout receives:
- All standard helpers (`tw`, `image`, `qr`, `template`, etc.)
- `children` prop containing the rendered child content
- Animation context (`frame`, `progress`) for video layouts
```tsx
export default function Layout({ tw, children, frame, progress }) {
// tw, image, qr, template, path, textPath all available
return (
<div style={tw('flex w-full h-full')}>
{children}
</div>
);
}
```
---
## Video Layouts
Layouts work with video templates. Both the layout and child can use animations:
```tsx
// .loopwind/video-layout/template.tsx
export const meta = {
name: 'video-layout',
type: 'video',
size: { width: 1920, height: 1080 },
video: { fps: 60, duration: 4 },
props: {},
};
export default function VideoLayout({ tw, children }) {
return (
<div style={tw('flex flex-col w-full h-full bg-background')}>
{/* Animated header */}
<div style={tw('flex items-center px-12 py-6 ease-out enter-slide-down/0/500')}>
<span style={tw('text-3xl font-bold text-primary')}>loopwind</span>
</div>
{/* Content */}
<div style={tw('flex flex-1')}>
{children}
</div>
{/* Animated footer */}
<div style={tw('flex px-12 py-6 ease-out enter-fade-in/500/400')}>
<span style={tw('text-muted-foreground')}>loopwind.dev</span>
</div>
</div>
);
}
```
---
## Example: Consistent OG Images
Create a layout for all your OG images:
```tsx
// .loopwind/og-layout/template.tsx
export const meta = {
name: 'og-layout',
type: 'image',
size: { width: 1200, height: 630 },
props: {},
};
export default function OGLayout({ tw, image, children }) {
return (
<div style={tw('flex w-full h-full bg-background')}>
{/* Content area */}
<div style={tw('flex flex-col flex-1 p-12')}>
{/* Logo */}
<div style={tw('flex items-center gap-3 mb-auto')}>
<img src={image('logo.svg')} style={tw('h-10 w-auto')} />
<span style={tw('text-2xl font-bold')}>MyBrand</span>
</div>
{/* Slot for page-specific content */}
<div style={tw('flex flex-1 items-center')}>
{children}
</div>
{/* Domain */}
<span style={tw('text-muted-foreground mt-auto')}>mybrand.com</span>
</div>
</div>
);
}
```
Then create page-specific templates:
```tsx
// .loopwind/og-blog/template.tsx
export const meta = {
name: 'og-blog',
type: 'image',
layout: '../og-layout',
props: {
title: 'string',
author: 'string',
},
};
export default function OGBlog({ tw, title, author }) {
return (
<div style={tw('flex flex-col')}>
<span style={tw('text-sm text-muted-foreground uppercase tracking-wider mb-2')}>
Blog Post
</span>
<h1 style={tw('text-4xl font-bold text-foreground mb-4 text-balance')}>
{title}
</h1>
<span style={tw('text-muted-foreground')}>By {author}</span>
</div>
);
}
```
---
## Next Steps
- [Templates](/templates) - Template structure and metadata
- [Animation](/animation) - Animation classes for video layouts
- [Helpers](/helpers) - Using image(), qr(), and template()
# Embedding Images
Use the `image()` helper to embed images in your templates. It supports loading from props, template directories, and URLs.
## Prop-based Images
Pass the prop name to load an image path from props:
```tsx
export const meta = {
name: "product-card",
type: "image",
size: { width: 1200, height: 630 },
props: {
title: "string",
background: "string?"
}
};
export default function ProductCard({ tw, image, title, background }) {
// Use fallback if no background prop provided
const bgSrc = background
? image('background')
: 'https://images.unsplash.com/photo-1557682250-33bd709cbe85?w=1200';
return (
<div style={tw('relative w-full h-full')}>
<img
src={bgSrc}
style={tw('absolute inset-0 w-full h-full object-cover')}
/>
<div style={tw('relative z-10 p-12')}>
<h1 style={tw('text-6xl font-bold text-white')}>{title}</h1>
</div>
</div>
);
}
```
The `image('background')` helper loads from the `background` prop value (file path or URL).
## Direct File Images
Load images directly from your template directory by including the file extension:
```tsx
export default function ChangelogItem({ tw, image, text }) {
return (
<div style={tw('flex items-center gap-4')}>
{/* Load check.svg from template directory */}
<img
src={image('check.svg')}
style={tw('w-6 h-6')}
/>
<span style={tw('text-lg')}>{text}</span>
</div>
);
}
```
You can also use subdirectories:
```tsx
<img src={image('assets/icons/star.svg')} />
<img src={image('shared/logo.png')} />
```
**Template directory structure:**
```
.loopwind/my-template/
├── template.tsx
├── check.svg ← image('check.svg')
└── assets/
└── icons/
└── star.svg ← image('assets/icons/star.svg')
```
## URLs
The `image()` helper also supports loading images from URLs:
```json
{
"background": "https://example.com/image.jpg"
}
```
## Supported Formats
- **JPEG** (`.jpg`, `.jpeg`)
- **PNG** (`.png`)
- **GIF** (`.gif`)
- **WebP** (`.webp`)
- **SVG** (`.svg`)
## Image Positioning
Use Tailwind's object-fit utilities:
```tsx
export default function ImageGrid({ tw, image, img1, img2, img3 }) {
return (
<div style={tw('flex gap-4 w-full h-full p-8 bg-gray-100')}>
{/* Cover - fills entire area, may crop */}
<img
src={image('img1')}
style={tw('w-full h-full object-cover rounded-lg')}
/>
{/* Contain - fits within area, may letterbox */}
<img
src={image('img2')}
style={tw('w-full h-full object-contain')}
/>
{/* Fill - stretches to fill */}
<img
src={image('img3')}
style={tw('w-full h-full object-fill')}
/>
</div>
);
}
```
## Troubleshooting
### Images Not Loading
Check file paths are relative to the props file:
```json
{
"background": "./images/bg.jpg"
}
```
Absolute paths won't work.
### Optimize Image Sizes
Use appropriately sized images before embedding:
```bash
convert large-image.jpg -resize 1600x900 optimized.jpg
```
---
## Next Steps
- [Templates](/templates) - Creating image and video templates
- [Animation](/animation) - Animation classes for videos
- [Styling](/styling) - Tailwind & shadcn/ui integration
# Animation
loopwind provides **Tailwind-style animation classes** that work with time to create smooth video animations without writing custom code.
> **Note:** Animation classes only work with **video templates** and **GIFs**. For static images, animations will have no effect since there's no time context.
## Quick Start
```tsx
export default function MyVideo({ tw, title, subtitle }) {
return (
<div style={tw('flex flex-col items-center justify-center w-full h-full bg-black')}>
{/* Bounce in from below: starts at 0, lasts 400ms */}
<h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/400')}>
{title}
</h1>
{/* Fade in with upward motion: starts at 300ms, lasts 400ms */}
<p style={tw('text-2xl text-white/80 mt-4 ease-out enter-fade-in-up/300/400')}>
{subtitle}
</p>
{/* Continuous floating animation: repeats every 1s (1000ms) */}
<div style={tw('mt-8 text-4xl loop-float/1000')}>
⬇️
</div>
</div>
);
}
```
## Animation Format
loopwind uses three types of animations with **millisecond timing**:
| Type | Format | Description |
|------|--------|-------------|
| Enter | `enter-{type}/{start}/{duration}` | Animations that play when entering |
| Exit | `exit-{type}/{start}/{duration}` | Animations that play when exiting |
| Loop | `loop-{type}/{duration}` | Continuous looping animations |
All timing values are in **milliseconds** (1000ms = 1 second).
## Utility-Based Animations
In addition to predefined animations, loopwind supports **Tailwind utility-based animations** that let you animate any transform or opacity property directly:
```tsx
// Slide in 20px from the left
<div style={tw('enter-translate-x-5/0/1000')}>Content</div>
// Rotate 90 degrees on entrance
<div style={tw('enter-rotate-90/0/500')}>Spinning</div>
// Fade to 50% opacity in a loop
<div style={tw('loop-opacity-50/1000')}>Pulsing</div>
// Scale down with negative value
<div style={tw('enter--scale-50/0/800')}>Shrinking</div>
```
### Supported Utilities
| Utility | Format | Description | Example |
|---------|--------|-------------|---------|
| **translate-x** | `enter-translate-x-{value}` | Translate horizontally | `enter-translate-x-5` = 20px<br/>`enter-translate-x-full` = 100%<br/>`enter-translate-x-[20px]` = 20px |
| **translate-y** | `enter-translate-y-{value}` | Translate vertically | `loop-translate-y-10` = 40px<br/>`enter-translate-y-1/2` = 50%<br/>`enter-translate-y-[5rem]` = 80px |
| **opacity** | `enter-opacity-{n}` | Set opacity (0-100) | `enter-opacity-50` = 50% |
| **scale** | `enter-scale-{n}` | Scale element (0-200) | `enter-scale-100` = 1.0x |
| **rotate** | `enter-rotate-{n}` | Rotate in degrees | `enter-rotate-45` = 45° |
| **skew-x** | `enter-skew-x-{n}` | Skew on X axis in degrees | `enter-skew-x-12` = 12° |
| **skew-y** | `enter-skew-y-{n}` | Skew on Y axis in degrees | `exit-skew-y-6` = 6° |
**Translate value formats:**
- **Numeric**: `5` = 20px (Tailwind spacing scale: 1 unit = 4px)
- **Keywords**: `full` = 100%
- **Fractions**: `1/2` = 50%, `1/3` = 33.333%, `2/3` = 66.666%, etc.
- **Arbitrary values**: `[20px]`, `[5rem]`, `[10%]` (rem converts to px: 1rem = 16px)
All utilities work with:
- **All prefixes**: `enter-`, `exit-`, `loop-`, `animate-`
- **Negative values**: Prefix with `-` (e.g., `-translate-x-5`, `-rotate-45`)
- **Timing syntax**: Add `/start/duration` (e.g., `enter-translate-x-5/0/800`)
### Translate Animations
```tsx
// Numeric (Tailwind spacing): 20px (5 * 4px)
<div style={tw('enter-translate-x-5/0/500')}>Content</div>
// Keyword: Full width (100%)
<div style={tw('enter-translate-y-full/0/800')}>Dropping full height</div>
// Fraction: Half width (50%)
<div style={tw('enter-translate-x-1/2/0/600')}>Slide in halfway</div>
// Arbitrary values: Exact px or rem
<div style={tw('enter-translate-y-[20px]/0/500')}>Slide 20px</div>
<div style={tw('enter-translate-x-[5rem]/0/800')}>Slide 5rem (80px)</div>
// Loop with fractions
<div style={tw('loop-translate-y-1/4/1000')}>Oscillate 25%</div>
// Negative values
<div style={tw('exit--translate-y-8/2000/500')}>Rising</div>
```
### Opacity Animations
```tsx
// Fade to 100% opacity
<div style={tw('enter-opacity-100/0/500')}>Fading In</div>
// Fade to 50% opacity
<div style={tw('enter-opacity-50/0/800')}>Half Opacity</div>
// Pulse between 50% and 100%
<div style={tw('loop-opacity-50/1000')}>Pulsing</div>
// Fade out to 0%
<div style={tw('exit-opacity-0/2500/500')}>Vanishing</div>
```
### Scale Animations
```tsx
// Scale from 0 to 100% (1.0x)
<div style={tw('enter-scale-100/0/500')}>Growing</div>
// Scale to 150% (1.5x)
<div style={tw('enter-scale-150/0/800')}>Enlarging</div>
// Pulse scale in a loop
<div style={tw('loop-scale-110/1000')}>Breathing</div>
// Scale down to 50%
<div style={tw('exit-scale-50/2000/500')}>Shrinking</div>
```
### Rotate Animations
```tsx
// Rotate 90 degrees
<div style={tw('enter-rotate-90/0/500')}>Quarter Turn</div>
// Rotate 180 degrees
<div style={tw('enter-rotate-180/0/1000')}>Half Turn</div>
// Continuous rotation in loop (360 degrees per cycle)
<div style={tw('loop-rotate-360/2000')}>Spinning</div>
// Rotate backwards with negative value
<div style={tw('enter--rotate-45/0/500')}>Counter Rotation</div>
```
### Skew Animations
```tsx
// Skew on X axis
<div style={tw('enter-skew-x-12/0/500')}>Slanted</div>
// Skew on Y axis
<div style={tw('enter-skew-y-6/0/800')}>Tilted</div>
// Oscillating skew in loop
<div style={tw('loop-skew-x-6/1000')}>Wobbling</div>
// Negative skew
<div style={tw('exit--skew-x-12/2000/500')}>Reverse Slant</div>
```
### Combining Utilities
You can combine multiple utility animations on the same element:
```tsx
// Translate and rotate together
<div style={tw('enter-translate-y-10/0/500 enter-rotate-45/0/500')}>
Flying In
</div>
// Fade and scale
<div style={tw('enter-opacity-100/0/800 enter-scale-100/0/800')}>
Appearing
</div>
// Enter with translate, exit with rotation
<div style={tw('enter-translate-x-5/0/500 exit-rotate-180/2500/500')}>
Slide and Spin
</div>
```
### Bracket Notation
For more CSS-like syntax, you can use brackets with units:
```tsx
// Using bracket notation with seconds
<h1 style={tw('enter-slide-up/[0.6s]/[1.5s]')}>Hello</h1>
// Using bracket notation with milliseconds
<h1 style={tw('enter-fade-in/[300ms]/[800ms]')}>World</h1>
// Mix and match - plain numbers are milliseconds
<h1 style={tw('enter-bounce-in/0/[1.2s]')}>Mixed</h1>
```
## Enter Animations
Format: `enter-{type}/{startMs}/{durationMs}`
- `startMs` - when the animation begins (milliseconds from start)
- `durationMs` - how long the animation lasts
When values are omitted (`enter-fade-in`), it uses the full video duration.
### Fade Animations
Simple opacity transitions with optional direction.
```tsx
// Fade in from 0ms to 500ms
<h1 style={tw('enter-fade-in/0/500')}>Hello</h1>
// Fade in with upward motion
<h1 style={tw('enter-fade-in-up/0/600')}>Hello</h1>
```
| Class | Description |
|-------|-------------|
| `enter-fade-in/0/500` | Fade in (opacity 0 → 1) |
| `enter-fade-in-up/0/500` | Fade in + slide up (30px) |
| `enter-fade-in-down/0/500` | Fade in + slide down (30px) |
| `enter-fade-in-left/0/500` | Fade in + slide from left (30px) |
| `enter-fade-in-right/0/500` | Fade in + slide from right (30px) |
### Slide Animations
Larger movement (100px) with fade.
```tsx
// Slide in from left: starts at 0, lasts 500ms
<div style={tw('enter-slide-left/0/500')}>Content</div>
// Slide up from bottom: starts at 200ms, lasts 600ms
<div style={tw('enter-slide-up/200/600')}>Content</div>
```
| Class | Description |
|-------|-------------|
| `enter-slide-left/0/500` | Slide in from left (100px) |
| `enter-slide-right/0/500` | Slide in from right (100px) |
| `enter-slide-up/0/500` | Slide in from bottom (100px) |
| `enter-slide-down/0/500` | Slide in from top (100px) |
### Bounce Animations
Playful entrance with overshoot effect.
```tsx
// Bounce in with scale overshoot
<h1 style={tw('enter-bounce-in/0/500')}>Bouncy!</h1>
// Bounce in from below
<div style={tw('enter-bounce-in-up/0/600')}>Pop!</div>
```
| Class | Description |
|-------|-------------|
| `enter-bounce-in/0/500` | Bounce in with scale overshoot |
| `enter-bounce-in-up/0/500` | Bounce in from below |
| `enter-bounce-in-down/0/500` | Bounce in from above |
| `enter-bounce-in-left/0/500` | Bounce in from left |
| `enter-bounce-in-right/0/500` | Bounce in from right |
### Scale & Zoom Animations
Size-based transitions.
```tsx
// Scale in from 50%
<div style={tw('enter-scale-in/0/500')}>Growing</div>
// Zoom in from 0%
<div style={tw('enter-zoom-in/0/1000')}>Zooming</div>
```
| Class | Description |
|-------|-------------|
| `enter-scale-in/0/500` | Scale up from 50% to 100% |
| `enter-zoom-in/0/500` | Zoom in from 0% to 100% |
### Rotate & Flip Animations
Rotation-based transitions.
```tsx
// Rotate in 180 degrees
<div style={tw('enter-rotate-in/0/500')}>Spinning</div>
// 3D flip on X axis
<div style={tw('enter-flip-in-x/0/500')}>Flipping</div>
```
| Class | Description |
|-------|-------------|
| `enter-rotate-in/0/500` | Rotate in from -180° |
| `enter-flip-in-x/0/500` | 3D flip on horizontal axis |
| `enter-flip-in-y/0/500` | 3D flip on vertical axis |
## Exit Animations
Format: `exit-{type}/{startMs}/{durationMs}`
- `startMs` - when the exit animation begins
- `durationMs` - how long the exit animation lasts
Exit animations use the same timing system but animate elements out.
```tsx
// Fade out starting at 2500ms, lasting 500ms (ends at 3000ms)
<h1 style={tw('exit-fade-out/2500/500')}>Goodbye</h1>
// Combined enter and exit on same element
<h1 style={tw('enter-fade-in/0/500 exit-fade-out/2500/500')}>
Hello and Goodbye
</h1>
```
| Class | Description |
|-------|-------------|
| `exit-fade-out/2500/500` | Fade out (opacity 1 → 0) |
| `exit-fade-out-up/2500/500` | Fade out + slide up |
| `exit-fade-out-down/2500/500` | Fade out + slide down |
| `exit-fade-out-left/2500/500` | Fade out + slide left |
| `exit-fade-out-right/2500/500` | Fade out + slide right |
| `exit-slide-up/2500/500` | Slide out upward (100px) |
| `exit-slide-down/2500/500` | Slide out downward (100px) |
| `exit-slide-left/2500/500` | Slide out to left (100px) |
| `exit-slide-right/2500/500` | Slide out to right (100px) |
| `exit-scale-out/2500/500` | Scale out to 150% |
| `exit-zoom-out/2500/500` | Zoom out to 200% |
| `exit-rotate-out/2500/500` | Rotate out to 180° |
| `exit-bounce-out/2500/500` | Bounce out with scale |
| `exit-bounce-out-up/2500/500` | Bounce out upward |
| `exit-bounce-out-down/2500/500` | Bounce out downward |
| `exit-bounce-out-left/2500/500` | Bounce out to left |
| `exit-bounce-out-right/2500/500` | Bounce out to right |
## Loop Animations
Format: `loop-{type}/{durationMs}`
Loop animations repeat every `{durationMs}` milliseconds:
- `/1000` = 1 second loop
- `/500` = 0.5 second loop
- `/2000` = 2 second loop
When duration is omitted (`loop-bounce`), it defaults to 1000ms (1 second).
```tsx
// Pulse opacity every 500ms
<div style={tw('loop-fade/500')}>Pulsing</div>
// Bounce every 800ms
<div style={tw('loop-bounce/800')}>Bouncing</div>
// Full rotation every 2000ms
<div style={tw('loop-spin/2000')}>Spinning</div>
```
| Class | Description |
|-------|-------------|
| `loop-fade/{ms}` | Opacity pulse (0.5 → 1 → 0.5) |
| `loop-bounce/{ms}` | Bounce up and down |
| `loop-spin/{ms}` | Full 360° rotation |
| `loop-ping/{ms}` | Scale up + fade out (radar effect) |
| `loop-wiggle/{ms}` | Side to side wiggle |
| `loop-float/{ms}` | Gentle up and down floating |
| `loop-pulse/{ms}` | Scale pulse (1.0 → 1.05 → 1.0) |
| `loop-shake/{ms}` | Shake side to side |
## Easing Functions
Add an easing class **before** the animation class to control the timing curve.
```tsx
// Ease in (accelerate)
<h1 style={tw('ease-in enter-fade-in/0/1000')}>Accelerating</h1>
// Ease out (decelerate) - default
<h1 style={tw('ease-out enter-fade-in/0/1000')}>Decelerating</h1>
// Ease in-out (smooth)
<h1 style={tw('ease-in-out enter-fade-in/0/1000')}>Smooth</h1>
// Strong cubic easing
<h1 style={tw('ease-out-cubic enter-bounce-in/0/500')}>Dramatic</h1>
```
| Class | Description | Best For |
|-------|-------------|----------|
| `linear` | Constant speed | Mechanical motion |
| `ease-in` | Slow start, fast end | Exit animations |
| `ease-out` | Fast start, slow end (default) | Enter animations |
| `ease-in-out` | Slow start and end | Subtle transitions |
| `ease-in-cubic` | Strong slow start | Dramatic exits |
| `ease-out-cubic` | Strong fast start | Impactful entrances |
| `ease-in-out-cubic` | Strong both ends | Emphasis animations |
| `ease-in-quart` | Very strong slow start | Powerful exits |
| `ease-out-quart` | Very strong fast start | Punchy entrances |
| `ease-in-out-quart` | Very strong both ends | Maximum drama |
### Per-Animation-Type Easing
You can apply **different easing functions** to enter, exit, and loop animations on the same element using `enter-ease-*`, `exit-ease-*`, and `loop-ease-*` classes.
```tsx
// Different easing for enter and exit
<h1 style={tw('enter-ease-out-cubic enter-fade-in/0/500 exit-ease-in exit-fade-out/2500/500')}>
Smooth entrance, sharp exit
</h1>
// Loop with linear easing, enter with bounce
<div style={tw('enter-ease-out enter-bounce-in/0/400 loop-ease-linear loop-fade/1000')}>
Bouncy entrance, linear loop
</div>
// Default easing still works (applies to all animations)
<div style={tw('ease-in-out enter-fade-in/0/500 exit-fade-out/2500/500')}>
Same easing for both
</div>
// Mix default with specific overrides
<div style={tw('ease-out enter-fade-in/0/500 exit-ease-in-cubic exit-fade-out/2500/500')}>
Default ease-out for enter, cubic-in for exit
</div>
```
**How it works:**
1. **Default easing** (`ease-*`) applies to ALL animations if no specific override is set
2. **Specific easing** (`enter-ease-*`, `exit-ease-*`, `loop-ease-*`) overrides the default for that animation type
3. If both are present, specific easing takes priority for its animation type
**Available easing classes:**
| Default (all animations) | Enter only | Exit only | Loop only |
|--------------------------|------------|-----------|-----------|
| `ease-in` | `enter-ease-in` | `exit-ease-in` | `loop-ease-in` |
| `ease-out` | `enter-ease-out` | `exit-ease-out` | `loop-ease-out` |
| `ease-in-out` | `enter-ease-in-out` | `exit-ease-in-out` | `loop-ease-in-out` |
| `ease-in-cubic` | `enter-ease-in-cubic` | `exit-ease-in-cubic` | `loop-ease-in-cubic` |
| `ease-out-cubic` | `enter-ease-out-cubic` | `exit-ease-out-cubic` | `loop-ease-out-cubic` |
| `ease-in-out-cubic` | `enter-ease-in-out-cubic` | `exit-ease-in-out-cubic` | `loop-ease-in-out-cubic` |
| `ease-in-quart` | `enter-ease-in-quart` | `exit-ease-in-quart` | `loop-ease-in-quart` |
| `ease-out-quart` | `enter-ease-out-quart` | `exit-ease-out-quart` | `loop-ease-out-quart` |
| `ease-in-out-quart` | `enter-ease-in-out-quart` | `exit-ease-in-out-quart` | `loop-ease-in-out-quart` |
| `linear` | `enter-ease-linear` | `exit-ease-linear` | `loop-ease-linear` |
| `ease-spring` | `enter-ease-spring` | `exit-ease-spring` | `loop-ease-spring` |
### Spring Easing
Spring easing creates natural, physics-based bouncy animations. Use the built-in `ease-spring` easing or create custom springs with configurable parameters.
```tsx
// Default spring easing
<h1 style={tw('ease-spring enter-bounce-in/0/500')}>Bouncy spring!</h1>
// Per-animation-type spring
<div style={tw('enter-ease-spring enter-fade-in/0/500 exit-ease-out exit-fade-out/2500/500')}>
Spring entrance, smooth exit
</div>
// Custom spring with parameters: ease-spring/mass/stiffness/damping
<h1 style={tw('ease-spring/1/100/10 enter-scale-in/0/800')}>
Custom spring (mass=1, stiffness=100, damping=10)
</h1>
// More bouncy spring (lower damping)
<div style={tw('ease-spring/1/170/8 enter-bounce-in-up/0/600')}>
Extra bouncy!
</div>
// Stiffer spring (higher stiffness, faster)
<div style={tw('ease-spring/1/200/12 enter-fade-in-up/0/400')}>
Snappy spring
</div>
// Per-animation-type custom springs
<div style={tw('enter-ease-spring/1/150/10 enter-fade-in/0/500 exit-ease-spring/1/100/15 exit-fade-out/2500/500')}>
Different springs for enter and exit
</div>
```
**Spring parameters:**
| Parameter | Description | Effect when increased | Default |
|-----------|-------------|----------------------|---------|
| **mass** | Mass of the spring | Slower, more inertia | 1 |
| **stiffness** | Spring stiffness | Faster, snappier | 100 |
| **damping** | Damping coefficient | Less bounce, smoother | 10 |
**Common spring presets:**
```tsx
// Gentle bounce (default)
ease-spring/1/100/10
// Extra bouncy
ease-spring/1/170/8
// Snappy (no bounce)
ease-spring/1/200/15
// Slow and bouncy
ease-spring/2/100/8
// Fast and tight
ease-spring/0.5/300/20
```
**How spring works:**
1. **Default `ease-spring`** - Uses a pre-calculated spring curve optimized for most use cases
2. **Custom `ease-spring/mass/stiffness/damping`** - Generates a physics-based spring curve using the [damped harmonic oscillator](https://www.kvin.me/css-springs) formula
3. The spring automatically calculates its ideal duration to reach the final state
4. Works with all animation types: `ease-spring`, `enter-ease-spring`, `exit-ease-spring`, `loop-ease-spring`
## Combining Enter and Exit
You can use both enter and exit animations on the same element:
```tsx
export default function EnterExit({ tw, title }) {
return (
<div style={tw('flex items-center justify-center w-full h-full bg-black')}>
{/* Fade in during first 500ms, fade out during last 500ms (assuming 3s video) */}
<h1 style={tw('text-8xl font-bold text-white enter-fade-in/0/500 exit-fade-out/2500/500')}>
{title}
</h1>
</div>
);
}
```
The opacities from multiple animations are **multiplied together**, so you get smooth transitions that combine properly.
## Staggered Animations
Create sequenced animations by offsetting start times:
```tsx
export default function StaggeredList({ tw, items }) {
return (
<div style={tw('flex flex-col gap-4')}>
{/* First item: starts at 0ms, lasts 300ms */}
<div style={tw('ease-out enter-fade-in-left/0/300')}>
{items[0]}
</div>
{/* Second item: starts at 100ms, lasts 300ms */}
<div style={tw('ease-out enter-fade-in-left/100/300')}>
{items[1]}
</div>
{/* Third item: starts at 200ms, lasts 300ms */}
<div style={tw('ease-out enter-fade-in-left/200/300')}>
{items[2]}
</div>
</div>
);
}
```
### Dynamic Staggering
For dynamic lists, calculate the timing programmatically:
```tsx
export default function DynamicStagger({ tw, items }) {
return (
<div style={tw('flex flex-col gap-4')}>
{items.map((item, i) => {
const start = i * 100; // Each item starts 100ms later
const duration = 300; // Each animation lasts 300ms
return (
<div
key={i}
style={tw(`ease-out enter-fade-in-up/${start}/${duration}`)}
>
{item}
</div>
);
})}
</div>
);
}
```
## Common Patterns
### Intro Sequence
```tsx
export default function IntroVideo({ tw, title, subtitle, logo }) {
return (
<div style={tw('flex flex-col items-center justify-center w-full h-full bg-gradient-to-br from-blue-600 to-purple-700')}>
{/* Logo appears first */}
<img
src={logo}
style={tw('h-20 mb-8 ease-out enter-scale-in/0/300')}
/>
{/* Title bounces in */}
<h1 style={tw('text-7xl font-bold text-white ease-out enter-bounce-in-up/200/500')}>
{title}
</h1>
{/* Subtitle fades in last */}
<p style={tw('text-2xl text-white/80 mt-4 ease-out enter-fade-in-up/400/700')}>
{subtitle}
</p>
</div>
);
}
```
### Text Reveal
```tsx
export default function TextReveal({ tw, words }) {
return (
<div style={tw('flex flex-wrap gap-2 justify-center')}>
{words.split(' ').map((word, i) => (
<span
key={i}
style={tw(`text-4xl font-bold ease-out enter-fade-in-up/${i * 100}/200`)}
>
{word}
</span>
))}
</div>
);
}
```
### Looping Background Element
```tsx
export default function AnimatedBackground({ tw, children }) {
return (
<div style={tw('relative w-full h-full')}>
{/* Floating background circles */}
<div style={tw('absolute top-10 left-10 w-20 h-20 rounded-full bg-white/10 loop-float/2000')} />
<div style={tw('absolute bottom-20 right-20 w-32 h-32 rounded-full bg-white/10 loop-fade/1500')} />
{/* Main content */}
<div style={tw('relative z-10')}>
{children}
</div>
</div>
);
}
```
### Full Enter/Exit Animation
```tsx
export default function FullAnimation({ tw, title }) {
return (
<div style={tw('flex items-center justify-center w-full h-full bg-black')}>
{/* Enter: starts at 0, lasts 400ms. Exit: starts at 2600ms, lasts 400ms */}
<h1 style={tw('text-8xl font-bold text-white ease-out enter-bounce-in-up/0/400 exit-fade-out-up/2600/400')}>
{title}
</h1>
</div>
);
}
```
## Programmatic Animations
For complete control beyond animation classes, use `progress` and `frame` directly.
### Available Props
| Prop | Type | Description |
|------|------|-------------|
| `progress` | `number` | 0 to 1 through the video (0% to 100%) |
| `frame` | `number` | Current frame number (0, 1, 2, ... totalFrames-1) |
These are **only available in video templates**. Use them when animation classes aren't flexible enough.
### Using `frame`
```tsx
export default function FrameAnimation({ tw, frame, title }) {
// Color cycling using frame number
const hue = (frame * 5) % 360; // Cycle through colors
// Pulsing based on frame
const fps = 30;
const pulse = Math.sin(frame / fps * Math.PI * 2) * 0.2 + 0.8; // 0.6 to 1.0
return (
<div style={tw('flex items-center justify-center w-full h-full bg-black')}>
<h1 style={{
...tw('text-8xl font-bold'),
color: `hsl(${hue}, 70%, 60%)`,
transform: `scale(${pulse})`
}}>
{title}
</h1>
</div>
);
}
```
### Using `progress`
```tsx
export default function ProgressAnimation({ tw, progress, title }) {
// Custom fade based on progress
const opacity = progress < 0.3 ? progress / 0.3 : 1;
// Custom scale based on progress
const scale = 0.8 + progress * 0.2; // 0.8 to 1.0
return (
<div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}>
<h1 style={{
...tw('text-8xl font-bold text-white'),
opacity,
transform: `scale(${scale})`
}}>
{title}
</h1>
</div>
);
}
```
### Custom Easing
```tsx
export default function CustomEasing({ tw, progress, title }) {
// Smoothstep easing
const eased = progress * progress * (3 - 2 * progress);
// Elastic easing
const elastic = Math.pow(2, -10 * progress) * Math.sin((progress - 0.075) * (2 * Math.PI) / 0.3) + 1;
return (
<div style={tw('flex items-center justify-center w-full h-full')}>
<h1 style={{
...tw('text-8xl font-bold'),
opacity: eased,
transform: `translateY(${(1 - elastic) * 100}px)`
}}>
{title}
</h1>
</div>
);
}
```
### When to Use Programmatic Animations
Use `progress`/`frame` instead of animation classes when you need:
- **Custom easing functions** (elastic, bounce with specific curves beyond built-in ease-spring)
- **Color cycling or gradients** based on time
- **Mathematical animations** (sine waves, spirals, etc.)
- **Complex multi-property animations** that need precise coordination
- **Conditional logic** based on specific frame numbers
For everything else, prefer animation classes - they're simpler and more maintainable.
### Animating Along Paths
Animate elements along SVG paths with proper rotation using built-in **path helpers**:
```tsx
export default function PathFollowing({ tw, progress, path }) {
// Follow a quadratic Bezier curve - one line!
const rocket = path.followQuadratic(
{ x: 200, y: 400 }, // Start point
{ x: 960, y: 150 }, // Control point
{ x: 1720, y: 400 }, // End point
progress
);
return (
<div style={{ display: 'flex', ...tw('relative w-full h-full bg-gray-900') }}>
{/* Draw the path (optional) */}
<svg width="1920" height="1080" style={{ position: 'absolute' }}>
<path
d="M 200 400 Q 960 150 1720 400"
stroke="rgba(255,255,255,0.2)"
strokeWidth={2}
fill="none"
/>
</svg>
{/* Element following the path */}
<div
style={{
position: "absolute",
left: rocket.x,
top: rocket.y,
transform: `translate(-50%, -50%) rotate(${rocket.angle}deg)`,
fontSize: '48px'
}}
>
🚀
</div>
</div>
);
}
```
### Text Path Animations
Combine `textPath` helpers with animation classes to create animated text along curves:
**Rotating text around a circle:**
```tsx
export default function RotatingCircleText({ tw, textPath, progress }) {
return (
<div style={tw('relative w-full h-full bg-black')}>
{/* Text rotates around circle using progress */}
{textPath.onCircle(
"SPINNING TEXT • AROUND • ",
960, // center x
540, // center y
400, // radius
progress, // rotation offset (0-1 animates full rotation)
{
fontSize: "3xl",
fontWeight: "bold",
color: "yellow-300"
}
)}
</div>
);
}
```
**Animated text reveal along a path:**
```tsx
export default function PathTextReveal({ tw, textPath, progress }) {
// Create custom path follower that animates position
const pathFollower = (t) => {
// Only show characters up to current progress
const visibleProgress = progress * 1.5; // Extend range for smooth reveal
const opacity = t < visibleProgress ? 1 : 0;
// Follow quadratic curve
const pos = {
x: (1 - t) * (1 - t) * 200 + 2 * (1 - t) * t * 960 + t * t * 1720,
y: (1 - t) * (1 - t) * 400 + 2 * (1 - t) * t * 150 + t * t * 400,
angle: 0
};
return { ...pos, opacity };
};
return (
<div style={tw('relative w-full h-full bg-gray-900')}>
{textPath.onPath(
"REVEALING TEXT",
pathFollower,
{
fontSize: "4xl",
fontWeight: "bold",
color: "blue-300"
}
).map((char, i) => (
<div key={i} style={{ ...char.props.style, opacity: char.props.style.opacity || 1 }}>
{char}
</div>
))}
</div>
);
}
```
**Staggered character entrance:**
```tsx
export default function StaggeredCircleText({ tw, textPath }) {
const text = "HELLO WORLD";
return (
<div style={tw('relative w-full h-full bg-slate-900')}>
{textPath.onCircle(
text,
960, 540, 400, 0,
{ fontSize: "4xl", fontWeight: "bold", color: "white" }
).map((char, i) => {
// Stagger fade-in: each character starts 50ms later
const staggerDelay = i * 50;
return (
<div
key={i}
style={{
...char.props.style,
...tw(`enter-fade-in/${staggerDelay}/300 enter-scale-100/${staggerDelay}/300`)
}}
>
{char.props.children}
</div>
);
})}
</div>
);
}
```
**Text with bounce entrance along arc:**
```tsx
export default function BouncyArcText({ tw, textPath }) {
return (
<div style={tw('relative w-full h-full bg-gradient-to-br from-purple-600 to-blue-500')}>
{/* Draw the arc path */}
<svg width="1920" height="1080" style={{ position: 'absolute' }}>
<path
d="M 300 900 A 600 600 0 0 1 1620 900"
stroke="rgba(255,255,255,0.2)"
strokeWidth={2}
fill="none"
strokeDasharray="5 5"
/>
</svg>
{/* Text follows arc with staggered bounce */}
{textPath.onArc(
"BOUNCING ON ARC",
960, // cx
300, // cy
600, // radius
180, // start angle
360, // end angle
{ fontSize: "3xl", fontWeight: "bold", color: "white" }
).map((char, i) => (
<div
key={i}
style={{
...char.props.style,
...tw(`ease-out enter-bounce-in-up/${i * 80}/500`)
}}
>
{char.props.children}
</div>
))}
</div>
);
}
```
**Loop animation with text on curve:**
```tsx
export default function LoopingCurveText({ tw, textPath, frame }) {
// Calculate wave effect using frame
const waveOffset = Math.sin(frame / 30 * Math.PI * 2) * 0.1;
return (
<div style={tw('relative w-full h-full bg-black')}>
{textPath.onQuadratic(
"WAVY TEXT",
{ x: 200, y: 400 },
{ x: 960, y: 150 },
{ x: 1720, y: 400 },
{ fontSize: "4xl", fontWeight: "bold", color: "pink-300" }
).map((char, i) => (
<div
key={i}
style={{
...char.props.style,
transform: `${char.props.style.transform} translateY(${Math.sin((i + frame) / 5) * 10}px)`
}}
>
{char.props.children}
</div>
))}
</div>
);
}
```
**Tips for animating text paths:**
1. **Use `progress` for smooth rotation** on circles and arcs
2. **Map over returned characters** to apply individual animations
3. **Combine with animation classes** like `enter-fade-in`, `enter-bounce-in`, etc.
4. **Stagger character animations** by calculating delays: `i * delayMs`
5. **Use `frame` for continuous effects** like waves or pulsing
6. **Preserve the original transform** when adding animations: `transform: '${char.props.style.transform} ...'`
**Common path types:**
**Quadratic Bezier** (Q command):
```tsx
// Position: (1-t)²·P0 + 2(1-t)t·P1 + t²·P2
function pointOnQuadraticBezier(p0, p1, p2, t) {
const x = (1 - t) * (1 - t) * p0.x + 2 * (1 - t) * t * p1.x + t * t * p2.x;
const y = (1 - t) * (1 - t) * p0.y + 2 * (1 - t) * t * p1.y + t * t * p2.y;
return { x, y };
}
// Tangent angle
function angleOnQuadraticBezier(p0, p1, p2, t) {
const dx = 2 * (1 - t) * (p1.x - p0.x) + 2 * t * (p2.x - p1.x);
const dy = 2 * (1 - t) * (p1.y - p0.y) + 2 * t * (p2.y - p1.y);
return Math.atan2(dy, dx) * (180 / Math.PI);
}
```
**Cubic Bezier** (C command):
```tsx
// Position: (1-t)³·P0 + 3(1-t)²t·P1 + 3(1-t)t²·P2 + t³·P3
function pointOnCubicBezier(p0, p1, p2, p3, t) {
const mt = 1 - t;
const mt2 = mt * mt;
const mt3 = mt2 * mt;
const t2 = t * t;
const t3 = t2 * t;
const x = mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x;
const y = mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y;
return { x, y };
}
// Tangent angle
function angleOnCubicBezier(p0, p1, p2, p3, t) {
const mt = 1 - t;
const mt2 = mt * mt;
const t2 = t * t;
const dx = -3 * mt2 * p0.x + 3 * mt2 * p1.x - 6 * mt * t * p1.x - 3 * t2 * p2.x + 6 * mt * t * p2.x + 3 * t2 * p3.x;
const dy = -3 * mt2 * p0.y + 3 * mt2 * p1.y - 6 * mt * t * p1.y - 3 * t2 * p2.y + 6 * mt * t * p2.y + 3 * t2 * p3.y;
return Math.atan2(dy, dx) * (180 / Math.PI);
}
```
**Circle**:
```tsx
function pointOnCircle(cx, cy, radius, angleRadians) {
return {
x: cx + radius * Math.cos(angleRadians),
y: cy + radius * Math.sin(angleRadians)
};
}
// Usage
const angleRadians = progress * Math.PI * 2;
const pos = pointOnCircle(300, 300, 100, angleRadians);
const tangentAngle = (angleRadians * 180 / Math.PI) + 90; // Tangent is perpendicular
```
**Tips:**
- Use `progress` (0-1) for smooth animation
- The `translate(-50%, -50%)` centers the element on the path
- Combine rotation with the translate: `translate(-50%, -50%) rotate(${angle}deg)`
- For text following a path, you can animate individual characters at different progress values
## SVG Stroke Animations
Animate SVG path strokes with the **stroke-dash** classes, perfect for drawing or erasing line art, icons, and illustrations.
### How It Works
SVG stroke animations use `strokeDasharray` and `strokeDashoffset` CSS properties to create drawing effects:
1. **Enter animations** - Draw the stroke from start to finish
2. **Exit animations** - Erase the stroke from finish to start
3. **Loop animations** - Continuously draw and erase
### Format
All stroke-dash animations require the **path length** in brackets:
```tsx
enter-stroke-dash-[length]/start/duration
exit-stroke-dash-[length]/start/duration
loop-stroke-dash-[length]/duration
```
### Basic Examples
```tsx
export default function SVGAnimation({ tw }) {
return (
<svg width="400" height="200" viewBox="0 0 400 200">
{/* Draw a curve over 1 second */}
<path
d="M10 150 Q 95 10 180 150"
stroke="black"
strokeWidth={4}
fill="none"
style={tw('enter-stroke-dash-[300]/0/1000')}
/>
</svg>
);
}
```
### Enter Animations (Drawing)
Draw strokes from 0% to 100%:
```tsx
// Draw a 300px path over 1 second
<path style={tw('enter-stroke-dash-[300]/0/1000')} />
// Draw with spring easing
<path style={tw('ease-spring enter-stroke-dash-[500]/0/1500')} />
// Stagger multiple paths
<path style={tw('enter-stroke-dash-[200]/0/600')} />
<path style={tw('enter-stroke-dash-[200]/200/600')} />
<path style={tw('enter-stroke-dash-[200]/400/600')} />
```
### Exit Animations (Erasing)
Erase strokes from 100% to 0%:
```tsx
// Erase starting at 2000ms, lasting 500ms
<path style={tw('exit-stroke-dash-[300]/2000/500')} />
// Draw then erase the same path
<path style={tw('enter-stroke-dash-[400]/0/800 exit-stroke-dash-[400]/2200/800')} />
```
### Loop Animations
Continuously draw and erase:
```tsx
// Loop every 2 seconds (draws in first half, erases in second half)
<path style={tw('loop-stroke-dash-[300]/2000')} />
// Faster loop
<path style={tw('loop-stroke-dash-[200]/1000')} />
```
### Getting Path Length
To find the path length for your SVG:
```tsx
// In browser console or component:
const path = document.querySelector('path');
const length = path.getTotalLength();
console.log(length); // e.g., 347.89
```
Then use that value:
```tsx
<path style={tw('enter-stroke-dash-[347.89]/0/1000')} />
```
### Complete Example
```tsx
export default function DrawingEffect({ tw }) {
return (
<div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}>
<svg width="600" height="400" viewBox="0 0 600 400">
{/* Checkmark icon drawn in sequence */}
<path
d="M100 200 L 200 300 L 400 100"
stroke="#10b981"
strokeWidth={8}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
style={tw('ease-out enter-stroke-dash-[600]/0/1200')}
/>
{/* Circle drawn after checkmark */}
<circle
cx="250"
cy="200"
r="150"
stroke="#10b981"
strokeWidth={6}
fill="none"
style={tw('ease-out enter-stroke-dash-[942]/1000/1000')}
/>
</svg>
</div>
);
}
```
### Combining with Other Animations
Stroke animations work alongside other animation classes:
```tsx
// Fade in while drawing
<path style={tw('enter-stroke-dash-[300]/0/1000 enter-fade-in/0/1000')} />
// Draw with pulsing color
<svg>
<path
stroke="url(#gradient)"
style={tw('enter-stroke-dash-[500]/0/1500')}
/>
<defs>
<linearGradient id="gradient">
<stop offset="0%" stopColor="#8b5cf6" />
<stop offset="100%" stopColor="#ec4899" />
</linearGradient>
</defs>
</svg>
```
### Animated Dashed Strokes (Marching Ants)
For **marching ants** or **animated dashed patterns**, use `frame` or `progress` directly instead of animation classes:
```tsx
export default function MarchingAnts({ tw, frame }) {
// Calculate animated offset (loops every 30 frames)
const dashOffset = -(frame % 30) * 2;
return (
<div style={tw('flex items-center justify-center w-full h-full bg-gray-900')}>
<svg width="600" height="400" viewBox="0 0 600 400">
{/* Marching ants border */}
<rect
x="50"
y="50"
width="500"
height="300"
fill="none"
stroke="#3b82f6"
strokeWidth={3}
strokeDasharray="10 5"
strokeDashoffset={dashOffset}
/>
{/* Animated circle with different speed */}
<circle
cx="300"
cy="200"
r="80"
fill="none"
stroke="#10b981"
strokeWidth={4}
strokeDasharray="15 8"
strokeDashoffset={dashOffset * 1.5}
/>
</svg>
</div>
);
}
```
**Tips:**
- `strokeDasharray="10 5"` - 10px dash, 5px gap
- `strokeDashoffset={dashOffset}` - animates the pattern position
- Negative offset moves forward, positive moves backward
- Different speeds: multiply by different values (e.g., `dashOffset * 2`)
This technique is different from `stroke-dash` classes:
- **`stroke-dash` classes** - Draw/erase the stroke (reveal animation)
- **Marching ants** - Move a dashed pattern along the stroke
## Performance Tips
1. **Use Tailwind classes** when possible - they're optimized for the renderer
2. **Avoid too many nested animations** - each adds computation per frame
3. **Use loop animations sparingly** - they're computed every frame
4. **Prefer opacity and transform** - they're the most performant properties
## Next Steps
- [Templates](/templates) - Creating image and video templates
- [Helpers](/helpers) - QR codes, images, and more
# Template Helpers
Additional helpers for creating powerful, composable templates.
## Overview
Beyond the basics, loopwind provides:
- `template()` - Compose templates together
- `qr()` - Generate QR codes on the fly
- `config` - Access user configuration
For image embedding, see the [Images](/images) page.
## Template Composition
Compose multiple templates together to create complex designs.
### Usage
```tsx
export default function CompositeCard({ tw, template, title, author, avatar }) {
return (
<div style={tw('w-full h-full bg-gradient-to-br from-purple-600 to-blue-500 p-12')}>
<div style={tw('bg-white rounded-2xl p-8 shadow-xl')}>
<h1 style={tw('text-4xl font-bold text-gray-900 mb-6')}>{title}</h1>
{/* Embed another template */}
<div style={tw('mb-6')}>
{template('user-badge', {
name: author,
avatar: avatar
})}
</div>
<p style={tw('text-gray-600')}>Published by {author}</p>
</div>
</div>
);
}
```
**How it works:**
1. `template(name, props)` renders another installed template
2. The embedded template is rendered at its specified size
3. You can embed multiple templates in one design
4. Templates can be nested (template within a template)
### Use Cases
**1. Reusable components:**
```tsx
// Create a logo template once, use it everywhere
<div>{template('company-logo', { variant: 'dark' })}</div>
```
**2. Complex layouts:**
```tsx
// Combine multiple templates into one design
<div style={tw('grid grid-cols-2 gap-4')}>
{template('product-card', { product: product1 })}
{template('product-card', { product: product2 })}
</div>
```
**3. Dynamic content:**
```tsx
// Render templates based on data
{users.map(user =>
template('user-avatar', { name: user.name, image: user.avatar })
)}
```
### Best Practices
1. **Keep templates focused** - Each template should do one thing well
2. **Pass minimal props** - Only pass what the embedded template needs
3. **Document dependencies** - Note which templates are required in your README
4. **Avoid deep nesting** - Too many nested templates can be hard to debug
## QR Codes
Generate QR codes dynamically in your templates.
### Usage
```tsx
export default function QRCard({ tw, qr, title, url }) {
return (
<div style={tw('flex flex-col items-center justify-center w-full h-full bg-white p-10')}>
<h1 style={tw('text-4xl font-bold text-black mb-8')}>{title}</h1>
{/* Generate QR code for the URL */}
<img src={qr(url)} style={tw('w-64 h-64')} />
<p style={tw('text-gray-600 mt-4')}>{url}</p>
</div>
);
}
```
**Props format:**
```json
{
"title": "Scan Me",
"url": "https://example.com"
}
```
### QR Options
You can customize QR code appearance:
```tsx
// Basic QR code
<img src={qr('https://example.com')} />
// With error correction level
<img src={qr('https://example.com', { errorCorrectionLevel: 'H' })} />
// With custom size
<img src={qr('https://example.com', { width: 512 })} />
```
**Error correction levels:**
- `L` - Low (~7% correction)
- `M` - Medium (~15% correction) - default
- `Q` - Quartile (~25% correction)
- `H` - High (~30% correction)
## User Configuration
Access user settings from `.loopwind/loopwind.json` using the `config` prop:
```tsx
export default function BrandedTemplate({ tw, config, title }) {
// Access custom colors from loopwind.json
const primaryColor = config?.colors?.brand || '#6366f1';
return (
<div style={tw('w-full h-full p-12')}>
<h1 style={{
...tw('text-6xl font-bold'),
color: primaryColor
}}>
{title}
</h1>
</div>
);
}
```
**User's `.loopwind/loopwind.json`:**
```json title=".loopwind/loopwind.json"
{
"colors": {
"brand": "#ff6b6b"
},
"fonts": {
"sans": ["Inter", "system-ui", "sans-serif"]
}
}
```
This allows templates to adapt to user preferences and brand guidelines.
## Text on Path
Render text along curves, circles, and custom paths with automatic character positioning and rotation.
### Usage
```tsx
export default function CircleText({ tw, textPath, message }) {
return (
<div style={tw('relative w-full h-full bg-slate-900')}>
{textPath.onCircle(
message,
960, // center x
540, // center y
400, // radius
0, // rotation offset (0-1)
{
fontSize: "4xl",
fontWeight: "bold",
color: "white",
letterSpacing: 0.05
}
)}
</div>
);
}
```
### Available Functions
All `textPath` functions return an array of positioned character elements:
**`textPath.onCircle(text, cx, cy, radius, offset, options?)`**
```tsx
// Text around a circle
textPath.onCircle("HELLO WORLD", 960, 540, 400, 0, {
fontSize: "4xl",
color: "white"
})
```
**`textPath.onPath(text, pathFollower, options?)`**
```tsx
// Text along any custom path
textPath.onPath("CUSTOM PATH", (t) => ({
x: 100 + t * 800,
y: 200 + Math.sin(t * Math.PI) * 100,
angle: Math.cos(t * Math.PI) * 20
}), {
fontSize: "2xl",
fontWeight: "semibold"
})
```
**`textPath.onQuadratic(text, p0, p1, p2, options?)`**
```tsx
// Text along a quadratic Bezier curve
textPath.onQuadratic(
"CURVED TEXT",
{ x: 200, y: 400 }, // start
{ x: 960, y: 100 }, // control point
{ x: 1720, y: 400 }, // end
{ fontSize: "3xl", color: "blue-300" }
)
```
**`textPath.onCubic(text, p0, p1, p2, p3, options?)`**
```tsx
// Text along a cubic Bezier curve
textPath.onCubic(
"S-CURVE",
{ x: 200, y: 600 }, // start
{ x: 600, y: 400 }, // control 1
{ x: 1320, y: 800 }, // control 2
{ x: 1720, y: 600 }, // end
{ fontSize: "3xl", color: "purple-300" }
)
```
**`textPath.onArc(text, cx, cy, radius, startAngle, endAngle, options?)`**
```tsx
// Text along a circular arc
textPath.onArc(
"ARC TEXT",
960, // center x
540, // center y
400, // radius
0, // start angle (degrees)
180, // end angle (degrees)
{ fontSize: "2xl", color: "pink-300" }
)
```
### Options
All `textPath` functions accept an optional `options` object:
```typescript
{
fontSize?: string; // Tailwind size: "xl", "2xl", "4xl", etc.
fontWeight?: string; // Tailwind weight: "bold", "semibold", etc.
color?: string; // Tailwind color: "white", "blue-500", etc.
letterSpacing?: number; // Space between characters (0-1, default: 0)
style?: any; // Additional inline styles
}
```
### Examples
**Animated rotating text:**
```tsx
export default function RotatingText({ tw, textPath, progress }) {
return (
<div style={tw('relative w-full h-full bg-black')}>
{textPath.onCircle(
"SPINNING • TEXT • ",
960, 540, 400,
progress, // Rotate based on video progress
{ fontSize: "3xl", color: "yellow-300" }
)}
</div>
);
}
```
**Multiple text paths:**
```tsx
export default function MultiPath({ tw, textPath }) {
return (
<div style={tw('relative w-full h-full bg-gradient-to-br from-slate-900 to-slate-700')}>
{/* Text on outer circle */}
{textPath.onCircle(
"OUTER RING",
960, 540, 500, 0,
{ fontSize: "5xl", fontWeight: "bold", color: "white" }
)}
{/* Text on inner circle */}
{textPath.onCircle(
"inner ring",
960, 540, 300, 0.5, // offset by 50% for rotation
{ fontSize: "2xl", color: "white/60" }
)}
</div>
);
}
```
**Text following a drawn path:**
```tsx
export default function PathText({ tw, textPath }) {
return (
<div style={tw('relative w-full h-full bg-gray-900')}>
{/* Draw the path */}
<svg width="1920" height="1080" style={{ position: 'absolute' }}>
<path
d="M 200 400 Q 960 150 1720 400"
stroke="rgba(255,255,255,0.2)"
strokeWidth={2}
fill="none"
/>
</svg>
{/* Text following the path */}
{textPath.onQuadratic(
"FOLLOWING THE CURVE",
{ x: 200, y: 400 },
{ x: 960, y: 150 },
{ x: 1720, y: 400 },
{ fontSize: "3xl", fontWeight: "bold", color: "blue-300" }
)}
</div>
);
}
```
For animated text paths, see [Text Path Animations](/animation#text-path-animations).
## Reserved Prop Names
The following prop names are **reserved** and cannot be used in your template's
标签
skill
ai