Skill: tomtev/loopwind (version 0.25.11)

--- name: loopwind description: Generate images and videos from React + Tailwind CSS templates using the loopwind CLI. metadata: version: "0.25.11" --- # 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 [REDACTED_SECRET]** - 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 `meta.props`: - `tw`, `qr`, `image`, `template` - Core helpers - `path`, `textPath` - Path and text helpers - `config`, `frame`, `progress` - System props **Why?** These names are used for loopwind's built-in helpers. Using them as prop names would cause conflicts. **Example:** ```tsx // ❌ BAD - 'image' is reserved export const meta = { props: { title: "string", image: "string" // Error! } }; // ✅ GOOD - Use descriptive alternatives export const meta = { props: { title: "string", imageUrl: "string", // or imageSrc, photoUrl, etc. logoUrl: "string" } }; ``` If you try to use a reserved name, you'll get a helpful error: ``` Template uses reserved prop names: image Try renaming: "image" → "imageUrl" or "imageSrc" Reserved names: tw, qr, image, template, path, textPath, config, frame, progress ``` ## All Props Reference Every template receives these props: ```tsx export default function MyTemplate({ // Core helpers (RESERVED - cannot be used as prop names) tw, // Tailwind class converter qr, // QR code generator (this page) template, // Template composer (this page) config, // User config from loopwind.json (this page) textPath, // Text on path helpers (this page) // Media helpers (RESERVED) image, // Image embedder → see /images path, // Path following → see /animation // Video-specific (RESERVED - only in video templates) frame, // Current frame number → see /templates progress, // Animation progress 0-1 → see /templates // Your custom props (use any names EXCEPT the reserved ones above) ...props // Any props from your meta.props }) { // Your template code } ``` ## Next Steps - [Embedding Images](/images) - [Templates](/templates) - [Styling with Tailwind & shadcn/ui](/styling) - [Custom Fonts](/fonts) # Styling Templates Style your templates with Tailwind utility classes and shadcn/ui's beautiful design system. ## Quick Start ```tsx export default function MyTemplate({ title, tw }) { 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-7xl font-bold text-white')}> {title} </h1> </div> ); } ``` ## The `tw()` Function Every template receives a `tw()` function that converts Tailwind classes to inline styles compatible with Satori: ```tsx // Tailwind classes tw('flex items-center justify-center p-8 bg-blue-500') // Converts to inline styles: { display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '2rem', backgroundColor: '#3b82f6' } ``` ### Basic Usage ```tsx export default function Banner({ title, subtitle, tw }) { return ( <div style={tw('w-full h-full p-12 bg-gray-50')}> <h1 style={tw('text-6xl font-bold text-gray-900 mb-4')}> {title} </h1> <p style={tw('text-2xl text-gray-600')}> {subtitle} </p> </div> ); } ``` ### Combining with Custom Styles Mix Tailwind classes with custom styles using the spread operator: ```tsx export default function CustomGradient({ title, tw }) { return ( <div style={{ ...tw('flex flex-col items-center justify-center w-full h-full p-20'), background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', }} > <h1 style={tw('text-8xl font-bold text-white')}>{title}</h1> </div> ); } ``` ## shadcn/ui Design System loopwind uses **shadcn/ui's design system** by default, providing semantic color tokens for beautiful, consistent designs. ### Default Color Palette All templates automatically have access to these semantic colors defined in `.loopwind/loopwind.json`: ```typescript colors: { // Primary colors primary: '#18181b', // Main brand color 'primary-foreground': '#fafafa', // Secondary colors secondary: '#f4f4f5', // Subtle accents 'secondary-foreground': '#18181b', // Background background: '#ffffff', // Page background foreground: '#09090b', // Main text color // Muted muted: '#f4f4f5', // Subtle backgrounds 'muted-foreground': '#71717a', // Muted text // Accent accent: '#f4f4f5', // Highlight color 'accent-foreground': '#18181b', // Destructive destructive: '#ef4444', // Error/danger states 'destructive-foreground': '#fafafa', // UI Elements border: '#e4e4e7', // Border color input: '#e4e4e7', // Input borders ring: '#18181b', // Focus rings card: '#ffffff', // Card background 'card-foreground': '#09090b', } ``` ### Using Semantic Colors ```tsx export default function SemanticCard({ title, description, price, tw }) { return ( <div style={tw('bg-card border border-border rounded-lg p-6')}> <h2 style={tw('text-card-foreground text-2xl font-bold mb-2')}> {title} </h2> <p style={tw('text-muted-foreground mb-4')}> {description} </p> <div style={tw('text-primary text-3xl font-bold')}> ${price} </div> </div> ); } ``` ### Opacity Modifiers Use Tailwind's slash syntax for opacity with any color: ```tsx export default function OpacityExample({ tw }) { return ( <div style={tw('bg-primary/50')}> {/* 50% opacity */} <p style={tw('text-muted-foreground/75')}> {/* 75% opacity */} Subtle text </p> <div style={tw('border border-border/30')}> {/* 30% opacity */} Faint border </div> </div> ); } ``` **Supported syntax:** - `bg-{color}/{opacity}` - Background with opacity - `text-{color}/{opacity}` - Text with opacity - `border-{color}/{opacity}` - Border with opacity ### Text Hierarchy ```tsx // Primary text tw('text-foreground') // Secondary/muted text tw('text-muted-foreground') // Accent/brand text tw('text-primary') // Destructive/error text tw('text-destructive') ``` ### Backgrounds ```tsx // Page background tw('bg-background') // Card/elevated surfaces tw('bg-card') // Subtle backgrounds tw('bg-muted') // Accent backgrounds tw('bg-accent') ``` ## Supported Tailwind Classes ### Layout - **Display**: `flex`, `inline-flex`, `block`, `inline-block`, `hidden` - **Flex Direction**: `flex-row`, `flex-col`, `flex-row-reverse`, `flex-col-reverse` - **Justify**: `justify-start`, `justify-end`, `justify-center`, `justify-between`, `justify-around` - **Align**: `items-start`, `items-end`, `items-center`, `items-baseline`, `items-stretch` ### Spacing - **Padding**: `p-{n}`, `px-{n}`, `py-{n}`, `pt-{n}`, `pb-{n}`, `pl-{n}`, `pr-{n}` - **Margin**: `m-{n}`, `mx-{n}`, `my-{n}`, `mt-{n}`, `mb-{n}`, `ml-{n}`, `mr-{n}` - **Gap**: `gap-{n}`, `gap-x-{n}`, `gap-y-{n}` - **Sizes**: 0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56, 64 Examples: ```tsx tw('p-4') // padding: 1rem tw('px-8') // paddingLeft: 2rem, paddingRight: 2rem tw('m-6') // margin: 1.5rem tw('gap-4') // gap: 1rem ``` ### Sizing - **Width**: `w-{n}`, `w-full`, `w-screen`, `w-1/2`, `w-1/3`, `w-2/3` - **Height**: `h-{n}`, `h-full`, `h-screen` Examples: ```tsx tw('w-full') // width: 100% tw('h-64') // height: 16rem tw('w-1/2') // width: 50% ``` ### Typography - **Font Size**: `text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`, `text-3xl`, `text-4xl`, `text-5xl`, `text-6xl`, `text-7xl`, `text-8xl`, `text-9xl` - **Font Weight**: `font-thin`, `font-light`, `font-normal`, `font-medium`, `font-semibold`, `font-bold`, `font-extrabold`, `font-black` - **Text Align**: `text-left`, `text-center`, `text-right` - **Line Height**: `leading-none`, `leading-tight`, `leading-normal`, `leading-relaxed`, `leading-loose` ### Colors All standard Tailwind colors plus shadcn semantic colors: **Standard colors:** - `text-{color}-{shade}`, `bg-{color}-{shade}`, `border-{color}-{shade}` - Colors: `red`, `blue`, `green`, `yellow`, `purple`, `pink`, `gray`, `indigo`, `teal`, `orange` - Shades: `50`, `100`, `200`, `300`, `400`, `500`, `600`, `700`, `800`, `900` **shadcn semantic colors:** - `text-foreground`, `text-primary`, `text-muted-foreground`, `text-destructive` - `bg-background`, `bg-card`, `bg-muted`, `bg-accent`, `bg-primary` - `border-border`, `border-input` ```tsx tw('text-blue-500') // Standard Tailwind color tw('bg-purple-600') // Standard Tailwind color tw('text-primary') // shadcn semantic color tw('bg-card') // shadcn semantic color ``` ### Position & Layout - **Position**: `relative`, `absolute`, `fixed`, `sticky` - **Inset**: `inset-0`, `top-0`, `bottom-0`, `left-0`, `right-0` - **Z-Index**: `z-0`, `z-10`, `z-20`, `z-30`, `z-40`, `z-50` ### Borders - **Border Width**: `border`, `border-{n}`, `border-t`, `border-b`, `border-l`, `border-r` - **Border Radius**: `rounded`, `rounded-sm`, `rounded-md`, `rounded-lg`, `rounded-xl`, `rounded-2xl`, `rounded-3xl`, `rounded-full` - **Border Color**: `border-{color}-{shade}`, `border-border`, `border-input` ### Effects - **Shadow**: `shadow-sm`, `shadow`, `shadow-md`, `shadow-lg`, `shadow-xl`, `shadow-2xl` - **Opacity**: `opacity-0`, `opacity-25`, `opacity-50`, `opacity-75`, `opacity-100` ### Filters - **Blur**: `blur-none`, `blur-sm`, `blur`, `blur-md`, `blur-lg`, `blur-xl` - **Brightness**: `brightness-0`, `brightness-50`, `brightness-100`, `brightness-150`, `brightness-200` - **Contrast**: `contrast-0`, `contrast-50`, `contrast-100`, `contrast-150`, `contrast-200` ## Gradients ### Linear Gradients ```tsx // Gradient direction tw('bg-gradient-to-r') // left to right tw('bg-gradient-to-br') // top-left to bottom-right tw('bg-gradient-to-t') // bottom to top // Gradient colors tw('from-blue-500') // Start color tw('via-purple-500') // Middle color tw('to-pink-500') // End color // Complete gradient tw('bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500') ``` ### Gradient Examples ```tsx export default function GradientCard({ title, tw }) { return ( <div style={tw('w-full h-full bg-gradient-to-br from-cyan-500 to-blue-600 p-12')}> <h1 style={tw('text-white text-6xl font-bold')}> {title} </h1> </div> ); } ``` ## Custom Theme Colors You can override the default shadcn colors or add your own custom colors in `.loopwind/loopwind.json`: ```json title=".loopwind/loopwind.json" { "theme": { "colors": { "primary": "#3b82f6", "primary-foreground": "#ffffff", "accent": "#10b981", "brand": "#ff6b6b" } } } ``` Then use these custom colors in your templates: ```tsx tw('text-brand') // Uses your custom brand color tw('bg-primary') // Uses your custom primary color tw('bg-accent') // Uses your custom accent color ``` ## Auto-Detection from tailwind.config.js loopwind automatically detects and loads your project's Tailwind configuration: ``` your-project/ ├── tailwind.config.js ← Automatically detected └── .loopwind/ ├── loopwind.json └── templates/ ``` This includes: - Custom colors - Custom spacing values - Custom fonts - Theme extensions - Custom utilities ## Complete Example ```tsx export default function ModernCard({ tw, image, title, description, category, author, avatar }) { return ( <div style={tw('w-full h-full bg-card')}> {/* Hero image */} <div style={tw('relative h-2/3')}> <img src={image(hero)} style={tw('w-full h-full object-cover')} /> {/* Category badge */} <div style={tw('absolute top-4 left-4 bg-primary/90 backdrop-blur px-4 py-2 rounded-full')}> <span style={tw('text-sm font-semibold text-primary-foreground')}> {category} </span> </div> </div> {/* Content */} <div style={tw('h-1/3 p-8 flex flex-col justify-between')}> <div> <h2 style={tw('text-3xl font-bold text-foreground mb-2')}> {title} </h2> <p style={tw('text-muted-foreground line-clamp-2')}> {description} </p> </div> {/* Author */} <div style={tw('flex items-center gap-3')}> <img src={image(avatar)} style={tw('w-10 h-10 rounded-full border-2 border-border')} /> <span style={tw('text-sm text-muted-foreground')}> {author} </span> </div> </div> </div> ); } ``` ## Why This Approach? - **Semantic naming**: `text-primary` instead of `text-blue-600` - **Consistency**: All templates use the same design language - **Flexibility**: Easy to customize entire theme - **Accessibility**: Pre-tested color contrasts - **Modern**: Same system as shadcn/ui components - **Familiar**: Standard Tailwind syntax ## Next Steps - [Custom Fonts](/fonts) - [Built-in Helpers](/helpers) - [Templates](/templates) - [Embedding Images](/images) # Font Handling in loopwind The recommended way to use fonts is through `loopwind.json` - configure fonts once, use everywhere. ## Using Fonts from loopwind.json (Recommended) Configure fonts in your `.loopwind/loopwind.json` and use Tailwind classes in templates. ### Simple Setup Define font families in `.loopwind/loopwind.json` without loading custom fonts (uses system fonts): ```json title=".loopwind/loopwind.json" { "fonts": { "sans": ["Inter", "system-ui", "-apple-system", "sans-serif"], "serif": ["Georgia", "serif"], "mono": ["Courier New", "monospace"] } } ``` **Template usage:** ```tsx export default function({ title, tw }) { return ( <div style={tw('w-full h-full')}> {/* Uses fonts.sans from loopwind.json */} <h1 style={tw('font-sans text-6xl font-bold')}> {title} </h1> {/* Uses fonts.mono from loopwind.json */} <code style={tw('font-mono text-sm')}> {code} </code> </div> ); } ``` **Result:** Uses system fonts, falls back to Inter for rendering. ### Complete Setup (With Font Files) Load custom font files for brand-specific typography in `.loopwind/loopwind.json`: ```json title=".loopwind/loopwind.json" { "fonts": { "sans": { "family": ["Inter", "system-ui", "sans-serif"], "files": [ { "path": "./fonts/Inter-Regular.woff", "weight": 400 }, { "path": "./fonts/Inter-Bold.woff", "weight": 700 } ] }, "mono": { "family": ["JetBrains Mono", "monospace"], "files": [ { "path": "./fonts/JetBrainsMono-Regular.woff", "weight": 400 } ] } } } ``` **Project structure:** ``` your-project/ ├── .loopwind/ │ ├── loopwind.json │ └── templates/ └── fonts/ ├── Inter-Regular.woff ├── Inter-Bold.woff └── JetBrainsMono-Regular.woff ``` **Template usage (same as before):** ```tsx <h1 style={tw('font-sans font-bold')}> {/* Uses Inter Bold from loopwind.json */} {title} </h1> ``` **Available classes:** - `font-sans` - Uses `fonts.sans` from loopwind.json - `font-serif` - Uses `fonts.serif` from loopwind.json - `font-mono` - Uses `fonts.mono` from loopwind.json **Supported formats:** - ✅ **WOFF** (`.woff`) - Recommended for best compatibility - ✅ **TTF** (`.ttf`) - Also supported - ✅ **OTF** (`.otf`) - Also supported - ❌ **WOFF2** (`.woff2`) - Not supported by renderer ## Font Loading Priority loopwind loads fonts in this order: 1. **loopwind.json fonts** (if configured with `files`) 2. **Bundled Inter fonts** (included with CLI) This ensures fonts work out of the box with no configuration. ## Default Fonts If no fonts are configured, loopwind uses **Inter** (Regular 400, Bold 700) which is bundled with the CLI. This means fonts work offline with no configuration required. ## Best Practices 1. ✅ **Use loopwind.json for project-wide fonts** - Configure once, use everywhere 2. ✅ **Use font classes** - `tw('font-sans')` instead of `fontFamily: 'Inter'` 3. ✅ **Include fallbacks** - Always add system fonts: `["Inter", "system-ui", "sans-serif"]` 4. ✅ **Match names** - First font in `family` array is used as the loaded font name 5. ✅ **Relative paths** - Font paths are relative to `loopwind.json` location ## Examples ### Minimal Setup (System Fonts) ```json title=".loopwind/loopwind.json" { "fonts": { "sans": ["Inter", "-apple-system", "sans-serif"] } } ``` Uses system Inter if available, falls back to Noto Sans for rendering. ### Brand Fonts Setup ```json title=".loopwind/loopwind.json" { "fonts": { "sans": { "family": ["Montserrat", "sans-serif"], "files": [ { "path": "./fonts/Montserrat-Regular.woff", "weight": 400 }, { "path": "./fonts/Montserrat-Bold.woff", "weight": 700 } ] } } } ``` Loads and uses Montserrat for all templates. ### Multi-Font Setup ```json title=".loopwind/loopwind.json" { "fonts": { "sans": { "family": ["Inter", "sans-serif"], "files": [ { "path": "./fonts/Inter-Regular.woff", "weight": 400 }, { "path": "./fonts/Inter-Bold.woff", "weight": 700 } ] }, "serif": { "family": ["Playfair Display", "serif"], "files": [ { "path": "./fonts/Playfair-Regular.woff", "weight": 400 } ] }, "mono": { "family": ["Fira Code", "monospace"], "files": [ { "path": "./fonts/FiraCode-Regular.woff", "weight": 400 } ] } } } ``` Loads different fonts for each style class. ### External Font URLs Load fonts directly from CDNs without downloading files: ```json title=".loopwind/loopwind.json" { "fonts": { "sans": { "family": ["Inter", "sans-serif"], "files": [ { "path": "https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-400-normal.woff", "weight": 400 }, { "path": "https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-700-normal.woff", "weight": 700 } ] } } } ``` You can also mix local and external fonts: ```json title=".loopwind/loopwind.json" { "fonts": { "sans": { "family": ["Inter", "sans-serif"], "files": [ { "path": "./fonts/Inter-Regular.woff", "weight": 400 }, { "path": "https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-700-normal.woff", "weight": 700 } ] } } } ``` **Note:** Use WOFF format (`.woff`) for best compatibility. WOFF2 is not supported by the underlying renderer. ## Performance - ✅ **Font caching** - Fonts load once and are cached for all renders - ✅ **Video optimization** - 90-frame video loads fonts once, not 90 times - ✅ **No CDN delays** - Local fonts load instantly ## Next Steps - [Styling with Tailwind & shadcn/ui](/styling) - [Built-in Helpers](/helpers) - [Templates](/templates) - [Embedding Images](/images) # loopwind.json Configure colors and fonts for all your templates in `.loopwind/loopwind.json`. ## File Location ``` your-project/ ├── .loopwind/ │ ├── loopwind.json ← Configuration file │ └── templates/ ``` ## Minimal Example ```json title=".loopwind/loopwind.json" { "theme": { "colors": { "primary": "#3b82f6", "background": "#ffffff" } } } ``` ## Theme Colors ### Default shadcn/ui Palette ```json title=".loopwind/loopwind.json" { "theme": { "colors": { "primary": "#18181b", "primary-foreground": "#fafafa", "secondary": "#f4f4f5", "secondary-foreground": "#18181b", "background": "#ffffff", "foreground": "#09090b", "muted": "#f4f4f5", "muted-foreground": "#71717a", "accent": "#f4f4f5", "accent-foreground": "#18181b", "destructive": "#ef4444", "destructive-foreground": "#fafafa", "border": "#e4e4e7", "input": "#e4e4e7", "ring": "#18181b", "card": "#ffffff", "card-foreground": "#09090b" } } } ``` ### Custom Colors ```json title=".loopwind/loopwind.json" { "theme": { "colors": { "primary": "#3b82f6", "brand": "#ff6b6b", "success": "#22c55e", "warning": "#f59e0b" } } } ``` **Use in templates:** ```tsx tw('text-brand') // #ff6b6b tw('bg-success') // #22c55e tw('border-warning') // #f59e0b ``` ## Fonts ### Simple (System Fonts) ```json title=".loopwind/loopwind.json" { "fonts": { "sans": ["Inter", "system-ui", "sans-serif"], "serif": ["Georgia", "serif"], "mono": ["Courier New", "monospace"] } } ``` ### With Font Files ```json title=".loopwind/loopwind.json" { "fonts": { "sans": { "family": ["Inter", "system-ui", "sans-serif"], "files": [ { "path": "./fonts/Inter-Regular.woff", "weight": 400 }, { "path": "./fonts/Inter-Bold.woff", "weight": 700 } ] } } } ``` Paths are relative to `loopwind.json`. **Supported formats:** - ✅ WOFF (`.woff`) - ✅ TTF (`.ttf`) - ✅ OTF (`.otf`) - ❌ WOFF2 (`.woff2`) ### External URLs ```json title=".loopwind/loopwind.json" { "fonts": { "sans": { "family": ["Inter", "sans-serif"], "files": [ { "path": "https://unpkg.com/@fontsource/inter@5.0.18/files/inter-latin-400-normal.woff", "weight": 400 } ] } } } ``` ## Complete Example ```json title=".loopwind/loopwind.json" { "theme": { "colors": { "primary": "#6366f1", "primary-foreground": "#ffffff", "background": "#ffffff", "foreground": "#0f172a", "muted": "#f1f5f9", "muted-foreground": "#64748b", "border": "#e2e8f0", "card": "#ffffff", "brand": "#8b5cf6" } }, "fonts": { "sans": { "family": ["Inter", "sans-serif"], "files": [ { "path": "./fonts/Inter-Regular.woff", "weight": 400 }, { "path": "./fonts/Inter-Bold.woff", "weight": 700 } ] }, "serif": { "family": ["Playfair Display", "serif"], "files": [ { "path": "./fonts/Playfair-Regular.woff", "weight": 400 } ] } } } ``` ## Schema ```typescript { "theme"?: { "colors"?: { [name: string]: string; // Hex color } }, "fonts"?: { [class: string]: string[] | { family: string[]; files: Array<{ path: string; // Local or URL weight: number; // 100-900 }>; } } } ``` ## Auto-Detection If no `loopwind.json` exists, loopwind auto-detects `tailwind.config.js`: ``` your-project/ ├── tailwind.config.js ← Auto-detected └── .loopwind/ └── templates/ ``` **Priority:** 1. `.loopwind/loopwind.json` 2. `tailwind.config.js` 3. Built-in defaults ## Next Steps - [Styling](/styling) - Use colors in templates - [Fonts](/fonts) - Font configuration details - [Getting Started](/getting-started) - Setup guide


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: clean (LOW)