diff --git a/packages/motion-presets/docs/presets/README.md b/packages/motion-presets/docs/presets/README.md deleted file mode 100644 index 46bad520..00000000 --- a/packages/motion-presets/docs/presets/README.md +++ /dev/null @@ -1,176 +0,0 @@ -# Animation Presets Reference - -Complete documentation for all 82+ animation presets in Wix Motion, organized by category with detailed configuration options, usage examples, and best practices. - -## ๐Ÿ“ Directory Structure - -### ๐ŸŽญ [Entrance Animations](entrance/) (19 presets) - -Perfect for element reveals and page transitions. - -**Featured Presets**: [FadeIn](entrance/fade-in.md) โ€ข [ArcIn](entrance/arc-in.md) โ€ข [BounceIn](entrance/bounce-in.md) โ€ข [SlideIn](entrance/slide-in.md) โ€ข [FlipIn](entrance/flip-in.md) - -### ๐Ÿ”„ [Ongoing Animations](ongoing/) (16 presets) - -Continuous looping animations for attention and delight. - -**Featured Presets**: [Pulse](ongoing/pulse.md) โ€ข [Breathe](ongoing/breathe.md) โ€ข [Spin](ongoing/spin.md) โ€ข [Wiggle](ongoing/wiggle.md) โ€ข [Bounce](ongoing/bounce.md) - -### ๐Ÿ“œ [Scroll Animations](scroll/) (19 presets) - -Scroll-driven effects for immersive storytelling. - -**Featured Presets**: [ParallaxScroll](scroll/parallax-scroll.md) โ€ข [FadeScroll](scroll/fade-scroll.md) โ€ข [GrowScroll](scroll/grow-scroll.md) โ€ข [RevealScroll](scroll/reveal-scroll.md) โ€ข [TiltScroll](scroll/tilt-scroll.md) - -### ๐Ÿ–ฑ๏ธ [Mouse Animations](mouse/) (12 presets) - -Interactive pointer-driven effects. - -**Featured Presets**: [TrackMouse](mouse/track-mouse.md) โ€ข [Tilt3DMouse](mouse/tilt-3d-mouse.md) โ€ข [ScaleMouse](mouse/scale-mouse.md) โ€ข [BlurMouse](mouse/blur-mouse.md) - -## ๐Ÿ” Quick Reference - -### By Complexity - -- **Simple**: Single-property animations, minimal configuration -- **Medium**: Multi-property effects with directional controls -- **Complex**: Advanced 3D transforms, multi-stage animations - -### By Use Case - -- **UI Elements**: Buttons, cards, modals, tooltips -- **Content Blocks**: Text, images, sections, articles -- **Navigation**: Menus, tabs, drawers, overlays -- **Media**: Backgrounds, videos, galleries, hero sections -- **Interactive**: Hover effects, cursor followers, 3D showcases - -## ๐Ÿ“– Using This Reference - -Each preset page includes: - -### ๐Ÿ“‹ **Overview** - -- Animation description and visual behavior -- Complexity level and performance characteristics -- Best use cases and target elements - -### โš™๏ธ **Configuration** - -- Required and optional parameters -- Default values and ranges -- TypeScript interface definitions - -### ๐Ÿ’ป **Code Examples** - -- Basic usage with `getWebAnimation()` -- CSS mode with `getCSSAnimation()` -- Advanced configurations and combinations - -### ๐ŸŽฏ **Use Cases** - -- Common implementation patterns -- Framework integration examples -- Real-world scenarios and tips - -### ๐Ÿ”— **Related Animations** - -- Similar effects in the same category -- Complementary animations for sequences -- Alternative approaches for different contexts - -## ๐Ÿ› ๏ธ Common Patterns - -### CSS Custom Properties - -Motion presets respect certain CSS custom properties set on your elements. This allows animations to work seamlessly with your existing styles. - -#### `--motion-rotate` - -If your element has a rotation applied via CSS transform, set the `--motion-rotate` custom property to preserve it during animations: - -```css -.rotated-element { - --motion-rotate: 45deg; - transform: rotate(45deg); -} -``` - -Without this property, animations that manipulate transforms may reset your element's rotation to `0deg`. By setting `--motion-rotate`, the preset includes your rotation in all transform calculations. - -### Basic Animation Creation - -```typescript -import { getWebAnimation } from '@wix/motion'; - -const animation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 800, - easing: 'easeOut', -}); - -await animation.play(); -``` - -### Scroll-Driven Animation - -```typescript -const animation = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 0.5, - }, - }, - { - trigger: 'view-progress', - element, - }, -); -``` - -### Mouse Animation - -```typescript -const mouseAnimation = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 15, - perspective: 800, - }, - }, - { - trigger: 'pointer-move', - element: containerElement, - }, -); -``` - -## ๐ŸŽฎ Interactive Examples - -Many presets include live examples in our [Storybook playground](../../playground/). Look for the "โ–ถ๏ธ Try it" links in individual preset documentation. - -## ๐Ÿ“ฑ Mobile Considerations - -Preset documentation includes specific guidance for: - -- **Touch Device Compatibility**: Which animations work well on mobile -- **Performance Optimization**: Lighter alternatives for resource-constrained devices -- **Reduced Motion Support**: Accessibility-friendly variations - -## ๐Ÿ”„ Migration Guide - -When upgrading or changing animations: - -- **Version Compatibility**: Breaking changes and migration paths -- **Deprecation Notices**: Sunset timelines for older presets -- **Alternative Recommendations**: Modern replacements for legacy effects - ---- - -**Ready to explore?** Click on any category above to browse individual preset documentation, or use the search function to find specific animations by name or use case. diff --git a/packages/motion-presets/docs/presets/_template.md b/packages/motion-presets/docs/presets/_template.md deleted file mode 100644 index 23b8f2e4..00000000 --- a/packages/motion-presets/docs/presets/_template.md +++ /dev/null @@ -1,301 +0,0 @@ -# [Animation Name] - -> **Template for individual preset documentation. Remove this note when creating actual preset docs.** - -[Brief description of what the animation does and its visual effect] - -## Overview - -**Category**: [Entrance/Ongoing/Scroll/Mouse/Background Scroll] -**Complexity**: [Simple/Medium/Complex] -**Performance**: [GPU Optimized/CPU Efficient/Moderate] -**Mobile Friendly**: [Yes/No/With Optimization] - -### Best Use Cases - -- [Primary use case] -- [Secondary use case] -- [Third use case] - -### Target Elements - -- [Type of elements this works best with] -- [Recommended containers/layouts] - -## Configuration - -### TypeScript Interface - -```typescript -export type [AnimationName] = BaseDataItemLike<'[AnimationName]'> & { - [property]: [type]; // [description] - [optionalProperty]?: [type]; // [description] -}; -``` - -### Parameters - -| Parameter | Type | Default | Description | Examples | -| ---------- | -------- | ----------- | ------------- | ------------------ | -| `[param1]` | `[type]` | `[default]` | [Description] | `[example values]` | -| `[param2]` | `[type]` | `[default]` | [Description] | `[example values]` | - -### Directional Support (if applicable) - -- **Four directions**: `top`, `right`, `bottom`, `left` -- **Eight directions**: Includes corners like `top-right`, `bottom-left` -- **Angles**: Numeric degrees (0ยฐ = up, 90ยฐ = right, etc.) - -## Usage Examples - -### Basic Usage - -```typescript -import { getWebAnimation } from '@wix/motion'; - -const animation = getWebAnimation(element, { - type: '[TimeAnimationOptions/ScrubAnimationOptions]', - namedEffect: { - type: '[AnimationName]', - // Basic configuration - }, - duration: 1000, // For time-based animations - easing: 'easeOut', -}); - -await animation.play(); -``` - -### Advanced Configuration - -```typescript -const advancedAnimation = getWebAnimation(element, { - type: '[TimeAnimationOptions/ScrubAnimationOptions]', - namedEffect: { - type: '[AnimationName]', - [param1]: [value], - [param2]: [value], - }, - duration: 1200, - delay: 200, - easing: 'backOut', -}); -``` - -### CSS Mode (if supported) - -```typescript -import { getCSSAnimation } from '@wix/motion'; - -const cssRules = getCSSAnimation('elementId', { - type: 'TimeAnimationOptions', - namedEffect: { type: '[AnimationName]' }, - duration: 800, -}); - -// Insert CSS rules into stylesheet -``` - -### Scroll Animation (if applicable) - -```typescript -const animation = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: '[AnimationName]', - [scrollSpecificParams]: [values], - }, - }, - { - trigger: 'view-progress', - element, - }, -); -``` - -### Mouse Animation (if applicable) - -```typescript -const mouseAnimation = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: '[AnimationName]', - [mouseSpecificParams]: [values], - }, - }, - { - trigger: 'pointer-move', - element: containerElement, - }, -); -``` - -## Common Patterns - -### [Pattern Name 1] - -```typescript -// [Description of when to use this pattern] -const [patternExample] = getWebAnimation(element, { - // Configuration for this pattern -}); -``` - -### [Pattern Name 2] - -```typescript -// [Description of when to use this pattern] -const [patternExample] = getWebAnimation(element, { - // Configuration for this pattern -}); -``` - -### Staggered Animation (if applicable) - -```typescript -// [Description of staggering approach] -elements.forEach((el, index) => { - const animation = getWebAnimation(el, { - type: 'TimeAnimationOptions', - namedEffect: { type: '[AnimationName]' }, - duration: 600, - delay: index * 100, // 100ms stagger - }); - animation.play(); -}); -``` - -## Framework Integration - -### React - -```typescript -import React, { useEffect, useRef } from 'react'; -import { getWebAnimation } from '@wix/motion'; - -function Animated[Component]({ children }) { - const elementRef = useRef(null); - - useEffect(() => { - if (!elementRef.current) return; - - const animation = getWebAnimation(elementRef.current, { - type: 'TimeAnimationOptions', - namedEffect: { type: '[AnimationName]' }, - duration: 800 - }); - - animation.play(); - - return () => animation.cancel(); - }, []); - - return ( -
- {children} -
- ); -} -``` - -### Vue - -```vue - - - -``` - -## Performance Tips - -- [Performance tip 1] -- [Performance tip 2] -- [Performance tip 3] - -### Mobile Optimization - -- [Mobile-specific optimization] -- [Reduced motion alternative] -- [Battery-conscious approach] - -## Accessibility - -### Reduced Motion Support - -```typescript -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -if (prefersReducedMotion) { - // Alternative approach or disabled animation -} else { - // Full animation experience -} -``` - -### Focus Management (if applicable) - -- [Guidelines for maintaining focus] -- [Screen reader considerations] - -## Related Animations - -### Same Category - -- **[Related Animation 1]** - [Brief comparison] -- **[Related Animation 2]** - [Brief comparison] - -### Other Categories - -- **[Cross-category Animation 1]** - [When to use instead] -- **[Cross-category Animation 2]** - [How to combine] - -### Complementary Effects - -- **[Animation that works well before this]** -- **[Animation that works well after this]** -- **[Animation that works well alongside this]** - -## Troubleshooting - -### Common Issues - -- **[Issue 1]**: [Solution] -- **[Issue 2]**: [Solution] -- **[Issue 3]**: [Solution] - -### Debug Tips - -- [Debug tip 1] -- [Debug tip 2] - ---- - -## Interactive Example - -โ–ถ๏ธ **[Try it in Storybook](../../playground/)** - [Link to specific story if available] - ---- - -**[Back to [Category] Animations](../[category]/) | [Back to All Presets](../)** diff --git a/packages/motion-presets/docs/presets/background-scroll/README.md b/packages/motion-presets/docs/presets/background-scroll/README.md deleted file mode 100644 index 2db91bf7..00000000 --- a/packages/motion-presets/docs/presets/background-scroll/README.md +++ /dev/null @@ -1,541 +0,0 @@ -> **NOT PRODUCTION READY** โ€” Background-scroll presets are experimental and not available in production builds. Do not use them in production code or reference them in user-facing documentation. - -# Background Scroll Animations - -Specialized effects designed for background media elements and hero sections. Perfect for creating immersive backgrounds, parallax landscapes, and cinematic scroll experiences. - -## Complete Preset List (12 presets) - -### ๐ŸŒŠ Parallax & Movement - -| Animation | Complexity | Measurement | Directions | Description | -| -------------------------------------- | ---------- | ----------- | ---------- | ------------------------------------ | -| **[BgParallax](bg-parallax.md)** | Simple | โœ“ | - | Classic background parallax movement | -| **[ImageParallax](image-parallax.md)** | Medium | โœ“ | - | Enhanced image parallax with options | -| **[BgPan](bg-pan.md)** | Simple | โœ“ | 2-way | Horizontal panning movement | - -### ๐Ÿ” Zoom & Scale - -| Animation | Complexity | Measurement | Directions | Description | -| --------------------------------- | ---------- | ----------- | ---------- | --------------------------- | -| **[BgZoom](bg-zoom.md)** | Complex | โœ“ | 2-way | Dynamic zoom in/out effects | -| **[BgCloseUp](bg-close-up.md)** | Medium | โœ“ | - | Perspective zoom with fade | -| **[BgPullBack](bg-pull-back.md)** | Medium | โœ“ | - | 3D pull-back effect | - -### ๐ŸŽญ Fade & Opacity - -| Animation | Complexity | Measurement | Description | -| --------------------------------- | ---------- | ----------- | ---------------------------- | -| **[BgFade](bg-fade.md)** | Simple | โœ“ | Directional fade transitions | -| **[BgFadeBack](bg-fade-back.md)** | Medium | โœ“ | Scale + fade combination | - -### ๐Ÿ”„ Rotation & Transform - -| Animation | Complexity | Measurement | Directions | Description | -| ---------------------------- | ---------- | ----------- | ---------- | ----------------------- | -| **[BgRotate](bg-rotate.md)** | Simple | - | 2-way | Smooth rotation effects | -| **[BgSkew](bg-skew.md)** | Medium | โœ“ | 2-way | Skewing transformation | - -### ๐ŸŽจ Advanced 3D - -| Animation | Complexity | Measurement | Description | -| ----------------------------- | ---------- | ----------- | ----------------------- | -| **[BgFake3D](bg-fake-3d.md)** | Complex | โœ“ | Multi-layer 3D parallax | - -### ๐Ÿ› ๏ธ Utility - -| Animation | Complexity | Description | -| ---------------------------- | ---------- | ----------------------- | -| **[BgReveal](bg-reveal.md)** | Simple | Measurement-only reveal | - -## Quick Reference - -### By Use Case - -#### Hero Sections - -**Best**: BgParallax, BgZoom, BgFade -**Alternative**: ImageParallax, BgCloseUp - -#### Video Backgrounds - -**Best**: BgParallax, BgPan, BgFade -**Alternative**: BgZoom, BgRotate - -#### Image Galleries - -**Best**: BgZoom, BgCloseUp, BgPullBack -**Alternative**: BgFake3D, BgSkew - -#### Creative Showcases - -**Best**: BgFake3D, BgSkew, BgRotate -**Alternative**: BgZoom, BgPullBack - -#### Landing Pages - -**Best**: ImageParallax, BgParallax, BgCloseUp -**Alternative**: BgFade, BgZoom - -### By Complexity - -#### Simple (Minimal configuration) - -- BgParallax, BgPan, BgFade, BgRotate, BgReveal - -#### Medium (Directional/Scale controls) - -- ImageParallax, BgCloseUp, BgPullBack, BgFadeBack, BgSkew - -#### Complex (Multi-property/3D effects) - -- BgZoom, BgFake3D - -### By Performance - -#### GPU Optimized (60fps) - -- BgParallax, BgRotate, BgFade, BgReveal - -#### Moderate Performance - -- ImageParallax, BgPan, BgCloseUp, BgPullBack, BgFadeBack - -#### Resource Intensive - -- BgZoom, BgFake3D, BgSkew - -## Target Element Structure - -Background scroll animations target specific element parts using `data-motion-part` attributes: - -### Required HTML Structure - -```html - -
- -
- -
- - -
-
- - -
-

Hero Title

-

Hero description

-
-
-``` - -### Motion Part Types - -- **`BG_LAYER`** - Background container layer (for clipping the media and overlays) -- **`BG_MEDIA`** - Main background media container (for media resizing) -- **`BG_IMG`** - Background image element (for direct image styling) - -## Common Configuration Patterns - -### Basic Background Animation - -```typescript -import { getWebAnimation } from '@wix/motion'; - -const bgAnimation = getWebAnimation( - '#hero-background', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.3, // 30% of scroll speed - }, - }, - { - trigger: 'view-progress', - element: docuement.querySelector('#hero-section'), - }, -); -``` - -### Speed Control - -```typescript -// Slow, subtle parallax -const subtleConfig = { - type: 'BgParallax', - speed: 0.1, // Very slow movement -}; - -// Fast, dramatic parallax -const dramaticConfig = { - type: 'BgParallax', - speed: 0.8, // Fast movement -}; -``` - -### Directional Effects - -```typescript -// Horizontal panning -const panConfig = { - type: 'BgPan', - direction: 'left', // 'left' | 'right' - speed: 0.4, -}; - -// Zoom effects -const zoomConfig = { - type: 'BgZoom', - direction: 'in', // 'in' | 'out' - zoom: 40, -}; -``` - -### Multi-Layer Setup - -```typescript -// Layer 1: Far background (slowest) -getWebAnimation( - '.bg-layer-1', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.1, - }, - }, - triggerConfig, -); - -// Layer 2: Mid background -getWebAnimation( - '.bg-layer-2', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.3, - }, - }, - triggerConfig, -); - -// Layer 3: Near background (fastest) -getWebAnimation( - '.bg-layer-3', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.6, - }, - }, - triggerConfig, -); -``` - -## Advanced Patterns - -### Hero Section with Multiple Effects - -```typescript -// Orchestrated hero background -function setupHeroBackground() { - const heroSection = document.querySelector('#hero'); - - // Background parallax - getWebAnimation( - heroSection, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.3, - }, - }, - { - trigger: 'view-progress', - element: heroSection, - }, - ); - - // Overlay fade - getWebAnimation( - heroSection, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgFade', - range: 'out', - }, - }, - { - trigger: 'view-progress', - element: heroSection, - }, - ); - - // Zoom effect - getWebAnimation( - heroSection, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgZoom', - direction: 'in', - zoom: 20, - }, - }, - { - trigger: 'view-progress', - element: heroSection, - }, - ); -} -``` - -### Responsive Background Behavior - -```typescript -// Adjust effects based on screen size -const isMobile = window.innerWidth < 768; - -const bgConfig = { - type: 'ScrubAnimationOptions', - namedEffect: isMobile - ? { type: 'BgFade', range: 'in' } // Simple fade on mobile - : { type: 'BgFake3D', stretch: 1.3 }, // Complex 3D on desktop -}; -``` - -### Performance-Aware Background Animation - -```typescript -// Check device capabilities -const isLowEnd = navigator.hardwareConcurrency < 4; -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -let backgroundConfig; - -if (prefersReducedMotion) { - // No animation - static background - backgroundConfig = { type: 'BgReveal' }; -} else if (isLowEnd) { - // Simple animation for low-end devices - backgroundConfig = { type: 'BgFade', range: 'in' }; -} else { - // Full animation for capable devices - backgroundConfig = { - type: 'BgParallax', - speed: 0.3, - }; -} -``` - -### Video Background Integration - -```typescript -// Video background with subtle parallax -function setupVideoBackground() { - const videoContainer = document.querySelector('.video-bg-container'); - const section = document.querySelector('.video-section'); - - // Very subtle parallax for video - getWebAnimation( - videoContainer, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.15, // Gentle movement for video - }, - }, - { - trigger: 'view-progress', - element: section, - }, - ); - - // Optional fade overlay - getWebAnimation( - videoContainer, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgFade', - range: 'out', - }, - }, - { - trigger: 'view-progress', - element: section, - }, - ); -} -``` - -## CSS Requirements - -### Basic Structure Styles - -```css -.hero-section { - position: relative; - height: 100vh; -} - -[data-motion-part='BG_LAYER'] { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 1; - overflow: clip; -} - -[data-motion-part='BG_MEDIA'] { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -[data-motion-part='BG_IMG'] { - width: 100%; - height: 100%; - object-fit: cover; -} - -.hero-content { - position: relative; - z-index: 2; - /* Content styles */ -} -``` - -## Framework Integration - -### React Component - -```typescript -import React, { useEffect, useRef } from 'react'; -import { getWebAnimation } from '@wix/motion'; - -interface BackgroundScrollProps { - children: React.ReactNode; - animationType: string; - animationConfig?: any; - className?: string; -} - -function BackgroundScroll({ - children, - animationType, - animationConfig = {}, - className -}: BackgroundScrollProps) { - const sectionRef = useRef(null); - const sceneRef = useRef(null); - - useEffect(() => { - if (!sectionRef.current) return; - - sceneRef.current = getWebAnimation(sectionRef.current, { - type: 'ScrubAnimationOptions', - namedEffect: { - type: animationType, - ...animationConfig - } - }, { - trigger: 'view-progress', - element: sectionRef.current - }); - - return () => { - if (sceneRef.current?.destroy) { - sceneRef.current.destroy(); - } - }; - }, [animationType, JSON.stringify(animationConfig)]); - - return ( -
-
-
- {children} -
-
-
- ); -} - -// Usage -function Hero() { - return ( - - Hero background - - ); -} -``` - -## Performance Monitoring - -### Background Animation Performance - -```typescript -// Monitor background scroll performance -class BackgroundPerformanceMonitor { - constructor() { - this.frameCount = 0; - this.lastTime = performance.now(); - this.monitor(); - } - - monitor() { - this.frameCount++; - const currentTime = performance.now(); - - if (currentTime - this.lastTime >= 1000) { - const fps = this.frameCount; - - if (fps < 30) { - console.warn('Background animation performance degraded'); - this.optimizeAnimations(); - } - - this.frameCount = 0; - this.lastTime = currentTime; - } - - requestAnimationFrame(() => this.monitor()); - } - - optimizeAnimations() { - // Could switch to simpler animations or disable some effects - document.querySelectorAll('[data-bg-animation]').forEach((el) => { - // Simplify or disable complex animations - }); - } -} - -// Initialize performance monitoring -const performanceMonitor = new BackgroundPerformanceMonitor(); -``` - ---- - -**[Back to All Presets](../) | [Browse Other Categories](../../categories/)** diff --git a/packages/motion-presets/docs/presets/background-scroll/bg-parallax.md b/packages/motion-presets/docs/presets/background-scroll/bg-parallax.md deleted file mode 100644 index a56a11a7..00000000 --- a/packages/motion-presets/docs/presets/background-scroll/bg-parallax.md +++ /dev/null @@ -1,438 +0,0 @@ -# BgParallax - -Classic background parallax scrolling effect specifically designed for background media elements. Creates smooth depth and immersion by moving background layers at different speeds relative to scroll position. - -## Overview - -**Category**: Background Scroll -**Complexity**: Simple -**Performance**: GPU Optimized -**Mobile Friendly**: Yes (with speed adjustment) - -### Best Use Cases - -- Hero section backgrounds with layered depth -- Large image backgrounds in landing pages -- Video backgrounds with subtle movement -- Multi-section background continuity -- Immersive storytelling backgrounds - -### Target Elements - -- Background media containers with `data-motion-part="BG_MEDIA"` -- Large background images and videos -- Hero section background layers -- Full-width background compositions - -## Configuration - -### TypeScript Interface - -```typescript -export type BgParallax = BaseDataItemLike<'BgParallax'> & { - speed?: number; -}; -``` - -### Parameters - -| Parameter | Type | Default | Description | Examples | -| --------- | -------- | ------- | -------------------------------------------- | -------------------------- | -| `speed` | `number` | `0.5` | Background movement speed relative to scroll | `0.1`, `0.3`, `0.5`, `0.8` | - -### Speed Guidelines - -- **`0.1 - 0.2`** - Very subtle parallax, professional interfaces -- **`0.3 - 0.4`** - Noticeable but gentle parallax effect -- **`0.5 - 0.6`** - Standard parallax, balanced movement -- **`0.7 - 0.8`** - Strong parallax, dramatic effect - -### Automatic Measurements - -BgParallax automatically measures component dimensions for accurate scroll calculations and responsive behavior. - -## Usage Examples - -### Basic Usage - -```typescript -import { getWebAnimation } from '@wix/motion'; - -const element = document.querySelector('#hero-section'); - -const animation = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.3, // Background moves at 30% of scroll speed - }, - }, - { - trigger: 'view-progress', - element, - }, -); -``` - -### Speed Variations - -```typescript -// Subtle parallax for professional sites -const subtleAnimation = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.2, - }, - }, - { - trigger: 'view-progress', - element: document.body, - }, -); - -// Dramatic parallax for creative showcases -const dramaticAnimation = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.7, - }, - }, - { - trigger: 'view-progress', - element, - }, -); - -// Very slow, cinematic parallax -const cinematicAnimation = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.1, - }, - }, - { - trigger: 'view-progress', - element, - }, -); -``` - -### Section-Scoped Parallax - -```typescript -// Parallax relative to specific section -const sectionParallax = getScrubScene( - '#section-background', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.4, - }, - }, - { - trigger: 'view-progress', - element: document.querySelector('#hero-section'), // Relative to section - }, -); -``` - -## Required HTML Structure - -### Basic Setup - -```html -
- -
- -
- - Hero background -
-
- - -
-

Hero Title

-

Hero description

-
-
-``` - -### Video Background Setup - -```html -
-
-
- -
-
- -
-

Video Hero

-
-
-``` - -## Common Patterns - -### Multi-Layer Parallax Background - -```typescript -// Create depth with multiple background layers -const backgroundLayers = [ - { - selector: '#bg-layer-1', - speed: 0.1, // Furthest layer - slowest - zIndex: 1, - }, - { - selector: '#bg-layer-2', - speed: 0.3, // Middle layer - zIndex: 2, - }, - { - selector: '#bg-layer-3', - speed: 0.5, // Nearest layer - fastest - zIndex: 3, - }, -]; - -backgroundLayers.forEach((layer) => { - const element = document.querySelector(layer.selector); - if (element) { - // Set z-index for layering - element.style.zIndex = layer.zIndex; - - getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: layer.speed, - }, - }, - { - trigger: 'view-progress', - element, - }, - ); - } -}); -``` - -### Full-Page Background Parallax - -```typescript -// Continuous parallax across entire page -function setupFullPageParallax() { - const sections = document.querySelectorAll('.section'); - - sections.forEach((section, index) => { - const speed = 0.2 + index * 0.1; // Vary speed: 0.2, 0.3, 0.4... - - animation = getWebAnimation( - section, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: speed, - }, - }, - { - trigger: 'view-progress', - element: section, - }, - ); - }); -} -``` - -## CSS Requirements - -### Essential Styles - -```css -.bg-parallax-section { - position: relative; - overflow: hidden; - min-height: 100vh; -} - -[data-motion-part='BG_LAYER'] { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 1; -} - -[data-motion-part='BG_MEDIA'] { - position: absolute; - top: -10%; /* Extra space for upward movement */ - left: 0; - width: 100%; - height: 120%; /* Extra height for parallax range */ - will-change: transform; - backface-visibility: hidden; -} - -[data-motion-part='BG_IMG'] { - width: 100%; - height: 100%; - object-fit: cover; - object-position: center; -} - -.hero-content { - position: relative; - z-index: 2; - /* Content styling */ -} -``` - -### Mobile Optimization - -```typescript -const isMobile = window.innerWidth < 768; -const isLowEnd = navigator.hardwareConcurrency < 4; - -const parallaxConfig = { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: isMobile || isLowEnd ? 0.1 : 0.3, // Gentler on mobile/low-end - }, -}; -``` - -### Battery-Conscious Implementation - -```typescript -// Pause parallax when page is hidden -document.addEventListener('visibilitychange', () => { - const parallaxElements = document.querySelectorAll('[data-bg-parallax]'); - - if (document.hidden) { - // Pause all parallax animations - parallaxElements.forEach((el) => { - if (el.parallaxScene) el.parallaxScene.destroy(); - }); - } else { - // Resume parallax animations - parallaxElements.forEach((el) => { - setupBgParallax(el); - }); - } -}); -``` - -## Accessibility - -### Reduced Motion Support - -```typescript -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -if (prefersReducedMotion) { - // Disable parallax - use static background - console.log('BgParallax disabled for reduced motion preference'); -} else { - // Enable parallax effects - setupBgParallax(); -} -``` - -### Performance-Based Fallbacks - -```typescript -// Automatic fallback for poor performance -let frameCount = 0; -let lastTime = performance.now(); - -function monitorParallaxPerformance() { - frameCount++; - const currentTime = performance.now(); - - if (currentTime - lastTime >= 1000) { - const fps = frameCount; - - if (fps < 24) { - // Disable parallax on poor performance - console.warn('Disabling BgParallax due to poor performance'); - document.querySelectorAll('[data-bg-parallax]').forEach((el) => { - if (el.parallaxScene) el.parallaxScene.destroy(); - }); - } - - frameCount = 0; - lastTime = currentTime; - } -} -``` - -## Related Animations - -### Same Category - -- **[ImageParallax](image-parallax.md)** - Enhanced parallax with additional options -- **[BgPan](bg-pan.md)** - Horizontal background movement -- **[BgFade](bg-fade.md)** - Background fade effects - -### Other Categories - -- **[ParallaxScroll](../scroll/parallax-scroll.md)** - General element parallax -- **[MoveScroll](../scroll/move-scroll.md)** - Directional scroll movement - -### Complementary Effects - -- **Before**: Page load animations, hero entrances -- **After**: Content reveal animations, interaction states -- **Alongside**: Background fade overlays, zoom effects - -## Troubleshooting - -### Common Issues - -- **Background jumping**: Ensure extra height (120%) on BG_MEDIA element -- **Performance issues**: Reduce speed or limit number of parallax backgrounds -- **Mobile scrolling problems**: Use gentler speeds on touch devices - -### Debug Tips - -- Use browser timeline tools to monitor scroll performance -- Check for layout thrashing in Chrome DevTools "Rendering" tab -- Verify proper HTML structure with required data attributes - ---- - -## Interactive Example - -โ–ถ๏ธ **[Try it in Storybook](../../playground/)** - Experiment with BgParallax speeds and setups - ---- - -**[Back to Background Scroll Animations](../) | [Back to All Presets](../../)** diff --git a/packages/motion-presets/docs/presets/entrance/README.md b/packages/motion-presets/docs/presets/entrance/README.md deleted file mode 100644 index 6a39957e..00000000 --- a/packages/motion-presets/docs/presets/entrance/README.md +++ /dev/null @@ -1,377 +0,0 @@ -# Entrance Animations - -Time-based animations designed to reveal elements with visual impact. These animations bring elements into view with style and are perfect for page loads, modal openings, and progressive disclosure patterns. - -## Complete Preset List (19 presets) - -### ๐ŸŒŸ Simple Fades - -| Animation | Complexity | Directions | Description | -| ------------------------ | ---------- | ---------- | ------------------------ | -| **[FadeIn](fade-in.md)** | Simple | - | Clean opacity transition | -| **[BlurIn](blur-in.md)** | Simple | - | Blur-to-focus transition | - -### ๐ŸŽฏ Directional Movement - -| Animation | Complexity | Directions | Description | -| ---------------------------- | ---------- | ---------- | -------------------------------- | -| **[SlideIn](slide-in.md)** | Medium | 4-way | Slide from edge with clip reveal | -| **[GlideIn](glide-in.md)** | Medium | 360ยฐ | Smooth directional movement | -| **[FloatIn](float-in.md)** | Simple | 4-way | Gentle floating movement | -| **[ExpandIn](expand-in.md)** | Medium | 360ยฐ | Directional scale growth | - -### ๐Ÿ”„ 3D Transforms - -| Animation | Complexity | Directions | Description | -| -------------------------- | ---------- | ---------- | ------------------------------ | -| **[ArcIn](arc-in.md)** | Complex | 4-way | Curved motion with 3D rotation | -| **[FlipIn](flip-in.md)** | Medium | 4-way | 3D flip rotation entrance | -| **[FoldIn](fold-in.md)** | Complex | 4-way | 3D fold with perspective | -| **[TurnIn](turn-in.md)** | Complex | 4-corner | Complex 3D corner rotation | -| **[CurveIn](curve-in.md)** | Complex | 4-way | Curved 3D perspective entrance | -| **[TiltIn](tilt-in.md)** | Complex | 2-way | 3D tilt with clip reveal | - -### โšก Dynamic & Bouncy - -| Animation | Complexity | Directions | Description | -| ---------------------------- | ---------- | -------------- | --------------------------------- | -| **[BounceIn](bounce-in.md)** | Medium | 5-way + center | Spring-based entrance with bounce | -| **[DropIn](drop-in.md)** | Simple | - | Scale-based drop with easing | -| **[ExpandIn](expand-in.md)** | Medium | 9-way | Scale from specific origin points | -| **[SpinIn](spin-in.md)** | Medium | 2-way | Rotation with scale entrance | - -### ๐ŸŽจ Shape & Clip - -| Animation | Complexity | Directions | Description | -| -------------------------------- | ---------- | ---------- | ---------------------------- | -| **[RevealIn](reveal-in.md)** | Medium | 4-way | Clean clip-path reveal | -| **[ShapeIn](shape-in.md)** | Medium | 5 shapes | Morphing shape reveals | -| **[ShuttersIn](shutters-in.md)** | Complex | 4-way | Multi-segment shutter effect | -| **[WinkIn](wink-in.md)** | Medium | 2-way | Accordion-style reveal | - -## Quick Reference - -### By Use Case - -#### Modal & Overlay Entrances - -**Best**: DropIn, ExpandIn (center), FadeIn -**Alternative**: BounceIn (center), ShapeIn (circle) - -#### Content Block Reveals - -**Best**: SlideIn, RevealIn, FadeIn -**Alternative**: GlideIn, BlurIn - -#### Hero & Featured Content - -**Best**: ArcIn, TurnIn -**Alternative**: CurveIn, FlipIn - -#### Button & Interactive Elements - -**Best**: BounceIn, DropIn, Pulse -**Alternative**: SpinIn, ExpandIn - -#### Navigation & Menu Items - -**Best**: SlideIn, GlideIn, FloatIn -**Alternative**: RevealIn, FadeIn - -### By Complexity - -#### Simple (Minimal configuration) - -- FadeIn, BlurIn, DropIn, FloatIn - -#### Medium (Directional controls) - -- SlideIn, GlideIn, BounceIn, FlipIn, ExpandIn, SpinIn - -#### Complex (Advanced 3D effects) - -- ArcIn, FoldIn, TurnIn, CurveIn, TiltIn - -### By Performance - -#### GPU Optimized (60fps) - -- FadeIn, DropIn, ExpandIn, SpinIn, FlipIn - -#### Moderate Performance - -- SlideIn, GlideIn, BounceIn, ArcIn, RevealIn - -#### Resource Intensive - -- TurnIn, FoldIn, ShuttersIn, TiltIn - -## Common Patterns - -### Sequential Reveals - -```typescript -// Stagger multiple elements -elements.forEach((el, index) => { - getWebAnimation(el, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'SlideIn', direction: 'bottom' }, - duration: 600, - delay: index * 150, // 150ms stagger - }).play(); -}); -``` - -### Hero Section Entrance - -```typescript -// Dramatic entrance for main content -const heroAnimation = getWebAnimation(heroElement, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'bottom', - }, - duration: 1200, - easing: 'quintOut', -}); -``` - -### Modal Appearance - -```typescript -// Smooth modal entrance -const modalAnimation = getWebAnimation(modal, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'DropIn', - }, - duration: 400, - easing: 'backOut', -}); -``` - -### Performance Monitoring - -```typescript -// Performance-aware ArcIn -function createPerformantArc(element, config) { - const isLowEnd = navigator.hardwareConcurrency < 4; - - if (isLowEnd) { - // Fallback to simpler animation - return getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: config.duration * 0.8, - }); - } - - return getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'ArcIn', ...config }, - duration: config.duration, - }); -} -``` - -### Mobile Optimization - -```typescript -const isMobile = window.innerWidth < 768; -const isReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -let arcConfig; -if (isReducedMotion) { - // No motion - instant reveal - element.style.opacity = '1'; - element.style.transform = 'none'; -} else if (isMobile) { - // Simplified arc for mobile - arcConfig = { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'bottom', - }, - duration: 700, // Faster completion - }; -} else { - // Full desktop experience - arcConfig = { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'bottom', - }, - duration: 1000, - }; -} -``` - -## Accessibility - -### Reduced Motion Support - -```typescript -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -if (prefersReducedMotion) { - // Replace with simple fade - getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 400, - }).play(); -} else { - // Full arc animation - getWebAnimation(element, arcConfig).play(); -} -``` - -## Framework Integration - -### React Component - -```typescript -import React, { useEffect, useRef, useState } from 'react'; -import { getWebAnimation } from '@wix/motion'; - -interface ArcInProps { - children: React.ReactNode; - direction?: 'top' | 'right' | 'bottom' | 'left'; - duration?: number; - delay?: number; - onComplete?: () => void; -} - -function ArcIn({ - children, - direction = 'bottom', - duration = 1000, - delay = 0, - onComplete -}: ArcInProps) { - const elementRef = useRef(null); - const [isAnimating, setIsAnimating] = useState(true); - - useEffect(() => { - if (!elementRef.current) return; - - const animation = getWebAnimation(elementRef.current, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction - }, - duration, - delay, - easing: 'quintOut' - }); - - animation.play().then(() => { - setIsAnimating(false); - onComplete?.(); - }); - - return () => animation.cancel(); - }, [direction, duration, delay, onComplete]); - - return ( -
- {children} -
- ); -} -``` - -### Vue Component with Intersection Observer - -```vue - - - - - -``` - ---- - -**[Back to All Presets](../) | [Browse Other Categories](../../categories/)** diff --git a/packages/motion-presets/docs/presets/entrance/arc-in.md b/packages/motion-presets/docs/presets/entrance/arc-in.md deleted file mode 100644 index a6b38401..00000000 --- a/packages/motion-presets/docs/presets/entrance/arc-in.md +++ /dev/null @@ -1,289 +0,0 @@ -# ArcIn - -Sophisticated 3D entrance animation featuring curved motion paths combined with perspective rotation. Creates dramatic, cinematic reveals perfect for hero content and featured elements. - -## Overview - -**Category**: Entrance -**Complexity**: Complex -**Performance**: Moderate (3D transforms) -**Mobile Friendly**: With optimization - -### Best Use Cases - -- Hero sections and featured content -- Premium product showcases -- Dramatic page introductions -- High-impact call-to-action elements -- Portfolio pieces and creative presentations - -### Target Elements - -- Large content blocks and hero sections -- Images and media with significant visual weight -- Primary navigation and key interface elements -- Cards and panels requiring dramatic entrance - -## Configuration - -### TypeScript Interface - -```typescript -export type ArcIn = BaseDataItemLike<'ArcIn'> & { - direction: EffectFourDirections; -}; -``` - -### Parameters - -| Parameter | Type | Default | Description | Examples | -| ----------- | -------- | ---------- | ----------------------------------------- | ---------------------------------------- | -| `direction` | `string` | `'bottom'` | Direction of arc motion and rotation axis | `'top'`, `'right'`, `'bottom'`, `'left'` | - -### Directional Support - -- **Four directions**: `top`, `right`, `bottom`, `left` -- **Arc behavior**: Each direction creates a unique curved entry path with corresponding rotation -- **3D rotation**: Direction determines the rotation axis and perspective angle - -### Motion Characteristics - -| Direction | Arc Path | Rotation Axis | Visual Effect | -| --------- | --------------------------- | ------------- | ---------------------- | -| `top` | Curves down from above | X-axis | Forward tilt revealing | -| `right` | Curves left from right side | Y-axis | Side rotation entrance | -| `bottom` | Curves up from below | X-axis | Upward tilt reveal | -| `left` | Curves right from left side | Y-axis | Side rotation entrance | - -## Usage Examples - -### Basic Usage - -```typescript -import { getWebAnimation } from '@wix/motion'; - -const animation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'bottom', - }, - duration: 1000, - easing: 'quintOut', -}); - -await animation.play(); -``` - -### Directional Examples - -```typescript -// Hero content from bottom with upward arc -const heroArc = getWebAnimation(heroSection, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'bottom', - }, - duration: 1400, - easing: 'quintOut', -}); - -// Side panel from left with rotation -const panelArc = getWebAnimation(sidePanel, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'left', - }, - duration: 900, - easing: 'cubicOut', -}); - -// Featured card from top with forward tilt -const cardArc = getWebAnimation(featuredCard, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'top', - }, - duration: 1000, - easing: 'backOut', -}); -``` - -### Advanced Timing - -```typescript -// Cinematic entrance with custom easing -const cinematicArc = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'bottom', - }, - duration: 1600, - delay: 300, - easing: 'cubic-bezier(0.16, 1, 0.3, 1)', // Custom dramatic curve -}); -``` - -## Common Patterns - -### Hero Section Sequence - -```typescript -// Orchestrated hero entrance -async function revealHero() { - // Background arc in first - const bgAnimation = getWebAnimation(heroBackground, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'bottom', - }, - duration: 1200, - }); - - // Title arcs in - const titleAnimation = getWebAnimation(heroTitle, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'bottom', - }, - duration: 1000, - delay: 200, - }); - - // Subtitle follows - const subtitleAnimation = getWebAnimation(heroSubtitle, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'bottom', - }, - duration: 800, - delay: 400, - }); - - await Promise.all([bgAnimation.play(), titleAnimation.play(), subtitleAnimation.play()]); -} -``` - -### Portfolio Grid Reveal - -```typescript -// Staggered portfolio items with alternating directions -const portfolioItems = document.querySelectorAll('.portfolio-item'); -portfolioItems.forEach((item, index) => { - const isEven = index % 2 === 0; - - getWebAnimation(item, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: isEven ? 'left' : 'right', - }, - duration: 1000, - delay: index * 200, - easing: 'quintOut', - }).play(); -}); -``` - -### Modal with Arc Entrance - -```typescript -// Premium modal with sophisticated entrance -function showPremiumModal() { - // Backdrop fades in - const backdropAnimation = getWebAnimation(backdrop, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 400, - }); - - // Modal content arcs in dramatically - const modalAnimation = getWebAnimation(modalContent, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'top', - }, - duration: 800, - delay: 200, - easing: 'backOut', - }); - - return Promise.all([backdropAnimation.play(), modalAnimation.play()]); -} -``` - -### Product Showcase - -```typescript -// Feature product with rotating arc entrance -const productShowcase = getWebAnimation(productContainer, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'right', - }, - duration: 1400, - easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)', // Elastic feel -}); -``` - -## Performance Tips - -- **Monitor 3D performance** - ArcIn uses transform3d which can be intensive -- **Limit concurrent arcs** - Multiple 3D animations can impact frame rate -- **Consider simplified versions** on lower-end devices - -### Vestibular Considerations - -ArcIn involves rotational motion which may trigger vestibular disorders. Always provide reduced motion alternatives. - -## Related Animations - -### Same Category - -- **[CurveIn](curve-in.md)** - Alternative 3D curved entrance -- **[TurnIn](turn-in.md)** - Complex corner-based 3D rotation -- **[FlipIn](flip-in.md)** - Simpler 3D flip without arc motion - -### Other Categories - -- **[ArcScroll](../scroll/arc-scroll.md)** - Scroll-driven arc effects -- **[TiltScroll](../scroll/tilt-scroll.md)** - Scroll-based 3D tilting - -### Complementary Effects - -- **Before**: Loading states, content preparation -- **After**: Hover animations, interaction states -- **Alongside**: Background parallax, subtle scale effects - -## Troubleshooting - -### Common Issues - -- **Choppy animation**: Check for other 3D transforms or reduce intensity -- **Element disappears**: Verify parent has perspective and overflow settings -- **Performance issues**: Use performance monitoring and device detection - -### Debug Tips - -- Use Chrome DevTools "Rendering" tab to monitor frame rate -- Check "Layout Shift" metrics for animation smoothness -- Test on actual mobile devices for performance validation - ---- - -## Interactive Example - -โ–ถ๏ธ **[Try it in Storybook](../../playground/)** - Experiment with ArcIn directions - ---- - -**[Back to Entrance Animations](../) | [Back to All Presets](../../)** diff --git a/packages/motion-presets/docs/presets/entrance/bounce-in.md b/packages/motion-presets/docs/presets/entrance/bounce-in.md deleted file mode 100644 index 59c0c585..00000000 --- a/packages/motion-presets/docs/presets/entrance/bounce-in.md +++ /dev/null @@ -1,264 +0,0 @@ -# BounceIn - -Spring-based entrance animation with elastic movement and bouncing effects. Creates engaging, playful reveals perfect for call-to-action elements and interactive content. - -## Overview - -**Category**: Entrance -**Complexity**: Medium -**Performance**: GPU Optimized -**Mobile Friendly**: Yes - -### Best Use Cases - -- Call-to-action buttons and interactive elements -- Notification badges and alerts -- Playful UI elements in games or creative interfaces -- Success states and achievement celebrations - -### Target Elements - -- Buttons, badges, and small interactive elements -- Icons and symbolic content -- Cards and panels requiring attention -- Modal dialogs and popups - -## Configuration - -### TypeScript Interface - -```typescript -export type BounceIn = BaseDataItemLike<'BounceIn'> & { - direction: EffectFourDirections | 'center'; - distanceFactor?: number; -}; -``` - -### Parameters - -| Parameter | Type | Default | Description | Examples | -| ---------------- | -------- | ---------- | ------------------------------------ | ---------------------------------------------------- | -| `direction` | `string` | `'bottom'` | Origin direction for bounce movement | `'top'`, `'right'`, `'bottom'`, `'left'`, `'center'` | -| `distanceFactor` | `number` | `1` | Movement distance multiplier | `0.5`, `1`, `2` | - -### Directional Support - -- **Four directions**: `top`, `right`, `bottom`, `left` - Element bounces in from specified edge -- **Center**: `center` - Element scales and bounces from center point -- **Movement**: Direction determines both initial position and bounce trajectory - -## Usage Examples - -### Basic Usage - -```typescript -import { getWebAnimation } from '@wix/motion'; - -const animation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'BounceIn', - direction: 'bottom', - }, - duration: 800, - easing: 'easeOut', -}); - -await animation.play(); -``` - -### Variations - -```typescript -// Subtle bounce for professional interfaces -const subtleBounce = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'BounceIn', - direction: 'bottom', - distanceFactor: 0.5, - }, - duration: 600, -}); - -// Dramatic bounce for playful elements -const dramaticBounce = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'BounceIn', - direction: 'center', - duration: 1000, -}); -``` - -### Custom Distance Control - -```typescript -// Reduced movement distance -const shortBounce = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'BounceIn', - direction: 'left', - distanceFactor: 0.5, // Half normal distance - }, - duration: 700, -}); - -// Extended movement distance -const longBounce = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'BounceIn', - direction: 'top', - distanceFactor: 1.5, // 50% more distance - }, - duration: 900, -}); -``` - -### Directional Examples - -```typescript -// Button appearing from right -const buttonBounce = getWebAnimation(button, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'BounceIn', - direction: 'right', - }, - duration: 600, -}); - -// Modal bouncing from center -const modalBounce = getWebAnimation(modal, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'BounceIn', - direction: 'center', - }, - duration: 500, -}); -``` - -## Common Patterns - -### Success State Animation - -```typescript -// Bounce in success message -function showSuccessMessage(element) { - return getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'BounceIn', - direction: 'center', - }, - duration: 800, - easing: 'backOut', - }); -} -``` - -### Notification Badge - -```typescript -// Attention-grabbing badge entrance -const badgeBounce = getWebAnimation(badge, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'BounceIn', - direction: 'top', - }, - duration: 600, - easing: 'elasticOut', -}); -``` - -### Sequential Button Reveals - -```typescript -// Bounce in action buttons one by one -const actionButtons = document.querySelectorAll('.action-btn'); -actionButtons.forEach((btn, index) => { - getWebAnimation(btn, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'BounceIn', - direction: 'bottom', - }, - duration: 600, - delay: index * 150, // 150ms stagger - easing: 'backOut', - }).play(); -}); -``` - -### Modal Dialog with Backdrop - -```typescript -// Backdrop fade + modal bounce -async function showModal() { - // Fade in backdrop first - const backdropAnimation = getWebAnimation(backdrop, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 300, - }); - - // Bounce in modal content - const modalAnimation = getWebAnimation(modalContent, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'BounceIn', - direction: 'center', - }, - duration: 500, - delay: 100, - }); - - await Promise.all([backdropAnimation.play(), modalAnimation.play()]); -} -``` - -## Related Animations - -### Same Category - -- **[DropIn](drop-in.md)** - Simpler scale-based alternative -- **[ExpandIn](expand-in.md)** - Scale-focused without bounce physics - -### Other Categories - -- **[Bounce](../ongoing/bounce.md)** - Ongoing bouncing animation -- **[Pulse](../ongoing/pulse.md)** - Gentler ongoing attention effect - -### Complementary Effects - -- **Before**: Loading states, preparation animations -- **After**: Hover effects, interaction feedback -- **Alongside**: Background color changes, shadow effects - -## Troubleshooting - -### Common Issues - -- **Bounce feels too aggressive**: Reduce distanceFactor -- **Animation cuts off**: Ensure parent container has sufficient space -- **Timing feels wrong**: Adjust duration based on element size and context - -### Debug Tips - -- Use browser timeline tools to analyze bounce curve -- Test with different distanceFactor values to find the right feel -- Verify initial element positioning for directional bounces - ---- - -## Interactive Example - -โ–ถ๏ธ **[Try it in Storybook](../../playground/)** - Experiment with BounceIn directions - ---- - -**[Back to Entrance Animations](../) | [Back to All Presets](../../)** diff --git a/packages/motion-presets/docs/presets/entrance/fade-in.md b/packages/motion-presets/docs/presets/entrance/fade-in.md deleted file mode 100644 index 3dc36a80..00000000 --- a/packages/motion-presets/docs/presets/entrance/fade-in.md +++ /dev/null @@ -1,213 +0,0 @@ -# FadeIn - -Simple opacity transition that brings elements into view with a clean, professional appearance. - -## Overview - -**Category**: Entrance -**Complexity**: Simple -**Performance**: GPU Optimized -**Mobile Friendly**: Yes - -### Best Use Cases - -- Subtle content reveals for professional interfaces -- Modal and overlay appearances -- Loading state transitions -- Progressive disclosure in forms and wizards - -### Target Elements - -- Any DOM element requiring gentle introduction -- Content blocks, cards, and panels -- Text elements and typography -- Images and media that should appear smoothly - -## Configuration - -### TypeScript Interface - -```typescript -export type FadeIn = BaseDataItemLike<'FadeIn'>; -``` - -### Parameters - -FadeIn has no configurable parameters - it provides a pure opacity transition from 0 to 1. - -| Parameter | Type | Default | Description | Examples | -| --------- | ---- | ------- | -------------------------------- | -------- | -| _None_ | - | - | FadeIn uses no custom parameters | - | - -## Usage Examples - -### Basic Usage - -```typescript -import { getWebAnimation } from '@wix/motion'; - -const animation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'FadeIn', - }, - duration: 600, - easing: 'easeOut', -}); - -await animation.play(); -``` - -### Advanced Configuration - -```typescript -// Slower, more dramatic fade -const dramaticFade = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 1200, - delay: 300, - easing: 'quintOut', -}); - -// Quick, snappy fade -const quickFade = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 300, - easing: 'cubicOut', -}); -``` - -### CSS Mode - -```typescript -import { getCSSAnimation } from '@wix/motion'; - -const cssRules = getCSSAnimation('elementId', { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 500, -}); - -// Generates optimized CSS keyframes for better performance -``` - -## Common Patterns - -### Sequential Content Reveal - -```typescript -// Reveal content blocks one after another -const contentBlocks = document.querySelectorAll('.content-block'); -contentBlocks.forEach((block, index) => { - getWebAnimation(block, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 500, - delay: index * 200, // 200ms stagger - }).play(); -}); -``` - -### Modal Backdrop - -```typescript -// Fade in modal backdrop -const backdropFade = getWebAnimation(backdrop, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 300, - easing: 'easeOut', -}); - -// Fade in modal content with slight delay -const modalFade = getWebAnimation(modalContent, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 400, - delay: 150, - easing: 'easeOut', -}); -``` - -### Loading State Transition - -```typescript -// Hide loading, show content -async function showContent() { - // Fade out loading spinner - await getWebAnimation(loadingSpinner, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, // Will be reversed - duration: 200, - fill: 'backwards', - }).reverse(); - - // Fade in actual content - await getWebAnimation(content, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 400, - easing: 'easeOut', - }).play(); -} -``` - -### Image Gallery Reveal - -```typescript -// Reveal images after they load -images.forEach((img, index) => { - img.addEventListener('load', () => { - getWebAnimation(img, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 600, - delay: index * 100, - }).play(); - }); -}); -``` - -## Related Animations - -### Same Category - -- **[BlurIn](blur-in.md)** - Adds blur-to-focus effect to the fade -- **[DropIn](drop-in.md)** - Combines fade with subtle scale effect - -### Other Categories - -- **[FadeScroll](../scroll/fade-scroll.md)** - Scroll-driven fade effects -- **[Pulse](../ongoing/pulse.md)** - Ongoing fade-based attention effect - -### Complementary Effects - -- **Before**: Loading spinners, skeleton screens -- **After**: Content interactions, hover states -- **Alongside**: Background color transitions, shadow animations - -## Troubleshooting - -### Common Issues - -- **Element not visible**: Ensure initial opacity is set to 0 in CSS -- **Animation not smooth**: Check for conflicting CSS transitions -- **Flash of content**: Use `visibility: hidden` initially if needed - -### Debug Tips - -- Use browser dev tools to inspect animation timeline -- Verify element has proper positioning context -- Check for CSS transform conflicts - ---- - -## Interactive Example - -โ–ถ๏ธ **[Try it in Storybook](../../playground/)** - Experiment with FadeIn timing and easing - ---- - -**[Back to Entrance Animations](../) | [Back to All Presets](../../)** diff --git a/packages/motion-presets/docs/presets/mouse/README.md b/packages/motion-presets/docs/presets/mouse/README.md deleted file mode 100644 index 54881432..00000000 --- a/packages/motion-presets/docs/presets/mouse/README.md +++ /dev/null @@ -1,448 +0,0 @@ -# Mouse Animations - -Interactive pointer-driven effects that respond to mouse movement in real-time. Perfect for creating engaging user interactions, 3D effects, and cursor-following elements. - -## Complete Preset List (12 presets) - -### ๐ŸŽฏ Position Tracking - -| Animation | Complexity | Axis Control | Description | -| ---------------------------------- | ---------- | ------------ | ------------------------------- | -| **[TrackMouse](track-mouse.md)** | Simple | โœ“ | Element follows cursor movement | -| **[AiryMouse](airy-mouse.md)** | Medium | โœ“ | Lightweight floating movement | -| **[BounceMouse](bounce-mouse.md)** | Simple | โœ“ | Elastic cursor following | - -### ๐Ÿ”„ 3D Transformations - -| Animation | Complexity | Axis Control | Description | -| ------------------------------------- | ---------- | ------------ | --------------------------------- | -| **[Tilt3DMouse](tilt-3d-mouse.md)** | Medium | - | 3D tilt based on pointer position | -| **[Track3DMouse](track-3d-mouse.md)** | Medium | โœ“ | 3D tracking with perspective | -| **[SwivelMouse](swivel-mouse.md)** | Complex | - | Pivot-point 3D rotation | - -### ๐Ÿ“ Scale & Deformation - -| Animation | Complexity | Axis Control | Description | -| -------------------------------- | ---------- | ------------ | ---------------------------- | -| **[ScaleMouse](scale-mouse.md)** | Medium | โœ“ | Dynamic scaling on hover | -| **[BlobMouse](blob-mouse.md)** | Medium | - | Organic blob-like scaling | -| **[SkewMouse](skew-mouse.md)** | Medium | โœ“ | Skew transformation tracking | - -### โœจ Visual Effects - -| Animation | Complexity | Description | -| ------------------------------ | ---------- | --------------------------- | -| **[BlurMouse](blur-mouse.md)** | Complex | Motion blur with 3D effects | -| **[SpinMouse](spin-mouse.md)** | Simple | Rotation based on movement | - -### ๐ŸŽจ Custom Behaviors - -| Animation | Complexity | Description | -| ---------------------------------- | ---------- | -------------------------- | -| **[CustomMouse](custom-mouse.md)** | Variable | Programmable mouse effects | - -## Quick Reference - -### By Use Case - -#### Card & Panel Interactions - -**Best**: Tilt3DMouse, ScaleMouse, Track3DMouse -**Alternative**: AiryMouse, BlobMouse - -#### Cursor Followers - -**Best**: TrackMouse, AiryMouse, BounceMouse -**Alternative**: SpinMouse, CustomMouse - -#### 3D Showcases - -**Best**: Track3DMouse, SwivelMouse, Tilt3DMouse -**Alternative**: BlurMouse, ScaleMouse - -#### Creative Interfaces - -**Best**: BlobMouse, SkewMouse, BlurMouse -**Alternative**: CustomMouse, SpinMouse - -#### Button & Interactive Elements - -**Best**: ScaleMouse, Tilt3DMouse, AiryMouse -**Alternative**: BounceMouse, BlobMouse - -### By Complexity - -#### Simple (Minimal configuration) - -- TrackMouse, BounceMouse, SpinMouse - -#### Medium (Directional/Axis controls) - -- Tilt3DMouse, Track3DMouse, ScaleMouse, BlobMouse, SkewMouse, AiryMouse - -#### Complex (Advanced 3D/Multiple properties) - -- SwivelMouse, BlurMouse, CustomMouse - -## Common Configuration Patterns - -### Basic Mouse Animation - -```typescript -import { getScrubScene } from '@wix/motion'; - -const scene = getScrubScene( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'TrackMouse', - distance: { value: 50, unit: 'px' }, - }, - transitionDuration: 300, - transitionEasing: 'easeOut', - }, - { - trigger: 'pointer-move', - element: containerElement, - }, -); -``` - -### Axis Constraints - -```typescript -// Horizontal-only cursor following -const horizontalTrack = { - type: 'TrackMouse', - distance: { value: 100, unit: 'px' }, - axis: 'horizontal', -}; - -// Vertical-only scaling -const verticalScale = { - type: 'ScaleMouse', - distance: { value: 150, unit: 'px' }, - axis: 'vertical', - scale: 1.2, -}; -``` - -### Container-Scoped Effects - -```typescript -// Mouse effect only within specific container -const scene = getScrubScene( - targetElement, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 20, - }, - }, - { - trigger: 'pointer-move', - element: containerElement, // Effect only when mouse is in container - }, -); -``` - -## Advanced Patterns - -### Interactive Card Grid - -```typescript -// Apply tilt effect to all cards -document.querySelectorAll('.interactive-card').forEach((card) => { - const scene = getScrubScene( - card, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 12, - perspective: 1000, - }, - transitionDuration: 200, - }, - { - trigger: 'pointer-move', - element: card, - }, - ); - - // Add glow effect on hover - card.addEventListener('pointerenter', () => { - card.style.boxShadow = '0 10px 30px rgb(0 0 0 / 0.1)'; - }); - - card.addEventListener('pointerleave', () => { - card.style.boxShadow = ''; - }); -}); -``` - -### Cursor Follower System - -```typescript -// Global cursor follower element -class CursorFollower { - constructor() { - this.element = this.createFollowerElement(); - this.setupTracking(); - } - - createFollowerElement() { - const follower = document.createElement('div'); - follower.className = 'cursor-follower'; - follower.style.cssText = ` - position: fixed; - width: 20px; - height: 20px; - background: rgb(255 255 255 / 0.5); - border-radius: 50%; - pointer-events: none; - z-index: 9999; - mix-blend-mode: difference; - `; - document.body.appendChild(follower); - return follower; - } - - setupTracking() { - this.scene = getScrubScene( - this.element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'TrackMouse', - distance: { value: 0, unit: 'px' }, // Perfect following - axis: 'both', - }, - transitionDuration: 100, - transitionEasing: 'easeOut', - }, - { - trigger: 'pointer-move', - }, - ); - } -} - -// Initialize cursor follower -const cursorFollower = new CursorFollower(); -``` - -### Layered Mouse Effects - -```typescript -// Multiple elements responding to same mouse movement -const mouseResponders = [ - { - element: '.bg-layer', - effect: { type: 'AiryMouse', distance: { value: 30, unit: 'px' } }, - }, - { - element: '.mid-layer', - effect: { type: 'TrackMouse', distance: { value: 50, unit: 'px' } }, - }, - { - element: '.fg-layer', - effect: { type: 'Tilt3DMouse', angle: 15 }, - }, -]; - -mouseResponders.forEach(({ element, effect }) => { - const el = document.querySelector(element); - if (el) { - const scene = getScrubScene( - el, - { - type: 'ScrubAnimationOptions', - namedEffect: effect, - }, - { - trigger: 'pointer-move', - element: document.querySelector('.mouse-container'), - }, - ); - } -}); -``` - -### Responsive Mouse Behavior - -```typescript -// Adjust mouse sensitivity based on device -const isTouchDevice = window.matchMedia('(hover: none)').matches; -const isLargeScreen = window.innerWidth > 1200; - -let mouseConfig; -if (isTouchDevice) { - // Disable mouse animations on touch devices - mouseConfig = null; -} else if (isLargeScreen) { - // Full mouse effects on large screens - mouseConfig = { - type: 'Tilt3DMouse', - angle: 20, - perspective: 800, - }; -} else { - // Simplified effects on smaller screens - mouseConfig = { - type: 'ScaleMouse', - scale: 1.05, - }; -} - -if (mouseConfig) { - setupMouseAnimation(element, mouseConfig); -} -``` - -## Framework Integration Patterns - -### React Hook for Mouse Effects - -```typescript -import { useEffect, useRef } from 'react'; -import { getWebAnimation } from '@wix/motion'; - -function useMouseEffect( - effectType: string, - options: any = {}, - containerRef?: React.RefObject -) { - const elementRef = useRef(null); - const sceneRef = useRef(null); - - useEffect(() => { - if (!elementRef.current) return; - - const container = containerRef?.current || elementRef.current; - - sceneRef.current = getScrubScene(elementRef.current, { - type: 'ScrubAnimationOptions', - namedEffect: { type: effectType, ...options }, - transitionDuration: options.transitionDuration || 200, - transitionEasing: options.transitionEasing || 'easeOut' - }, { - trigger: 'pointer-move', - element: container - }); - - return () => { - if (sceneRef.current) { - sceneRef.current.cancel(); - } - }; - }, [effectType, JSON.stringify(options), containerRef]); - - return elementRef; -} - -// Usage -function InteractiveCard({ children }) { - const cardRef = useMouseEffect('Tilt3DMouse', { - angle: 15, - perspective: 1000, - }); - - return ( -
- {children} -
- ); -} -``` - -### Vue Composition API - -```typescript -import { ref, onMounted, onUnmounted } from 'vue'; -import { getWebAnimation } from '@wix/motion'; - -export function useMouseEffect(effectType: string, options: any = {}) { - const elementRef = ref(); - let scene: any = null; - - const setupMouseEffect = () => { - if (!elementRef.value) return; - - scene = getScrubScene( - elementRef.value, - { - type: 'ScrubAnimationOptions', - namedEffect: { type: effectType, ...options }, - }, - { - trigger: 'pointer-move', - element: elementRef.value, - }, - ); - }; - - const destroyMouseEffect = () => { - if (scene) { - scene.destroy(); - scene = null; - } - }; - - onMounted(setupMouseEffect); - onUnmounted(destroyMouseEffect); - - return { elementRef }; -} -``` - -## Performance Considerations - -### Touch Device Optimization - -```typescript -// Disable mouse animations on touch-only devices -const supportsMouse = window.matchMedia('(hover: hover)').matches; - -if (supportsMouse) { - // Enable mouse animations - setupMouseAnimations(); -} else { - // Use alternative touch interactions - setupTouchInteractions(); -} -``` - -### Performance Monitoring - -```typescript -// Monitor mouse animation performance -let frameCount = 0; -let lastTime = performance.now(); - -function monitorMousePerformance() { - frameCount++; - const currentTime = performance.now(); - - if (currentTime - lastTime >= 1000) { - const fps = frameCount; - console.log(`Mouse animation FPS: ${fps}`); - - if (fps < 30) { - console.warn('Mouse animation performance degraded'); - // Could reduce complexity or disable animations - } - - frameCount = 0; - lastTime = currentTime; - } - - requestAnimationFrame(monitorMousePerformance); -} -``` - ---- - -**[Back to All Presets](../) | [Browse Other Categories](../../categories/)** diff --git a/packages/motion-presets/docs/presets/mouse/tilt-3d-mouse.md b/packages/motion-presets/docs/presets/mouse/tilt-3d-mouse.md deleted file mode 100644 index befc006c..00000000 --- a/packages/motion-presets/docs/presets/mouse/tilt-3d-mouse.md +++ /dev/null @@ -1,538 +0,0 @@ -# Tilt3DMouse - -3D tilt effect that follows pointer position to create immersive depth and perspective. Perfect for interactive cards, panels, and showcases requiring sophisticated hover effects. - -## Overview - -**Category**: Mouse -**Complexity**: Medium -**Performance**: GPU Optimized -**Mobile Friendly**: Disable on touch devices - -### Best Use Cases - -- Interactive cards and product showcases -- Portfolio pieces and gallery items -- Premium UI elements requiring depth -- 3D interface components -- Hero sections with interactive elements - -### Target Elements - -- Cards, panels, and content blocks -- Images and media elements -- Buttons and interactive components -- Product displays and showcases - -## Configuration - -### TypeScript Interface - -```typescript -export type Tilt3DMouse = BaseDataItemLike<'Tilt3DMouse'> & { - angle?: number; - perspective?: number; -}; -``` - -### Parameters - -| Parameter | Type | Default | Description | Examples | -| ------------- | -------- | ------- | --------------------------------- | --------------------- | -| `angle` | `number` | `15` | Maximum tilt angle in degrees | `5`, `15`, `25`, `45` | -| `perspective` | `number` | `800` | 3D perspective distance in pixels | `400`, `800`, `1200` | - -### Angle Guidelines - -- **`5-10ยฐ`** - Subtle depth for professional interfaces -- **`15-20ยฐ`** - Noticeable but natural tilt effect -- **`25-45ยฐ`** - Dramatic perspective for creative showcases - -### Perspective Guidelines - -- **`400-600px`** - Strong perspective, pronounced 3D effect -- **`800-1000px`** - Balanced perspective, natural depth -- **`1200px+`** - Subtle perspective, gentle 3D feel - -## Usage Examples - -### Basic Usage - -```typescript -import { getScrubScene } from '@wix/motion'; - -const scene = getScrubScene( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 15, - perspective: 800, - }, - transitionDuration: 200, - transitionEasing: 'easeOut', - }, - { - trigger: 'pointer-move', - element: element, - }, -); -``` - -### Angle Variations - -```typescript -// Subtle tilt for professional cards -const scene = getScrubScene( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 8, - perspective: 1000, - }, - transitionDuration: 300, - }, - { - trigger: 'pointer-move', - element: element, - }, -); - -// Dramatic tilt for creative showcases -const scene = getScrubScene( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 30, - perspective: 600, - }, - transitionDuration: 150, - }, - { - trigger: 'pointer-move', - element: element, - }, -); -``` - -### Perspective Control - -```typescript -// Strong perspective for cards -const scene = getScrubScene( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 20, - perspective: 500, // Closer perspective = stronger effect - }, - }, - triggerConfig, -); - -// Gentle perspective for large elements -const scene = getScrubScene( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 15, - perspective: 1200, // Distant perspective = gentler effect - }, - }, - triggerConfig, -); -``` - -### Power Level Examples - -```typescript -// Professional interface - minimal response -const professionalTilt = { - type: 'Tilt3DMouse', - angle: 10, - perspective: 1000, -}; - -// Creative interface - maximum response -const creativeTilt = { - type: 'Tilt3DMouse', - angle: 25, - perspective: 600, -}; -``` - -## Common Patterns - -### Interactive Card Grid - -```typescript -// Apply consistent tilt to all cards -document.querySelectorAll('.tilt-card').forEach((card) => { - const scene = getScrubScene( - card, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 12, - perspective: 1000, - }, - transitionDuration: 200, - transitionEasing: 'easeOut', - }, - { - trigger: 'pointer-move', - element: card, - }, - ); - - // Enhanced hover state - card.addEventListener('pointerenter', () => { - card.style.boxShadow = '0 20px 40px rgb(0 0 0 / 0.1)'; - card.style.transform = 'translateZ(10px)'; - }); - - card.addEventListener('pointerleave', () => { - card.style.boxShadow = ''; - card.style.transform = ''; - }); -}); -``` - -### Product Showcase - -```typescript -// Premium product display with dramatic tilt -function createProductShowcase(productElement) { - const scene = getScrubScene( - productElement, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 25, - perspective: 800, - }, - transitionDuration: 250, - transitionEasing: 'easeOut', - }, - { - trigger: 'pointer-move', - element: productElement.parentElement, - }, - ); - - return showcase; -} - -// Usage -const productCards = document.querySelectorAll('.product-card'); -productCards.forEach((card) => { - const productImage = card.querySelector('.product-image'); - if (productImage) { - createProductShowcase(productImage); - } -}); -``` - -### Portfolio Gallery - -```typescript -// Portfolio items with varied tilt intensities -const portfolioItems = document.querySelectorAll('.portfolio-item'); - -portfolioItems.forEach((item, index) => { - // Alternate between subtle and strong tilts - const isStrong = index % 2 === 0; - - getScrubScene( - item, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: isStrong ? 20 : 12, - perspective: isStrong ? 600 : 1000, - }, - transitionDuration: 200, - }, - { - trigger: 'pointer-move', - element: item, - }, - ); -}); -``` - -### Interactive Button - -```typescript -// CTA button with subtle tilt feedback -function createTiltButton(button) { - const scene = getScrubScene( - button, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 8, - perspective: 800, - }, - transitionDuration: 150, - transitionEasing: 'easeOut', - }, - { - trigger: 'pointer-move', - element: button, - }, - ); - - // Add scale effect - button.addEventListener('pointerenter', () => { - button.style.transform = 'scale(1.05)'; - }); - - button.addEventListener('pointerleave', () => { - button.style.transform = ''; - }); - - return scene; -} -``` - -### Modal with 3D Tilt - -```typescript -// Modal content with tilt interaction -function setupModalTilt(modal) { - const modalContent = modal.querySelector('.modal-content'); - - return getScrubScene( - modalContent, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 15, - perspective: 1000, - }, - transitionDuration: 300, - }, - { - trigger: 'pointer-move', - element: modal, // Track mouse over entire modal - }, - ); -} -``` - -## Framework Integration - -### React Component - -```typescript -import React, { useEffect, useRef } from 'react'; -import { getScrubScene } from '@wix/motion'; - -interface Tilt3DProps { - children: React.ReactNode; - angle?: number; - perspective?: number; - transitionDuration?: number; - className?: string; - disabled?: boolean; -} - -function Tilt3D({ - children, - angle = 15, - perspective = 800, - transitionDuration = 200, - className, - disabled = false -}: Tilt3DProps) { - const elementRef = useRef(null); - const animationRef = useRef(null); - - useEffect(() => { - if (!elementRef.current || disabled) return; - - // Check if device supports hover - const supportsHover = window.matchMedia('(hover: hover)').matches; - if (!supportsHover) return; - - animationRef.current = getScrubScene(elementRef.current, { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle, - perspective - }, - transitionDuration, - transitionEasing: 'easeOut' - }, { - trigger: 'pointer-move', - element: elementRef.current - }); - - return () => { - if (animationRef.current) { - animationRef.current.cancel(); - } - }; - }, [angle, perspective, transitionDuration, disabled]); - - return ( -
- {children} -
- ); -} - -// Usage -function ProductCard({ product }) { - return ( - - {product.name} -

{product.name}

-

{product.description}

-
- ); -} -``` - -### Vue Component - -```vue - - - -``` - -## Browser Support - -- **Web Animations API**: Full support in modern browsers -- **3D Transforms**: IE10+ with vendor prefixes -- **Perspective**: Baseline support across all modern browsers -- **Performance**: Hardware acceleration in Chrome 36+, Firefox 31+, Safari 9+ - -## Related Animations - -### Same Category - -- **[Track3DMouse](track-3d-mouse.md)** - 3D tracking with movement -- **[SwivelMouse](swivel-mouse.md)** - Pivot-based 3D rotation -- **[ScaleMouse](scale-mouse.md)** - Scaling without 3D effects - -### Other Categories - -- **[ArcIn](../entrance/arc-in.md)** - 3D entrance animation -- **[TiltScroll](../scroll/tilt-scroll.md)** - Scroll-driven 3D tilting - -### Complementary Effects - -- **Before**: Element entrance animations -- **After**: Click animations, state changes -- **Alongside**: Box shadow transitions, scale effects - ---- - -## Interactive Example - -โ–ถ๏ธ **[Try it in Storybook](../../playground/)** - Experiment with Tilt3DMouse angles - ---- - -**[Back to Mouse Animations](../) | [Back to All Presets](../../)** diff --git a/packages/motion-presets/docs/presets/ongoing/README.md b/packages/motion-presets/docs/presets/ongoing/README.md deleted file mode 100644 index 3848fe1d..00000000 --- a/packages/motion-presets/docs/presets/ongoing/README.md +++ /dev/null @@ -1,529 +0,0 @@ -# Ongoing Animations - -Time-based looping animations designed to create continuous movement and draw user attention. Perfect for call-to-action emphasis, loading states, and ambient motion. - -## Complete Preset List (15 presets) - -### ๐Ÿ’“ Rhythmic Scaling - -| Animation | Complexity | Description | -| ------------------------- | ---------- | ----------------------------- | -| **[Pulse](pulse.md)** | Simple | Smooth scale breathing effect | -| **[Breathe](breathe.md)** | Medium | Organic movement with scaling | - -### ๐Ÿƒ Movement & Position - -| Animation | Complexity | Directions | Description | -| ----------------------- | ---------- | ---------- | -------------------------- | -| **[Wiggle](wiggle.md)** | Medium | - | Random shake movement | -| **[Poke](poke.md)** | Medium | 4-way | Directional poking motion | -| **[Cross](cross.md)** | Complex | 8-way | Multi-directional crossing | - -### ๐Ÿ”„ Rotation & Spin - -| Animation | Complexity | Directions | Description | -| ------------------- | ---------- | ---------- | -------------------- | -| **[Spin](spin.md)** | Simple | 2-way | Continuous rotation | -| **[Flip](flip.md)** | Medium | 2-way | 3D flip rotation | -| **[Fold](fold.md)** | Complex | 4-way | 3D folding animation | - -### โšก Dynamic Effects - -| Animation | Complexity | Description | -| ----------------------- | ---------- | ------------------------ | -| **[Bounce](bounce.md)** | Medium | Vertical bouncing motion | -| **[Rubber](rubber.md)** | Medium | Elastic scaling effect | -| **[Jello](jello.md)** | Medium | Gelatinous wobble effect | -| **[Swing](swing.md)** | Complex | Pendulum swinging motion | - -### โœจ Visual Effects - -| Animation | Complexity | Description | -| --------------------- | ---------- | ----------------------- | -| **[Flash](flash.md)** | Simple | Opacity blinking effect | - -_Note: Experimental animations are currently disabled in production but available in development environments._ - -## Quick Reference - -### By Use Case - -#### Call-to-Action Elements - -**Best**: Pulse, Bounce, Wiggle -**Alternative**: Flash, Poke - -#### Loading & Processing States - -**Best**: Spin, Pulse, Flash -**Alternative**: Rubber, Breathe - -#### Ambient Interface Motion - -**Best**: Breathe, Pulse (soft), Swing (soft) -**Alternative**: Float, Cross - -#### Attention & Notifications - -**Best**: Wiggle, Flash, Bounce -**Alternative**: Poke, Pulse (hard) - -#### Creative & Playful Elements - -**Best**: Jello, Rubber, Bounce -**Alternative**: Flip, Fold, Cross - -### By Power Level Support - -#### Full Power Control - -- Pulse, Spin, Poke, Bounce, Rubber, Jello, Wiggle, Swing, Flip, Fold - -#### Fixed Configuration - -- Breathe, Flash, Cross - -### By Performance - -#### Lightweight (GPU Optimized) - -- Pulse, Spin, Flash, Bounce - -#### Moderate Performance - -- Wiggle, Poke, Rubber, Jello, Breathe - -#### Resource Intensive - -- Swing, Flip, Fold, Cross - -## Common Configuration Patterns - -### Basic Looping Animation - -```typescript -const animation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - }, - duration: 2000, - iterations: Infinity, // Loop forever - alternate: true, // Ping-pong effect -}); -``` - -### Controlled Loop Count - -```typescript -// Run animation 5 times then stop -const limitedAnimation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'Bounce' }, - duration: 1000, - iterations: 5, - alternate: true, -}); -``` - -### Attention Sequence - -```typescript -// Wiggle 3 times to get attention -function drawAttention(element) { - return getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Wiggle', - intensity: 0.8, - }, - duration: 500, - iterations: 3, - alternate: true, - }); -} -``` - -### Loading State Management - -```typescript -class LoadingSpinner { - constructor(element) { - this.element = element; - this.animation = null; - } - - start() { - this.animation = getWebAnimation(this.element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Spin', - direction: 'clockwise', - }, - duration: 1000, - iterations: Infinity, - }); - this.animation.play(); - } - - stop() { - if (this.animation) { - this.animation.cancel(); - this.animation = null; - } - } -} -``` - -## Framework Integration Patterns - -### React Hook for Ongoing Animations - -```typescript -import { useEffect, useRef } from 'react'; -import { getWebAnimation } from '@wix/motion'; - -function useOngoingAnimation( - animationType: string, - options: any = {}, - enabled: boolean = true -) { - const elementRef = useRef(null); - const animationRef = useRef(null); - - useEffect(() => { - if (!elementRef.current || !enabled) return; - - animationRef.current = getWebAnimation(elementRef.current, { - type: 'TimeAnimationOptions', - namedEffect: { type: animationType, ...options }, - duration: options.duration || 2000, - iterations: Infinity, - alternate: true - }); - - animationRef.current.play(); - - return () => { - if (animationRef.current) { - animationRef.current.cancel(); - } - }; - }, [animationType, enabled, JSON.stringify(options)]); - - return elementRef; -} - -// Usage -function PulsingButton({ children }) { - const buttonRef = useOngoingAnimation('Pulse', {}, true); - - return ( - - ); -} -``` - -### Vue Composition API - -```typescript -import { ref, onMounted, onUnmounted, watch } from 'vue'; -import { getWebAnimation } from '@wix/motion'; - -export function useOngoingAnimation( - animationType: string, - options: any = {}, - enabled: boolean = true, -) { - const elementRef = ref(); - let animation: any = null; - - const startAnimation = () => { - if (!elementRef.value || !enabled) return; - - animation = getWebAnimation(elementRef.value, { - type: 'TimeAnimationOptions', - namedEffect: { type: animationType, ...options }, - duration: options.duration || 2000, - iterations: Infinity, - alternate: true, - }); - - animation.play(); - }; - - const stopAnimation = () => { - if (animation) { - animation.cancel(); - animation = null; - } - }; - - watch( - () => enabled, - (newEnabled) => { - if (newEnabled) { - startAnimation(); - } else { - stopAnimation(); - } - }, - ); - - onMounted(startAnimation); - onUnmounted(stopAnimation); - - return { elementRef, startAnimation, stopAnimation }; -} -``` - -## Performance Considerations - -### Battery & Resource Management - -```typescript -// Pause animations when page is hidden -document.addEventListener('visibilitychange', () => { - const ongoingAnimations = document.querySelectorAll('[data-ongoing-animation]'); - - if (document.hidden) { - // Pause all ongoing animations - ongoingAnimations.forEach((el) => { - if (el.animation) el.animation.pause(); - }); - } else { - // Resume animations - ongoingAnimations.forEach((el) => { - if (el.animation) el.animation.play(); - }); - } -}); -``` - -### Intersection Observer Integration - -```typescript -// Only animate visible elements -const animationObserver = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - const animation = entry.target.ongoingAnimation; - - if (entry.isIntersecting) { - animation?.play(); - } else { - animation?.pause(); - } - }); -}); - -// Observe ongoing animation elements -document.querySelectorAll('[data-ongoing]').forEach((el) => { - animationObserver.observe(el); -}); -``` - -## Framework Integration - -### React Hook - -```typescript -import React, { useEffect, useRef } from 'react'; -import { getWebAnimation } from '@wix/motion'; - -interface PulseProps { - children: React.ReactNode; - intensity?: number; - duration?: number; - enabled?: boolean; - onHover?: 'pause' | 'stop' | 'continue'; -} - -function Pulse({ - children, - intensity = 1.0, - duration = 1500, - enabled = true, - onHover = 'continue' -}: PulseProps) { - const elementRef = useRef(null); - const animationRef = useRef(null); - - useEffect(() => { - if (!elementRef.current || !enabled) return; - - animationRef.current = getWebAnimation(elementRef.current, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - intensity - }, - duration, - iterations: Infinity, - alternate: true - }); - - animationRef.current.play(); - - return () => { - if (animationRef.current) { - animationRef.current.cancel(); - } - }; - }, [intensity, duration, enabled]); - - const handleMouseEnter = () => { - if (onHover === 'pause' && animationRef.current) { - animationRef.current.pause(); - } else if (onHover === 'stop' && animationRef.current) { - animationRef.current.cancel(); - } - }; - - const handleMouseLeave = () => { - if ((onHover === 'pause' || onHover === 'stop') && animationRef.current) { - animationRef.current.play(); - } - }; - - return ( -
- {children} -
- ); -} -``` - -### Vue Component - -```vue - - - -``` - -## Performance Tips - -- **Use CSS mode** for simple pulses that don't require control -- **Limit concurrent pulses** - too many can be distracting -- **Consider pausing** on page visibility change for battery saving -- **Use shorter durations** on mobile for better perceived performance - -### CSS Mode Alternative - -```typescript -import { getCSSAnimation } from '@wix/motion'; - -// For simple pulse without JavaScript control -const cssRules = getCSSAnimation('elementId', { - type: 'TimeAnimationOptions', - namedEffect: { type: 'Pulse' }, - duration: 2000, - iterations: Infinity, - alternate: true, -}); -``` - -## Accessibility - -### Reduced Motion Support - -```typescript -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -if (prefersReducedMotion) { - // No pulsing - maybe just a subtle opacity change - element.style.opacity = '0.9'; -} else { - // Full pulse animation - getWebAnimation(element, pulseConfig).play(); -} -``` - ---- - -**[Back to All Presets](../) | [Browse Other Categories](../../categories/)** diff --git a/packages/motion-presets/docs/presets/ongoing/pulse.md b/packages/motion-presets/docs/presets/ongoing/pulse.md deleted file mode 100644 index 67ada6c5..00000000 --- a/packages/motion-presets/docs/presets/ongoing/pulse.md +++ /dev/null @@ -1,339 +0,0 @@ -# Pulse - -Smooth scaling animation that creates a rhythmic breathing effect. Perfect for call-to-action buttons, notifications, and elements requiring gentle attention. - -## Overview - -**Category**: Ongoing -**Complexity**: Simple -**Performance**: GPU Optimized -**Mobile Friendly**: Yes - -### Best Use Cases - -- Call-to-action buttons and primary actions -- Notification badges and indicators -- Loading states and processing indicators -- Heartbeat effects and status indicators -- Gentle attention-seeking elements - -### Target Elements - -- Buttons, badges, and small interactive elements -- Icons and status indicators -- Small to medium-sized content blocks -- Elements requiring subtle emphasis - -## Configuration - -### TypeScript Interface - -```typescript -export type Pulse = BaseDataItemLike<'Pulse'> & { - intensity?: number; -}; -``` - -### Parameters - -| Parameter | Type | Default | Description | Examples | -| ---------------- | -------- | ------- | -------------------------------------------- | -------------------------- | -| `intensity` | `number` | `1.0` | Multiplier for scale amount | `0.5`, `1.0`, `1.5`, `2.0` | -| `iterationDelay` | `number` | `0` | Idle time (ms) appended after each iteration | `0`, `500`, `1000` | - -### Intensity Control - -The `intensity` parameter multiplies the base scale amount: - -- `intensity: 0.5` - Half the normal scale change -- `intensity: 1.0` - Standard scale change (default) -- `intensity: 1.5` - 50% more scale change -- `intensity: 2.0` - Double the scale change - -## Usage Examples - -### Basic Usage - -```typescript -import { getWebAnimation } from '@wix/motion'; - -const animation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - }, - duration: 1500, - iterations: Infinity, - alternate: true, -}); - -await animation.play(); -``` - -### Intensity Customization - -```typescript -// Fine-tuned pulse with custom intensity -const customPulse = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - intensity: 0.7, - }, - duration: 1800, - iterations: Infinity, - alternate: true, -}); - -// Exaggerated pulse for dramatic effect -const dramaticPulse = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - intensity: 2.5, - }, - duration: 1200, - iterations: Infinity, - alternate: true, -}); -``` - -### Duration Variations - -```typescript -// Fast heartbeat effect -const heartbeat = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - }, - duration: 800, // Fast pulse - iterations: Infinity, - alternate: true, -}); - -// Slow breathing effect -const breathe = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - }, - duration: 3000, // Slow, relaxed pulse - iterations: Infinity, - alternate: true, -}); -``` - -## Common Patterns - -### Call-to-Action Button - -```typescript -// CTA button that pulses to draw attention -function createPulsingCTA(button) { - const pulseAnimation = getWebAnimation(button, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - intensity: 1.2, - }, - duration: 1500, - iterations: Infinity, - alternate: true, - }); - - // Start pulsing - pulseAnimation.play(); - - // Stop pulsing on interaction - button.addEventListener('mouseenter', () => { - pulseAnimation.pause(); - }); - - button.addEventListener('mouseleave', () => { - pulseAnimation.play(); - }); - - return pulseAnimation; -} -``` - -### Notification Badge - -```typescript -// Badge that pulses when there are new notifications -class NotificationBadge { - constructor(element) { - this.element = element; - this.pulseAnimation = null; - this.count = 0; - } - - updateCount(newCount) { - this.count = newCount; - - if (newCount > 0) { - this.startPulsing(); - } else { - this.stopPulsing(); - } - } - - startPulsing() { - if (this.pulseAnimation) return; - - this.pulseAnimation = getWebAnimation(this.element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - intensity: 1.3, - }, - duration: 1000, - iterations: Infinity, - alternate: true, - }); - - this.pulseAnimation.play(); - } - - stopPulsing() { - if (this.pulseAnimation) { - this.pulseAnimation.cancel(); - this.pulseAnimation = null; - } - } -} -``` - -### Loading Indicator - -```typescript -// Pulse-based loading indicator -function createLoadingPulse(element) { - return getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - intensity: 1.5, - }, - duration: 1200, - iterations: Infinity, - alternate: true, - easing: 'easeInOut', - }); -} - -// Usage with loading states -async function performAsyncOperation() { - const loadingElement = document.querySelector('.loading-indicator'); - const pulseAnimation = createLoadingPulse(loadingElement); - - pulseAnimation.play(); - - try { - await someAsyncOperation(); - } finally { - pulseAnimation.cancel(); - } -} -``` - -### Status Indicator - -```typescript -// Server status indicator with different pulse patterns -function createStatusIndicator(element, status) { - const configs = { - online: { - namedEffect: { type: 'Pulse', intensity: 0.8 }, - duration: 2000, - }, - warning: { - namedEffect: { type: 'Pulse', intensity: 1.2 }, - duration: 1000, - }, - error: { - namedEffect: { type: 'Pulse', intensity: 1.5 }, - duration: 600, - }, - }; - - const config = configs[status] || configs.online; - - return getWebAnimation(element, { - type: 'TimeAnimationOptions', - ...config, - iterations: Infinity, - alternate: true, - }); -} -``` - -### Heartbeat Effect - -```typescript -// Medical or fitness app heartbeat visualization -function createHeartbeat(element, bpm = 60) { - const durationMs = (60 / bpm) * 1000; // Convert BPM to milliseconds - - return getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - intensity: 1.8, - }, - duration: durationMs / 2, // Each pulse is half the beat duration - iterations: Infinity, - alternate: true, - easing: 'easeInOut', - }); -} - -// Usage -const heartIcon = document.querySelector('.heart-icon'); -const heartbeat = createHeartbeat(heartIcon, 72); // 72 BPM -heartbeat.play(); -``` - -## Related Animations - -### Same Category - -- **[Breathe](breathe.md)** - Combines scaling with gentle movement -- **[Bounce](bounce.md)** - More dynamic vertical movement -- **[Rubber](rubber.md)** - Elastic scaling variation - -### Other Categories - -- **[DropIn](../entrance/drop-in.md)** - One-time scale entrance -- **[ScaleMouse](../mouse/scale-mouse.md)** - Mouse-driven scaling - -### Complementary Effects - -- **Before**: Element reveals, entrance animations -- **After**: Click animations, state changes -- **Alongside**: Color transitions, glow effects - -## Troubleshooting - -### Common Issues - -- **Pulse too aggressive**: Reduce intensity -- **Animation conflicts**: Check for competing CSS transitions -- **Performance problems**: Limit number of concurrent pulses - -### Debug Tips - -- Monitor frame rate with browser dev tools -- Use animation inspector to verify timing -- Test on actual devices for performance validation - ---- - -## Interactive Example - -โ–ถ๏ธ **[Try it in Storybook](../../playground/)** - Experiment with Pulse intensity and timing - ---- - -**[Back to Ongoing Animations](../) | [Back to All Presets](../../)** diff --git a/packages/motion-presets/docs/presets/scroll/README.md b/packages/motion-presets/docs/presets/scroll/README.md deleted file mode 100644 index cdac6461..00000000 --- a/packages/motion-presets/docs/presets/scroll/README.md +++ /dev/null @@ -1,508 +0,0 @@ -# Scroll Animations - -Scroll-driven effects that respond to viewport position for immersive storytelling and progressive disclosure. These animations create smooth, synchronized effects tied to the user's scroll progress. - -## Complete Preset List (19 presets) - -### ๐Ÿ“ Transform-Based - -| Animation | Complexity | Range Support | Directions | Description | -| ---------------------------------------- | ---------- | ------------- | ---------- | --------------------------- | -| **[ParallaxScroll](parallax-scroll.md)** | Simple | โœ“ | - | Classic parallax movement | -| **[MoveScroll](move-scroll.md)** | Medium | โœ“ | 360ยฐ | Directional movement | -| **[GrowScroll](grow-scroll.md)** | Medium | โœ“ | 9-way | Scale from specific origins | -| **[ShrinkScroll](shrink-scroll.md)** | Medium | โœ“ | 9-way | Scale shrinking effects | -| **[SlideScroll](slide-scroll.md)** | Medium | โœ“ | 4-way | Sliding movement effects | -| **[SpinScroll](spin-scroll.md)** | Medium | โœ“ | 2-way | Rotation with scaling | -| **[PanScroll](pan-scroll.md)** | Medium | โœ“ | 2-way | Horizontal panning motion | -| **[SkewPanScroll](skew-pan-scroll.md)** | Medium | โœ“ | 2-way | Skewed panning effects | -| **[StretchScroll](stretch-scroll.md)** | Medium | โœ“ | - | Vertical stretching effects | - -### ๐ŸŽญ Opacity & Visibility - -| Animation | Complexity | Range Support | Description | -| -------------------------------- | ---------- | ------------- | ------------------------- | -| **[FadeScroll](fade-scroll.md)** | Simple | โœ“ | Opacity changes on scroll | -| **[BlurScroll](blur-scroll.md)** | Simple | โœ“ | Blur-to-focus transitions | - -### ๐Ÿ“บ 3D Perspective - -| Animation | Complexity | Range Support | Directions | Description | -| ------------------------------------- | ---------- | ------------- | ---------- | --------------------------- | -| **[ArcScroll](arc-scroll.md)** | Complex | โœ“ | 2-way | Curved 3D motion paths | -| **[FlipScroll](flip-scroll.md)** | Medium | โœ“ | 2-way | 3D flip rotations | -| **[Spin3dScroll](spin-3d-scroll.md)** | Complex | โœ“ | - | 3D rotation with depth | -| **[TiltScroll](tilt-scroll.md)** | Complex | โœ“ | 2-way | Perspective tilting effects | -| **[TurnScroll](turn-scroll.md)** | Complex | โœ“ | 2-way | Complex 3D turning | - -### โœ‚๏ธ Clip & Shape - -| Animation | Complexity | Range Support | Directions | Description | -| ---------------------------------------- | ---------- | ------------- | ---------- | ----------------------- | -| **[RevealScroll](reveal-scroll.md)** | Medium | โœ“ | 4-way | Clean clip-path reveals | -| **[ShapeScroll](shape-scroll.md)** | Complex | โœ“ | 5 shapes | Morphing shape reveals | -| **[ShuttersScroll](shutters-scroll.md)** | Complex | โœ“ | 4-way | Multi-segment reveals | - -## Quick Reference - -### By Use Case - -#### Hero & Background Elements - -**Best**: ParallaxScroll, FadeScroll, BlurScroll -**Alternative**: MoveScroll, GrowScroll - -#### Content Block Reveals - -**Best**: RevealScroll, SlideScroll, FadeScroll -**Alternative**: GrowScroll, MoveScroll - -#### Gallery & Media - -**Best**: GrowScroll, FlipScroll, TiltScroll -**Alternative**: ArcScroll, ShapeScroll - -#### Text & Typography - -**Best**: FadeScroll, RevealScroll, MoveScroll -**Alternative**: BlurScroll, SlideScroll - -#### Creative & Artistic - -**Best**: ArcScroll, TurnScroll, ShapeScroll -**Alternative**: StretchScroll, ShuttersScroll - -### By Range Support - -#### `in` - Entrance Effects - -Elements animate as they enter the viewport - -- All animations support this range - -#### `out` - Exit Effects - -Elements animate as they leave the viewport - -- FadeScroll, BlurScroll, RevealScroll, ShapeScroll - -#### `continuous` - Full Journey - -Animation spans the entire scroll interaction - -- ParallaxScroll, MoveScroll, TiltScroll, PanScroll - -### By Performance - -#### GPU Optimized (60fps) - -- ParallaxScroll, FadeScroll, SlideScroll, SpinScroll - -#### Moderate Performance - -- MoveScroll, GrowScroll, FlipScroll, BlurScroll - -#### Resource Intensive - -- ArcScroll, TurnScroll, StretchScroll, ShuttersScroll - -## Common Configuration Patterns - -### Basic Scroll Animation - -```typescript -import { getScrubScene } from '@wix/motion'; - -const scene = getScrubScene( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 0.5, - }, - }, - { - trigger: 'view-progress', - element: document.body, - }, -); -``` - -### Viewport Range Control - -```typescript -// Animation starts when element is 20% visible -const scene = getScrubScene( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'FadeScroll', - range: 'in', - }, - startOffset: { - name: 'entry', - offset: { value: 20, unit: 'percentage' }, - }, - endOffset: { - name: 'cover', - offset: { value: 0, unit: 'percentage' }, - }, - }, - { - trigger: 'view-progress', - element: element, - }, -); -``` - -### Staggered Scroll Reveals - -```typescript -// Cards reveal one by one as they scroll into view -document.querySelectorAll('.card').forEach((card, index) => { - getScrubScene( - card, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'RevealScroll', - direction: 'bottom', - range: 'in', - }, - startOffset: { - name: 'entry', - offset: { value: index * 10, unit: 'percentage' }, - }, - }, - { - trigger: 'view-progress', - element: card, - }, - ); -}); -``` - -### Hero Parallax Setup - -```typescript -// Multi-layer parallax background -const bgLayers = [ - { element: '.bg-layer-1', speed: 0.2 }, - { element: '.bg-layer-2', speed: 0.4 }, - { element: '.bg-layer-3', speed: 0.6 }, -]; - -bgLayers.forEach((layer) => { - getScrubScene( - document.querySelector(layer.element), - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: layer.speed, - range: 'continuous', - }, - }, - { - trigger: 'view-progress', - element: document.body, - }, - ); -}); -``` - -## Advanced Patterns - -### Intersection-Based Animation - -```typescript -// Only animate when element enters viewport -const observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - // Start scroll animation - const scene = getScrubScene(entry.target, scrollConfig, trigger); - entry.target.scrollScene = scene; - } else { - // Clean up animation - if (entry.target.scrollScene) { - entry.target.scrollScene.destroy(); - } - } - }); -}); - -document.querySelectorAll('.scroll-animate').forEach((el) => { - observer.observe(el); -}); -``` - -### Responsive Scroll Behavior - -```typescript -// Adjust animation based on screen size -const isMobile = window.innerWidth < 768; - -const scrollConfig = { - type: 'ScrubAnimationOptions', - namedEffect: isMobile - ? { type: 'FadeScroll', range: 'in' } // Simple on mobile - : { type: 'ArcScroll', direction: 'horizontal' }, // Complex on desktop -}; -``` - -### Performance-Aware Scroll - -```typescript -// Monitor scroll performance -let lastFrameTime = 0; -const performanceThreshold = 16; // 60fps - -function createOptimizedScrollAnimation(element, config) { - const startTime = performance.now(); - - const scene = getScrubScene(element, config, trigger); - - // Monitor performance - const checkPerformance = () => { - const currentTime = performance.now(); - const frameTime = currentTime - lastFrameTime; - - if (frameTime > performanceThreshold) { - console.warn('Scroll animation dropping frames'); - // Could switch to simpler animation here - } - - lastFrameTime = currentTime; - requestAnimationFrame(checkPerformance); - }; - - requestAnimationFrame(checkPerformance); - return scene; -} -``` - -### Mobile Optimization - -```typescript -const isMobile = window.innerWidth < 768; -const isLowEnd = navigator.hardwareConcurrency < 4; - -const parallaxConfig = { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: isMobile || isLowEnd ? 0.2 : 0.5, // Gentler on mobile - range: 'continuous', - }, -}; -``` - -## Browser Support & Polyfills - -### ViewTimeline API Support - -```typescript -// Feature detection and progressive enhancement -if (window.ViewTimeline) { - // Native ViewTimeline API support - const scene = getWebAnimation(element, config, trigger); -} else { - // Fallback to using a polyfill - const scene = getScrubScene(element, config, trigger); - // pass scene to polyfill library -} -``` - -### Reduced Motion Integration - -```typescript -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -if (prefersReducedMotion) { - // Disable scroll animations or use gentler alternatives - const config = { type: 'FadeScroll', range: 'in' }; -} else { - // Full scroll animation experience - const config = { type: 'ArcScroll', direction: 'horizontal' }; -} -``` - -## Framework Integration - -### React Hook - -```typescript -import React, { useEffect, useRef } from 'react'; -import { getScrubScene } from '@wix/motion'; - -interface ParallaxProps { - children: React.ReactNode; - speed?: number; - range?: 'in' | 'out' | 'continuous'; - className?: string; -} - -function ParallaxScroll({ - children, - speed = 0.5, - range = 'continuous', - className -}: ParallaxProps) { - const elementRef = useRef(null); - const sceneRef = useRef(null); - - useEffect(() => { - if (!elementRef.current) return; - - sceneRef.current = getScrubScene(elementRef.current, { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed, - range - } - }, { - trigger: 'view-progress', - element: document.body - }); - - return () => { - if (sceneRef.current?.destroy) { - sceneRef.current.destroy(); - } - }; - }, [speed, range]); - - return ( -
- {children} -
- ); -} - -// Usage -function Hero() { - return ( -
- - Hero background - - -
-

Hero Title

-

Hero description

-
-
- ); -} -``` - -### Vue Component - -```vue - - - -``` - -## Accessibility - -### Reduced Motion Support - -```typescript -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -if (prefersReducedMotion) { - // Disable parallax scrolling - console.log('Parallax disabled for reduced motion preference'); -} else { - // Enable parallax - setupParallax(); -} -``` - ---- - -**[Back to All Presets](../) | [Browse Other Categories](../../categories/)** diff --git a/packages/motion-presets/docs/presets/scroll/parallax-scroll.md b/packages/motion-presets/docs/presets/scroll/parallax-scroll.md deleted file mode 100644 index afdb3cb3..00000000 --- a/packages/motion-presets/docs/presets/scroll/parallax-scroll.md +++ /dev/null @@ -1,359 +0,0 @@ -# ParallaxScroll - -Classic parallax scrolling effect where elements move at different speeds relative to the scroll position. Creates depth and immersion by making background elements move slower than foreground content. - -## Overview - -**Category**: Scroll -**Complexity**: Simple -**Performance**: GPU Optimized -**Mobile Friendly**: Yes (with speed adjustment) - -### Best Use Cases - -- Hero section backgrounds and layered content -- Image backgrounds that should move independently -- Creating depth in landing pages and storytelling -- Multi-layer background compositions -- Video backgrounds with subtle movement - -### Target Elements - -- Background images and media -- Large content sections and hero areas -- Decorative elements and illustrations -- Video backgrounds and overlays - -## Configuration - -### TypeScript Interface - -```typescript -export type ParallaxScroll = BaseDataItemLike<'ParallaxScroll'> & { - speed: number; - range?: EffectScrollRange; -}; -``` - -### Parameters - -| Parameter | Type | Default | Description | Examples | -| --------- | -------- | -------------- | ---------------------------------- | ------------------------------- | -| `speed` | `number` | `0.5` | Parallax speed relative to scroll | `0.1`, `0.5`, `1.0`, `2.0` | -| `range` | `string` | `'continuous'` | When the parallax effect is active | `'in'`, `'out'`, `'continuous'` | - -### Speed Values - -- **`0.1 - 0.3`** - Very slow parallax, subtle depth effect -- **`0.4 - 0.6`** - Standard parallax, noticeable but natural -- **`0.7 - 1.0`** - Fast parallax, dramatic movement -- **`> 1.0`** - Reverse or exaggerated parallax effects - -### Range Options - -- **`continuous`** - Parallax active throughout scroll interaction (default) -- **`in`** - Parallax only as element enters viewport -- **`out`** - Parallax only as element exits viewport - -## Usage Examples - -### Basic Usage - -```typescript -import { getWebAnimation } from '@wix/motion'; - -const scene = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 0.5, // Element moves at 50% of scroll speed - }, - }, - { - trigger: 'view-progress', - element, - }, -); -``` - -### Speed Variations - -```typescript -// Slow, subtle parallax for professional sites -const subtleParallax = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 0.2, - range: 'continuous', - }, - }, - { - trigger: 'view-progress', - element, - }, -); - -// Fast, dramatic parallax for creative sites -const dramaticParallax = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 0.8, - range: 'continuous', - }, - }, - { - trigger: 'view-progress', - element, - }, -); - -// Reverse parallax (moves faster than scroll) -const reverseParallax = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 1.5, // Moves 50% faster than scroll - range: 'continuous', - }, - }, - { - trigger: 'view-progress', - element, - }, -); -``` - -### Range-Based Parallax - -```typescript -// Parallax only during element entrance -const entranceParallax = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 0.6, - range: 'in', - }, - }, - { - trigger: 'view-progress', - element, // Relative to element itself - }, -); - -// Parallax only during element exit -const exitParallax = getScrgetWebAnimationubScene( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 0.4, - range: 'out', - }, - }, - { - trigger: 'view-progress', - element, - }, -); -``` - -### Custom Viewport Offsets - -```typescript -// Start parallax when element is 20% visible -const customRangeParallax = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 0.5, - }, - startOffset: { - name: 'entry', - offset: { value: 20, unit: 'percentage' }, - }, - endOffset: { - name: 'exit', - offset: { value: 80, unit: 'percentage' }, - }, - }, - { - trigger: 'view-progress', - element, - }, -); -``` - -## Common Patterns - -### Multi-Layer Parallax - -```typescript -// Create depth with multiple parallax layers -const parallaxLayers = [ - { selector: '.bg-layer-far', speed: 0.1 }, - { selector: '.bg-layer-mid', speed: 0.3 }, - { selector: '.bg-layer-near', speed: 0.6 }, - { selector: '.fg-content', speed: 1.0 }, // Normal scroll speed -]; - -parallaxLayers.forEach((layer) => { - const element = document.querySelector(layer.selector); - if (element) { - getScrubScene( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: layer.speed, - range: 'continuous', - }, - }, - { - trigger: 'view-progress', - element, - }, - ); - } -}); -``` - -### Gallery Parallax - -```typescript -// Parallax effect for image gallery -document.querySelectorAll('.gallery-item').forEach((item, index) => { - const speed = 0.4 + (index % 3) * 0.1; // Vary speed: 0.4, 0.5, 0.6 - - getWebAnimation( - item, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: speed, - range: 'in', - }, - }, - { - trigger: 'view-progress', - element: item, - }, - ); -}); -``` - -### Video Background Parallax - -```typescript -// Subtle parallax for video backgrounds -function setupVideoParallax() { - const videoContainer = document.querySelector('.video-background'); - - getWebAnimation( - videoContainer, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 0.25, // Very subtle for video - range: 'continuous', - }, - }, - { - trigger: 'view-progress', - element: videoContainer, - }, - ); -} -``` - -### Section-Based Parallax - -```typescript -// Different parallax for each page section -document.querySelectorAll('.parallax-section').forEach((section) => { - const background = section.querySelector('.section-bg'); - const speed = parseFloat(section.dataset.parallaxSpeed) || 0.5; - - if (background) { - getWebAnimation( - background, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: speed, - range: 'continuous', - }, - }, - { - trigger: 'view-progress', - element: section, - }, - ); - } -}); -``` - -### Vestibular Safety - -ParallaxScroll creates relative motion which may trigger vestibular disorders. Always respect reduced motion preferences and provide alternatives. - -## Browser Support - -- **Web Animations API**: Full support in modern browsers -- **ViewTimeline API**: Chrome 115+, polyfill available for other browsers - -## Related Animations - -### Same Category - -- **[MoveScroll](move-scroll.md)** - Directional movement with custom angles -- **[FadeScroll](fade-scroll.md)** - Opacity-based scroll effects -- **[SlideScroll](slide-scroll.md)** - Sliding movement effects - -### Other Categories - -- **[BgParallax](../background-scroll/bg-parallax.md)** - Specialized background parallax -- **[ImageParallax](../background-scroll/image-parallax.md)** - Enhanced image parallax - -### Complementary Effects - -- **Before**: Element entrance animations -- **After**: Hover effects, interaction states -- **Alongside**: Fade effects, color transitions - -## Troubleshooting - -### Common Issues - -- **Choppy scrolling**: Reduce number of parallax elements or simplify effects -- **Element jumping**: Check for conflicting CSS transforms -- **Mobile performance**: Use gentler speeds and consider disabling on low-end devices - ---- - -## Interactive Example - -โ–ถ๏ธ **[Try it in Storybook](../../playground/)** - Experiment with ParallaxScroll speeds and ranges - ---- - -**[Back to Scroll Animations](../) | [Back to All Presets](../../)** diff --git a/packages/motion/README.md b/packages/motion/README.md index ed0dde55..599d4e45 100644 --- a/packages/motion/README.md +++ b/packages/motion/README.md @@ -180,8 +180,9 @@ Motion is the engine layer. The other packages in this repo build on top of it: - [Getting Started](https://github.com/wix/interact/blob/master/packages/motion/docs/getting-started.md) - [Core Concepts](https://github.com/wix/interact/blob/master/packages/motion/docs/core-concepts.md) - [API Reference](https://github.com/wix/interact/blob/master/packages/motion/docs/api/README.md) -- [Category Guides](https://github.com/wix/interact/blob/master/packages/motion/docs/categories/README.md) -- [Advanced Patterns](https://github.com/wix/interact/blob/master/packages/motion/docs/guides/README.md) +- [Guides](https://github.com/wix/interact/blob/master/packages/motion/docs/guides/README.md) โ€” custom effects, SSR/CSS generation, performance + +For the ready-made effect catalog (entrance, ongoing, scroll, mouse presets), see [`@wix/motion-presets`](https://github.com/wix/interact/tree/master/packages/motion-presets). ## License diff --git a/packages/motion/docs/DOCS_AUDIT.md b/packages/motion/docs/DOCS_AUDIT.md new file mode 100644 index 00000000..958ec553 --- /dev/null +++ b/packages/motion/docs/DOCS_AUDIT.md @@ -0,0 +1,498 @@ +# `@wix/motion` Documentation Audit + +> **Scope:** This audit covers **only** `packages/motion/docs/` โ€” the documentation for the `@wix/motion` +> package. It deliberately treats preset content (`@wix/motion-presets`) and the declarative +> interaction layer (`@wix/interact`) as **out of scope**: those packages own their own docs/rules +> (`packages/motion-presets/rules/`, `packages/interact/rules/`). +> +> **Method:** Every "inaccurate" claim below was checked against the current implementation in +> `packages/motion/src/` (commit on branch `docs_cleanup`). Source references are given as +> `file:line` so each finding is verifiable. +> +> **Date:** 2026-06-29 + +--- + +## 1. Executive summary + +The `docs/` tree was generated from an early exploration plan (`PLAN_DOCS.md`) that targeted a +_combined_ motion + presets surface ("82+ presets in 5 categories"). The package has since diverged +substantially, and most of the prose pre-dates the current API. The result: + +- **A large fraction of the docs are inaccurate against the current implementation** โ€” most damagingly, + a fabricated `type: 'TimeAnimationOptions' | 'ScrubAnimationOptions'` discriminator that appears in + **almost every code sample** and **does not exist in the types**. +- **A large fraction is out of scope** โ€” the entire `categories/` tree and the "Named Effect Types" + section of `api/types.md` document _presets_, which now live in `@wix/motion-presets` and are + documented in `packages/motion-presets/rules/`. +- **The genuinely good, current material is small and isolated**: the package `README.md`, + `api/sequence.md`, and `api/get-sequence.md` are accurate and match the code. They are the template + the rest should be rebuilt against. +- **There are no LLM-facing rules** for motion, unlike its siblings. This is the highest-value gap to + fill, because motion is the layer an agent reaches for when `@wix/interact`'s declarative config is + not enough (custom render callbacks, manual scrub-scene driving, sequences, SSR CSS generation). + +**Recommendation:** Delete the stale/out-of-scope material, rewrite the small accurate core to match +the `README`, and author a new `packages/motion/rules/` set modeled on +`motion-presets/rules/presets/presets-main.md` and `interact/rules/integration.md`. + +--- + +## 2. Document inventory & verdict + +| File | Intended purpose | Accuracy | Verdict | +| -------------------------------------------- | ------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PLAN_DOCS.md` | Original planning doc | n/a (stale plan) | **Delete** โ€” historical artifact; describes a `presets/` tree that was never built / since removed. | +| `getting-started.md` | First animation in 10 min | Low | **Rewrite** โ€” pervasive fake `type:` field, wrong easing names, wrong CDN path, misleading `play()` semantics. | +| `core-concepts.md` | Mental model | Lowโ€“Med | **Rewrite & trim** โ€” good Sequence section; rest mixes preset content + fabricated APIs. | +| `api/README.md` | API index | Med | **Rewrite** โ€” index is fine; embedded type snippets carry the fake `type:` field and fabricated unions. | +| `api/core-functions.md` | Core function reference | **Low (critical)** | **Rewrite** โ€” `getCSSAnimation` documented as returning a `string` (it returns an array of descriptors); wrong return types; fake `type:` everywhere. | +| `api/animation-group.md` | `AnimationGroup` class | Med | **Revise** โ€” method docs mostly correct but class surface is incomplete and examples use the fake `type:` field. | +| `api/sequence.md` | `Sequence` class | **High** | **Keep** (minor fix: `finished` is `Promise`). Matches code. | +| `api/get-sequence.md` | `getSequence`/`createAnimationGroups` | **High** | **Keep**. Matches code; uses correct `keyframeEffect` shape. | +| `api/types.md` | Type reference | **Low (critical)** | **Split**: keep core motion types (rewritten); delete the "Named Effect Types" + fabricated helper sections. | +| `categories/README.md` | Category overview | Out of scope | **Move/Delete** โ€” presets belong to `@wix/motion-presets`. | +| `categories/entrance-animations.md` | Entrance presets | Out of scope + inaccurate | **Delete**. | +| `categories/ongoing-animations.md` | Ongoing presets | Out of scope | **Delete**. | +| `categories/scroll-animations.md` | Scroll presets | Out of scope | **Delete**. | +| `categories/mouse-animations.md` | Mouse presets | Out of scope | **Delete**. | +| `categories/background-scroll-animations.md` | BG-scroll presets | Out of scope + stale category | **Delete** โ€” `background-scroll` is no longer a preset category. | +| `guides/README.md` | Guide index | Low | **Rewrite/trim** โ€” links to non-existent `testing.md`. | +| `guides/advanced-patterns.md` | Advanced patterns | Low | **Delete most** โ€” ~1.4k lines of invented framework code (HammerJS controllers, animation pools, state machines) using fabricated APIs. Salvage only the "custom effect authoring" idea (rewritten & corrected). | +| `guides/framework-integration.md` | React/Vue/Angular | Low | **Trim to React** โ€” Vue/Angular sections are speculative and contain `[TBD]` placeholders. | +| `guides/performance.md` | Performance | Med | **Trim** โ€” real `fastdom`/CSS guidance is buried under invented profiler/debugger classes; repeats the `getCSSAnimation`-returns-string error. | +| `examples/README.md` | Examples index | Low | **Rewrite** โ€” links to 3 files that don't exist + `../playground/` that doesn't exist. | + +--- + +## 3. Critical inaccuracies (wrong vs. current implementation) + +These are correctness bugs in the docs โ€” code that, if copied, will not compile or will not behave as +described. + +### 3.1 The fabricated `type: 'TimeAnimationOptions' | 'ScrubAnimationOptions'` discriminator (pervasive) + +Nearly every example sets a top-level `type` field on the options object: + +```typescript +getWebAnimation(el, { + type: 'TimeAnimationOptions', + namedEffect: { type: 'FadeIn' }, + duration: 1000, +}); +``` + +**No such field exists.** `TimeAnimationOptions` (`src/types.ts:143`) and `ScrubAnimationOptions` +(`src/types.ts:179`) have no `type` property; the union `AnimationOptions` (`src/types.ts:113`) is +discriminated **structurally** โ€” the engine branches on the _presence_ of `keyframeEffect` / +`namedEffect` / `customEffect` (`src/api/common.ts:64-86`) and on the `trigger` argument, never on a +`type` string. The correct call is: + +```typescript +getWebAnimation(el, { namedEffect: { type: 'FadeIn' }, duration: 1000 }); +``` + +Appears in: `getting-started.md`, `core-concepts.md`, `api/README.md`, `api/core-functions.md`, +`api/animation-group.md`, `api/types.md`, `categories/*`, all three `guides/*`, `examples/README.md`. +This single error is the most important thing to fix and is the strongest signal the docs are stale. + +### 3.2 `getCSSAnimation()` does **not** return a string + +`api/core-functions.md:344-394` and `guides/performance.md:23-42` document the signature as +`): string` and show `style.insertRule(cssRules)`. The implementation (`src/api/cssAnimations.ts:51-80`) +returns **an array of descriptor objects**: + +```typescript +{ + (target, animation, composition, custom, name, keyframes, id, animationTimeline, animationRange); +} +[]; +``` + +The package `README.md` and `getting-started.md` correctly iterate the array +(`cssAnimations.forEach(({ target, animation, keyframes }) => โ€ฆ)`). The core-functions/performance +docs contradict both reality and the rest of the docs. (The example also contains a `` typo and +creates a `CSSStyleSheet` but calls `style.insertRule`, never defining `style`.) + +### 3.3 Fabricated / non-exported types + +`api/types.md`, `api/README.md`, and the guides reference many types that **the package does not +export** (verified by grep against `src/`): + +- `EntranceAnimation`, `OngoingAnimation`, `ScrollAnimation`, `MouseAnimation`, + `BackgroundScrollAnimation`, and a `NamedEffect` defined as their union. **Reality:** + `NamedEffect = { type: string } & Record` (`src/types.ts:99`). The per-effect types + (`FadeIn`, `ArcIn`, `ScaleMouse`, `BgZoom`, โ€ฆ) are not in `@wix/motion` at all โ€” they belong to + `@wix/motion-presets`. +- `BaseDataItemLike` โ€” does not exist anywhere in the repo; invented as a base for the fake + effect types and reused in the guides' "custom effect" examples. +- `MouseEffectAxis = 'both' | 'horizontal' | 'vertical'` and `MousePivotAxis` โ€” do not exist. The real + axis type is `PointerMoveAxis = 'x' | 'y'` (`src/types.ts:158`), and it is set on the **trigger** + object, not on the effect. +- `MotionKeyframeEffect` is documented (`api/types.md:672`) as + `BaseDataItemLike<'KeyframeEffect'> & { name; keyframes }` with examples setting + `type: 'KeyframeEffect'`. **Reality:** `MotionKeyframeEffect = { name: string; keyframes: Keyframe[] }` + (`src/types.ts:138`) โ€” no `type` field. +- The `isTimeAnimation` / `isScrubAnimation` type guards (`api/types.md:766-781`) test + `options.type === โ€ฆ`, which is always `undefined` (see 3.1). The `createTimeAnimation` / + `createScrubAnimation` / `TypedAnimationFactory` helpers do not exist in the library. + +### 3.4 `customEffect` โ€” wrong shape documented; the working shape omitted + +`core-concepts.md:204-211` and `api/types.md:662-688` present `customEffect` exclusively as +`{ ranges: [{ name, min, max, step }] }` and call it "Full programmatic control." + +**Reality** (`src/types.ts:101-105`): `CustomEffect` is a union โ€” `{ ranges: โ€ฆ } | ((element, progress) => void)`. +Only the **function** form does anything at runtime: `getWebAnimation` builds a `CustomAnimation` +(driving a `requestAnimationFrame` loop) **only when `typeof customEffect === 'function'`** +(`src/api/webAnimations.ts:146`, `src/CustomAnimation.ts`). The `{ ranges }` object form is passed +through to a keyframe-less animation (`src/api/common.ts:82-84`) and produces no visible effect on its +own. The docs document the inert form as the primary one and never show the functional callback that +actually works. + +### 3.5 "Animation Groups" example โ€” `getWebAnimation` does not accept an array + +`core-concepts.md:310-320` shows: + +```typescript +const group = getWebAnimation(element, [{ namedEffect: โ€ฆ }, { namedEffect: โ€ฆ }]); +``` + +`getWebAnimation` takes a **single** `AnimationOptions` object (`src/api/webAnimations.ts:60-66`). An +`AnimationGroup` wraps the multiple `Animation`s produced by _one_ options object; you do not pass an +array. To coordinate multiple elements/effects use `getSequence(options, AnimationGroupArgs[])` +(`src/motion.ts:261`). + +### 3.6 Return types omit `null` and array variants + +- `getWebAnimation` is documented as `: AnimationGroup | MouseAnimationInstance` + (`api/core-functions.md:27`, `api/types.md:519`). Real signature returns + `AnimationGroup | MouseAnimationInstance | null` (`src/api/webAnimations.ts:66`). Examples like + `const animation: AnimationGroup = getWebAnimation(...)` won't type-check. +- `getScrubScene` is documented as `: ScrubScrollScene[] | ScrubPointerScene` + (`api/core-functions.md:212`). Real return is + `ScrubScrollScene[] | ScrubPointerScene | ScrubPointerScene[] | null` (`src/motion.ts:79`). The + keyframe-driven pointer path returns a single scene while the named-effect path can differ, so docs + that index `getScrubScene(...)[0]` for pointer scenes (`guides/advanced-patterns.md:1129`) are unsafe. + +### 3.7 Easing names that don't exist + +Docs reference easings that are **not** in the exported easing maps (`src/easings.ts`): + +- `easeOutCubic` (`getting-started.md:90`) โ€” not a key. Closest real keys: `cubicOut` (JS) or the CSS + alias `easeOut` โ†’ `ease-out`. +- `elasticOut`, `bounceOut`, `bounceIn` (`core-concepts.md:223`, `categories/README.md:111`) โ€” none + exist. (`elastic` and `bounce` exist only as `ScrubTransitionEasing` values for pointer smoothing, + `src/types.ts:131` โ€” a different field.) + +The real, exported sets are `jsEasings` (Penner functions, `src/easings.ts:187`) and `cssEasings` +(named โ†’ `cubic-bezier(...)`, `src/easings.ts:218`), resolved via `getEasing` / `getJsEasing` +(`src/utils.ts`). Valid named keys include `linear`, `ease`, `easeIn/Out/InOut`, and +`{sine,quad,cubic,quart,quint,expo,circ,back}{In,Out,InOut}`. + +### 3.8 Angle/direction convention contradicts the presets convention + +`core-concepts.md:247` and `categories/entrance-animations.md:87` state +`0ยฐ = up, 90ยฐ = right, 180ยฐ = down, 270ยฐ = left`. The authoritative presets rule states the opposite โ€” +`0ยฐ = right (east), angles increase counter-clockwise` (`motion-presets/rules/presets/presets-main.md:107`). +Since angles are a **preset** parameter, this should not live in motion docs at all; while it does, it +is wrong. + +### 3.9 `play()` resolution semantics misrepresented + +`getting-started.md:94-97` implies `await animation.play()` resolves on **completion** +(`console.log('Animation completed!')`). `AnimationGroup.play` (`src/AnimationGroup.ts:39-53`) awaits +`ready` and each animation's `ready` โ€” i.e. it resolves once playback has **started**, not finished. +Completion is observed via `onFinish(cb)` or the `finished` promise. Relatedly, `finished` is typed in +the docs as `Promise` (`api/animation-group.md:527`, `api/types.md:541`) but is +`Promise` (`Promise.all`, `src/AnimationGroup.ts:142-144`). + +### 3.10 Smaller factual errors + +- **CDN/import path:** `getting-started.md:25` imports from `โ€ฆ/dist/esm/index.js`. The published ESM + entry is `dist/es/motion.js` (`package.json` `module`; confirmed in `dist/es/`). +- **Node version:** `getting-started.md:8` says "Node.js 16+". `package.json` `engines` requires + `>=18` (and the repo pins a version via `nvm use`). +- **4th `options` argument:** `api/core-functions.md:96-101` claims it accepts `effectId` and + `measurementCallback`. `effectId` is read from `animationOptions.effectId` + (`src/api/webAnimations.ts:114`), not the 4th arg; `measurementCallback` is not a recognized key. The + options bag that the engine actually reads is `{ reducedMotion }` (`src/api/webAnimations.ts:38`) and, + for `getScrubScene`, `{ disabled, allowActiveEvent }` (`src/motion.ts:80`). +- **`getScrubScene` trigger object:** `api/core-functions.md:229-243` places `startOffset`/`endOffset` + inside the `trigger` argument. Those are properties of the **animation options** + (`ScrubAnimationOptions`, `src/types.ts:165-166`), not the trigger. +- **`NodeList.map`:** several examples call `.map` directly on `querySelectorAll(...)` results + (`api/core-functions.md:455`, `examples/README.md:65`) โ€” needs `Array.from(...)`. +- **`iterations: Infinity`:** works, but the engine's idiom is `iterations: 0` โ†’ `Infinity` + (`src/api/common.ts:100`; CSS path `src/api/cssAnimations.ts:30`). Worth standardizing. + +--- + +## 4. Out of scope โ€” belongs to other packages + +The motion docs repeatedly document things that are owned elsewhere. This is both redundant and a +drift hazard (two sources of truth that disagree โ€” see 3.8). + +### 4.1 Preset catalog โ†’ `@wix/motion-presets` + +- The **entire `categories/` tree** (6 files, ~2,775 lines) documents preset names, parameters, and + selection guidance. This is exactly what `packages/motion-presets/rules/presets/` now owns + (`presets-main.md` + `entrance/scroll/ongoing/mouse-presets.md`), and the presets rules are current + while these are not: + - Counts disagree: motion docs claim **"82+ presets, 5 categories"** including **background-scroll**; + the presets rules list **entrance 19, scroll 19, ongoing 13, mouse 11** and **no background-scroll + category** (its docs were removed โ€” see deleted files in `git status`). + - Preset params here (`SlideIn`, `Spin direction:'clockwise'`, `BgFake3D`, `ScaleMouse`, etc.) are not + defined by `@wix/motion` and several don't match the presets library. +- The **"Named Effect Types" section of `api/types.md`** (lines ~139-426) is the same problem in type + form. + +`@wix/motion`'s only contract with presets is the **registry** (`registerEffects`, `src/api/registry.ts`) +and the structural `EffectModule` shape (`src/types.ts:261`). Motion docs should document _that +contract_ and link to the presets package for the catalog โ€” not re-list presets. + +### 4.2 Declarative/framework concerns โ†’ `@wix/interact` + +- The Vue/Angular integration sections (`guides/framework-integration.md:314-965`) are speculative + wrappers (with `[TBD]` placeholders) around motion's imperative API. Reactive lifecycle binding, + triggers, and config-driven orchestration are `@wix/interact`'s job (`interact/rules/integration.md`). + Motion docs should show the minimal framework-agnostic lifecycle (create โ†’ play โ†’ `cancel` on + teardown) and point to `@wix/interact` for declarative usage. + +--- + +## 5. Redundant / unnecessary / low-value content + +- **`PLAN_DOCS.md`** โ€” a planning transcript ("Switch to agent mode and type execute"). Not + documentation. Delete. +- **`guides/advanced-patterns.md`** โ€” ~1,400 lines of invented application code: + `AdvancedParallaxSystem`, `ScrollManager`, `AnimationChoreographer`, `StateMachine`, + `GestureAnimationController` (depends on HammerJS), `ScrollStorytellingEngine`, `AnimationPool`, + `GlobalAnimationManager`. None of it is part of `@wix/motion`; it uses fabricated APIs + (`progressFunction` on scroll data, `BaseDataItemLike`, `type:` discriminator, non-existent preset + params like `DropIn.initialScale`, `SpinIn.spins`). It reads as "things you could build," not docs + for this package. Salvage only the **custom-effect authoring** concept (rewritten โ€” see ยง6). +- **`guides/performance.md`** โ€” the genuinely useful guidance (prefer transform/opacity, batch via + `fastdom`, CSS for fire-and-forget, pause off-screen loops) is ~20 lines, buried under invented + `AnimationProfiler` / `AnimationDebugger` classes. Trim hard. +- **Cross-file duplication** โ€” the same fade/slide/bounce examples and the same "CSS vs WAAPI" table are + repeated across `README`, `getting-started`, `core-concepts`, `api/core-functions`, `categories`, and + `guides/performance`. Consolidate to one canonical location each. + +--- + +## 6. Missing โ€” what motion docs _should_ cover but don't + +These are the topics unique to `@wix/motion` that an engineer or agent actually needs, and that are +currently absent, buried, or wrong. They should anchor the rewrite. + +1. **The three effect-definition modes, accurately** (`src/api/common.ts:64-86`): + - `keyframeEffect: { name, keyframes }` โ€” inline, zero registration. + - `customEffect: (element, progress) => void` โ€” per-frame JS callback via `CustomAnimation`'s rAF + loop; called with `null` on cancel (`src/CustomAnimation.ts:155-163`). **This is the only + "programmatic" mode and is currently mis-documented.** + - `namedEffect: { type, โ€ฆparams }` โ€” requires registration. +2. **`registerEffects` + the `EffectModule` contract** (`src/api/registry.ts`, `src/types.ts:57-66, +249-266`): the `{ web, getNames, style?, prepare? }` shape, what each returns (`AnimationData[]`), + and how to author a custom registered effect. (`getting-started.md` shows the object shape correctly; + the guides show it as loose free functions โ€” reconcile.) +3. **Scrub-scene driving contract** for the polyfill path: `getScrubScene` returns scenes exposing + `start` / `end` / `viewSource` / `ready` / `getProgress()` / `effect(_, progress)` / `destroy()` + (`src/types.ts:222-245`, `src/motion.ts:95-195`). How to drive `effect()` from an + IntersectionObserver/scroll or pointer listener, and that `@wix/interact` does this automatically via + `fizban`. This is motion's most differentiated capability and is undocumented in practical terms. +4. **Native ViewTimeline vs. polyfill behavior** (`src/api/common.ts:106-120`, + `src/api/webAnimations.ts:116-190`): when `window.ViewTimeline` exists, WAAPI is linked to the + timeline with `duration: 'auto'`; otherwise duration becomes `99.99ms`/`delay 0.01ms` so progress is + externally scrubbable. `getScrubScene` only emits scenes on the no-ViewTimeline branch + (`src/motion.ts:90`). +5. **Pointer-move specifics:** `axis: 'x' | 'y'` lives on the **trigger** (`src/motion.ts:123-145`); the + `Progress` payload `{ x, y, v?, active? }` (`src/types.ts:74`); `centeredToTarget`, + `transitionDuration` / `transitionEasing` smoothing (`ScrubTransitionEasing`), and `allowActiveEvent`. +6. **Reduced-motion handling:** the `{ reducedMotion }` option collapses single-iteration animations to + `duration: 1` and drops multi-iteration ones (`src/api/webAnimations.ts:38-44`); threaded through + `getAnimation`/`getSequence` via `context.reducedMotion` (`src/motion.ts:198-267`). +7. **The easing system as an exported API:** `getEasing`/`getJsEasing` are public (`src/index.ts`), + `getJsEasing` also parses `cubic-bezier(...)` and CSS `linear(...)` strings (`src/utils.ts:60-187`). + Note the correct spelling is `cubic-bezier(...)` (hyphenated) โ€” `api/types.md:854` writes + `cubicBezier(...)`, which won't parse. +8. **`data-motion-part` targeting:** effects can target sub-parts via + `[data-motion-part~="โ€ฆ"]` (`src/api/common.ts:19-24`, `src/api/cssAnimations.ts:10-12`). Undocumented. +9. **Full `AnimationGroup` surface:** `onAbort`, `hasAnimationName`, `hasAnimationId`, + `getTimingOptions`, `isCSS`, `longestAnimation`, and the `playState` aggregation rule ("running if + any is running," `src/AnimationGroup.ts:146-150`). Current `api/animation-group.md` omits these and + the dispatched `animationend`/`animationcancel` CustomEvents (`src/AnimationGroup.ts:99-140`). +10. **SSR / CSS-generation contract:** `getCSSAnimation` descriptor fields and their meaning + (`animationTimeline`, `animationRange`, `custom`, `composition`), and the `forCSS` flag forcing + `duration: 'auto'` for scroll animations regardless of runtime ViewTimeline support + (`src/api/common.ts:110-113`). This is how the layer above renders FOUC-free CSS. +11. **Package boundary / "when to use what":** a short map โ€” use `@wix/interact` for declarative + triggerโ†’effect wiring; use `@wix/motion-presets` for the catalog; drop to `@wix/motion` directly for + custom render callbacks, manual scrub driving, programmatic sequences, or custom CSS generation. + +--- + +## 7. Broken links & structural issues + +- `examples/README.md` links to `common-patterns.md`, `real-world-implementations.md`, + `interactive-demos.md` โ€” **none exist** (the `examples/` dir contains only `README.md`). +- `guides/README.md` links to `testing.md` โ€” **does not exist** (only `advanced-patterns`, + `framework-integration`, `performance`). +- `../playground/` and `../../playground/` are referenced from `getting-started.md`, `core-concepts.md`, + `examples/README.md` โ€” **no `playground/` directory exists** in the package. +- `framework-integration.md` ships `[TBD]` placeholders (lines 254, 311, 497, 527) โ€” incomplete content + in published docs. +- Category cross-links (`categories/entrance-animations.md` "Detailed Animation Guides" โ†’ per-preset + files) point at a `presets/` tree that doesn't exist. + +--- + +## 8. Recommended docs structure + +Keep human-facing docs lean and accurate; let the rules (see ยง9) carry the dense, agent-facing detail. +Single source of truth per topic. + +``` +packages/motion/ +โ”œโ”€โ”€ README.md # KEEP โ€” already accurate; canonical overview +โ”œโ”€โ”€ docs/ +โ”‚ โ”œโ”€โ”€ getting-started.md # REWRITE โ€” match README; one runnable example per mode +โ”‚ โ”œโ”€โ”€ core-concepts.md # REWRITE+TRIM โ€” effect modes, triggers, ViewTimeline/polyfill, +โ”‚ โ”‚ # easing, reduced motion, package boundary +โ”‚ โ”œโ”€โ”€ api/ +โ”‚ โ”‚ โ”œโ”€โ”€ README.md # REWRITE โ€” index + accurate signatures only +โ”‚ โ”‚ โ”œโ”€โ”€ core-functions.md # REWRITE โ€” fix return types (esp. getCSSAnimation) +โ”‚ โ”‚ โ”œโ”€โ”€ animation-group.md # REVISE โ€” complete the class surface; drop fake `type:` +โ”‚ โ”‚ โ”œโ”€โ”€ sequence.md # KEEP (minor fix) +โ”‚ โ”‚ โ”œโ”€โ”€ get-sequence.md # KEEP +โ”‚ โ”‚ โ””โ”€โ”€ types.md # REWRITE โ€” motion-owned types only; delete preset/union/helper types +โ”‚ โ””โ”€โ”€ guides/ +โ”‚ โ”œโ”€โ”€ custom-effects.md # NEW โ€” replaces advanced-patterns; registerEffects + EffectModule + +โ”‚ โ”‚ # customEffect callback + scrub-scene driving +โ”‚ โ”œโ”€โ”€ ssr-css.md # NEW โ€” getCSSAnimation descriptors + FOUC/SSR contract +โ”‚ โ””โ”€โ”€ performance.md # TRIM โ€” keep the real ~20 lines +โ”‚ +โ”œโ”€โ”€ rules/ # NEW โ€” see ยง9 +โ”‚ โ”œโ”€โ”€ motion-main.md +โ”‚ โ”œโ”€โ”€ waapi.md +โ”‚ โ”œโ”€โ”€ scrub-scenes.md +โ”‚ โ”œโ”€โ”€ sequences.md +โ”‚ โ”œโ”€โ”€ custom-effects.md +โ”‚ โ””โ”€โ”€ css-generation.md +``` + +**Delete:** `PLAN_DOCS.md`, `categories/` (entire), `guides/advanced-patterns.md`, +`guides/framework-integration.md` (or reduce to a tiny React note), `examples/` (fold a couple of +verified snippets into `getting-started.md`). + +--- + +## 9. LLM-facing rules: what they should contain & how to structure them + +Motion is the one package in the monorepo with **no** `rules/`. Add `packages/motion/rules/`, modeled +on the two proven in-repo templates: + +- `packages/motion-presets/rules/presets/presets-main.md` โ€” frontmatter with a "read this whenโ€ฆ" + `description`, a Table of Contents, terminology table, dense parameter tables, and an explicit "LLM + Guidance Principles" section. +- `packages/interact/rules/integration.md` โ€” task-oriented, hub-and-spoke (a main file linking to + trigger-specific files), heavy use of "MUST"/"Rules" callouts and signature tables. + +### 9.1 Authoring principles (apply to every rule file) + +1. **Accuracy is the contract.** Every signature, type, field, and default must be copied from + `src/`, not paraphrased from memory. The current docs failed precisely here โ€” do not repeat it. +2. **No fabrication.** Never invent types, fields, or easing names. If a value set is finite, list it + verbatim from source. The structural `AnimationOptions` discrimination (no `type:` field) must be + stated explicitly because it is counter-intuitive and was the #1 historical error. +3. **Stay in lane.** Presets โ†’ link to `@wix/motion-presets`. Declarative/triggers โ†’ link to + `@wix/interact`. Motion rules cover the imperative engine only. +4. **Frontmatter routing.** Each file starts with `name` + a `description` that says _when to read it_ + (so the agent can select the right file), exactly like `presets-main.md`. +5. **Show the gotchas.** `play()` resolves on start; `getCSSAnimation` returns an array; + `customEffect` must be a function to do anything; pointer `axis` is on the trigger; `0`โ†’`Infinity` + iterations. These are the things an agent gets wrong without being told. +6. **Link to `file:line` in `src/`** for the authoritative definition of each contract, so the rules + can be re-verified when code changes (and add a drift check to CI if feasible, mirroring + `interact-validate`'s schema drift guard). + +### 9.2 Proposed file set & contents + +**`rules/motion-main.md`** (entry point / router) + +- Frontmatter: _"Read when working directly with `@wix/motion` โ€” creating WAAPI/CSS/scroll/pointer + animations imperatively, driving scrub scenes, building sequences, or generating CSS. For preset + selection see `@wix/motion-presets`; for declarative trigger wiring see `@wix/interact`."_ +- ToC + **package boundary** decision table (motion vs presets vs interact). +- **Core mental model:** options are discriminated structurally (`keyframeEffect` | `namedEffect` | + `customEffect`) ร— trigger (`undefined` = time-based, `view-progress`, `pointer-move`). Explicit "there + is no `type` field." +- **Function map** table: `getWebAnimation`, `getCSSAnimation`, `getScrubScene`, `getAnimation`, + `prepareAnimation`, `getSequence`, `createAnimationGroups`, `registerEffects`, `getEasing` โ€” with the + _real_ signatures and return types (including `| null`). +- **Easing reference:** the exact `jsEasings`/`cssEasings` key list + `cubic-bezier()`/`linear()` parsing. +- Links to the spoke files below. + +**`rules/waapi.md`** โ€” `getWebAnimation` + `AnimationGroup` + +- Full `AnimationOptions` field tables (time vs scrub) from `src/types.ts`. +- Complete `AnimationGroup` surface incl. `onFinish`/`onAbort`, `finished` (`Promise`), + `playState` aggregation, dispatched `animationend`/`animationcancel` events. +- Gotchas: `play()` start-vs-finish; `iterations: 0` โ‡’ infinite; `reducedMotion` behavior. + +**`rules/scrub-scenes.md`** โ€” `getScrubScene` (the differentiator) + +- Native ViewTimeline vs. polyfill branch; when scenes are returned. +- `ScrubScrollScene` / `ScrubPointerScene` contracts; how to drive `effect(_, progress)`; pointer + `Progress` payload; `axis` on trigger; `centeredToTarget`, transition smoothing, `allowActiveEvent`; + cleanup via `destroy()`. Note `@wix/interact` + `fizban` automate this. + +**`rules/sequences.md`** โ€” `getSequence` / `Sequence` + +- Largely portable from the already-accurate `api/sequence.md` + `api/get-sequence.md`: offset formula + `easing(i/last) * last * offset | 0`, `addGroups`/`removeGroups`, target resolution, `reducedMotion` + context. + +**`rules/custom-effects.md`** โ€” `registerEffects` + authoring + +- `EffectModule` shape `{ web, getNames, style?, prepare? }` and `AnimationData[]` return; the + `customEffect` callback form (and that the `{ ranges }` form is inert in motion alone); + `data-motion-part` sub-target targeting. + +**`rules/css-generation.md`** โ€” `getCSSAnimation` / SSR + +- Descriptor array fields and meanings; `forCSS` forcing `duration: 'auto'`; how the output is injected; + relationship to `@wix/interact`'s `generate()` and FOUC prevention (link, don't duplicate). + +### 9.3 Minimal-set fallback + +If a 6-file set is too much to maintain, collapse to **two** files: `motion-main.md` (everything above, +condensed, the way `presets-main.md` packs a category) + `custom-effects.md` (authoring + scrub driving, +the parts most prone to agent error). Prefer the full set if the package keeps evolving. + +--- + +## 10. Suggested remediation order + +1. **Stop the bleeding (accuracy):** delete `PLAN_DOCS.md` and `categories/`; fix the + `getCSSAnimation`-returns-string error and the `type:` discriminator across `getting-started.md`, + `core-concepts.md`, `api/core-functions.md`, `api/README.md`, `api/types.md`. (Highest impact, since + these are the most-copied snippets.) +2. **Author `rules/motion-main.md`** โ€” gives agents a correct entry point immediately; it can ship + before the human docs are fully rewritten. +3. **Rewrite the human docs** against `README.md` as the reference of truth; remove broken links and the + `playground`/`examples`/`testing` references. +4. **Fill the gaps from ยง6** (scrub driving, custom effects, SSR/CSS, reduced motion) in the new + `guides/` + remaining rule spokes. +5. **Add a drift guard** (optional) so rule/doc signatures are checked against exported types in CI, + mirroring `@wix/interact-validate`'s approach. + +--- + +### Appendix: files referenced as ground truth + +`src/index.ts`, `src/motion.ts`, `src/types.ts`, `src/utils.ts`, `src/easings.ts`, +`src/AnimationGroup.ts`, `src/Sequence.ts`, `src/CustomAnimation.ts`, `src/api/common.ts`, +`src/api/webAnimations.ts`, `src/api/cssAnimations.ts`, `src/api/prepare.ts`, `src/api/registry.ts`, +`package.json`, `README.md`; plus the rule templates +`packages/motion-presets/rules/presets/presets-main.md` and `packages/interact/rules/integration.md`. diff --git a/packages/motion/docs/PLAN_DOCS.md b/packages/motion/docs/PLAN_DOCS.md deleted file mode 100644 index 9db02af7..00000000 --- a/packages/motion/docs/PLAN_DOCS.md +++ /dev/null @@ -1,210 +0,0 @@ -## Context Summary - -### Tools Utilized & Key Discoveries: - -Used `read_file` on `types.ts` (781 lines), animation index files, and implementation examples. Used `list_dir` to map complete library structure. Used `file_search` to find documentation patterns in other packages. Examined simple (`FadeIn.ts`) and complex (`ArcIn.ts`) animation implementations, plus test patterns. - -**Key Findings:** - -- **82+ animation presets** organized in 5 categories -- **Comprehensive TypeScript definitions** with detailed configuration options -- **Consistent API structure** across all animations (`web()`, `style()`, `getNames()`, `prepare()`) -- **Current documentation is minimal** (motion README only has 4 lines) -- **Rich reference pattern exists** in `interact` package (98-line structured docs) - -### Confirmation of User's Problem Statement: - -The motion package contains a sophisticated animation system with extensive presets but **lacks comprehensive documentation**. The current README is essentially empty, while the codebase contains 82+ well-structured animation presets across 5 distinct categories, each with specific configuration options and usage patterns. - -### Key Files, Functions, Types & Structures Involved: - -1. **Main structure**: `packages/motion/src/` -2. **Type definitions**: `types.ts` - Complete type system with detailed interfaces -3. **Five animation categories**: - - **Entrance animations**: 24 presets (ArcIn, BlurIn, BounceIn, etc.) - - **Ongoing animations**: 16 presets (Bounce, Breathe, Cross, etc.) - - **Scroll animations**: 19 presets (ArcScroll, BlurScroll, FadeScroll, etc.) - - **Mouse animations**: 12 presets (AiryMouse, BlobMouse, BlurMouse, etc.) - - **Background scroll animations**: 12 presets (BgCloseUp, BgFade, BgParallax, etc.) -4. **Core API**: `motion.ts`, `AnimationGroup.ts`, utility functions -5. **Test structure**: Comprehensive test files with usage examples - -### Current Data Flow & Observed Patterns: - -- **Animation structure**: Each preset exports `web()`, `style()`, `getNames()`, optional `prepare()` -- **Type hierarchy**: All animations inherit from `BaseDataItemLike` with specific configuration interfaces -- **Configuration system**: Common properties like `direction`, `intensity` with preset-specific options -- **Platform flexibility**: Dual `web()` and `style()` functions for different rendering contexts -- **Playground integration**: Storybook stories demonstrate interactive usage - -### Reference Implementations/Utilities Found: - -- **Documentation pattern**: `interact` package provides excellent structured documentation template -- **Test patterns**: Extensive test suites showing configuration examples and expected outputs -- **Playground demos**: Interactive Storybook implementations showing real usage -- **Type definitions**: Complete TypeScript interfaces for all configuration options - -### Potential Challenges, Risks & Considerations: - -1. **Scale complexity**: 82+ presets require systematic organization to avoid overwhelming users -2. **Technical depth**: Each animation has multiple configuration options requiring clear explanation -3. **Multiple audiences**: Need to serve both quick-reference and deep-dive use cases -4. **Maintenance burden**: Documentation must stay synchronized with ongoing development -5. **Interactive examples**: Static docs may not fully convey animation behavior - need embedded demos -6. **Platform differences**: `web()` vs `style()` usage patterns need clear explanation - ---- - -## Phase 2: Formulate a Plan - -### **Comprehensive Animation Presets Documentation Plan** - -Based on the thorough exploration, I propose creating a complete documentation ecosystem for the motion package's 82+ animation presets, structured for both discovery and deep technical reference. - -#### **Stage 1: Foundation & Structure (Week 1-2)** - -**What:** Create the core documentation architecture and main overview -**Where:** - -- `packages/motion/README.md` (complete rewrite) -- `packages/motion/docs/` (new directory structure) - **Why:** Establish the foundation for all subsequent documentation and provide immediate value - -**Deliverables:** - -1. **Enhanced Main README** - Replace 4-line README with comprehensive overview -2. **Documentation Structure** - Create organized directory hierarchy following `interact` package pattern -3. **Getting Started Guide** - Quick setup and first animation implementation -4. **Core Concepts** - Explain animation categories, configuration patterns, and usage modes - -**Dependencies:** None - can start immediately -**Success Criteria:** Users can understand what the motion package offers and implement their first animation in under 10 minutes - -#### **Stage 2: Category-Level Documentation (Week 3-4)** - -**What:** Document each of the 5 animation categories with overviews and comparison tables -**Where:** `packages/motion/docs/categories/` -**Why:** Help users navigate the large number of presets by understanding categories first - -**Deliverables:** - -1. **Entrance Animations Guide** (24 presets) - - Category overview and use cases - - Comparison table with visual descriptions - - Common configuration patterns -2. **Ongoing Animations Guide** (16 presets) -3. **Scroll Animations Guide** (19 presets) -4. **Mouse Animations Guide** (12 presets) -5. **Background Scroll Animations Guide** (12 presets) - -**Dependencies:** Stage 1 foundation -**Success Criteria:** Users can quickly identify which category and specific animations suit their needs - -#### **Stage 3: Individual Preset Documentation (Week 5-8)** - -**What:** Create detailed documentation for each of the 82+ animation presets -**Where:** `packages/motion/docs/presets/` -**Why:** Provide complete reference for every available animation with examples - -**Deliverables:** - -1. **Standardized preset documentation template** - - Description and visual behavior - - Configuration options with examples - - Code samples (web() and style() usage) - - Common use cases and variations - - Related animations suggestions -2. **Complete preset reference** - All 82+ animations documented using template -3. **Interactive examples** - Leverage existing Storybook integration - -**Dependencies:** Stage 2 categorization -**Success Criteria:** Every animation preset has complete, consistent documentation with working examples - -#### **Stage 4: API & Advanced Usage (Week 9-10)** - -**What:** Document the core APIs, advanced patterns, and integration guides -**Where:** `packages/motion/docs/api/` and `packages/motion/docs/guides/` -**Why:** Support power users and complex integration scenarios - -**Deliverables:** - -1. **API Reference** - - Core functions (`getWebAnimation`, `getScrubScene`, etc.) - - AnimationGroup class - - Type definitions with examples -2. **Advanced Guides** - - Custom animation development - - Performance optimization - - Framework integration patterns - - Testing animation behaviors - -**Dependencies:** Stages 1-3 foundation -**Success Criteria:** Advanced users can extend the system and optimize performance - -#### **Stage 5: Interactive Enhancements & Maintenance (Week 11-12)** - -**What:** Add interactive elements and establish maintenance processes -**Where:** Throughout documentation structure -**Why:** Maximize usability and ensure long-term documentation quality - -**Deliverables:** - -1. **Interactive Elements** - - Embedded playground examples - - Live configuration editors - - Visual animation previews -2. **Maintenance Framework** - - Documentation update workflow - - Automated sync with code changes - - Community contribution guidelines - -**Dependencies:** All previous stages -**Success Criteria:** Documentation provides rich interactive experience and stays current with development - -### **Documentation Architecture:** - -``` -packages/motion/ -โ”œโ”€โ”€ README.md (comprehensive overview) -โ”œโ”€โ”€ docs/ -โ”‚ โ”œโ”€โ”€ getting-started.md -โ”‚ โ”œโ”€โ”€ core-concepts.md -โ”‚ โ”œโ”€โ”€ categories/ -โ”‚ โ”‚ โ”œโ”€โ”€ entrance-animations.md -โ”‚ โ”‚ โ”œโ”€โ”€ ongoing-animations.md -โ”‚ โ”‚ โ”œโ”€โ”€ scroll-animations.md -โ”‚ โ”‚ โ”œโ”€โ”€ mouse-animations.md -โ”‚ โ”‚ โ””โ”€โ”€ background-scroll-animations.md -โ”‚ โ”œโ”€โ”€ presets/ -โ”‚ โ”‚ โ”œโ”€โ”€ entrance/ -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ arc-in.md -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ blur-in.md -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ [22 more files] -โ”‚ โ”‚ โ”œโ”€โ”€ ongoing/ -โ”‚ โ”‚ โ”œโ”€โ”€ scroll/ -โ”‚ โ”‚ โ”œโ”€โ”€ mouse/ -โ”‚ โ”‚ โ””โ”€โ”€ background-scroll/ -โ”‚ โ”œโ”€โ”€ api/ -โ”‚ โ”‚ โ”œโ”€โ”€ core-functions.md -โ”‚ โ”‚ โ”œโ”€โ”€ animation-group.md -โ”‚ โ”‚ โ””โ”€โ”€ types.md -โ”‚ โ”œโ”€โ”€ guides/ -โ”‚ โ”‚ โ”œโ”€โ”€ performance.md -โ”‚ โ”‚ โ”œโ”€โ”€ testing.md -โ”‚ โ”‚ โ””โ”€โ”€ advanced-patterns.md -โ”‚ โ””โ”€โ”€ examples/ -โ”‚ โ”œโ”€โ”€ common-patterns.md -โ”‚ โ””โ”€โ”€ real-world-implementations.md -``` - -### **Check-in Points:** - -- **End of Stage 1:** Review foundation structure and core concepts clarity -- **End of Stage 2:** Validate category organization and navigation effectiveness -- **End of Stage 3:** Sample review of 10 preset documentation pages for consistency -- **End of Stage 4:** API documentation completeness review -- **End of Stage 5:** Full documentation system testing and feedback integration - ---- - -**Switch to agent mode and type execute (or execute stage 1) to begin.** diff --git a/packages/motion/docs/api/README.md b/packages/motion/docs/api/README.md index aba88734..ad9d37b2 100644 --- a/packages/motion/docs/api/README.md +++ b/packages/motion/docs/api/README.md @@ -1,206 +1,55 @@ # API Reference -Complete reference for all Wix Motion functions, types, and classes. - -## Core Functions - -### [Animation Creation](core-functions.md) - -- `getWebAnimation()` - Create Web Animations API instances -- `getScrubScene()` - Generate scroll/pointer-driven scenes -- `getCSSAnimation()` - Generate CSS animation rules -- `prepareAnimation()` - Pre-calculate measurements - -### [Animation Group](animation-group.md) - -- `AnimationGroup` class - Manage multiple related animations -- Control methods and properties -- Event handling and callbacks - -### [Sequence](sequence.md) - -- `Sequence` class - Coordinate multiple AnimationGroups with staggered delay offsets -- `addGroups()` / `removeGroups()` for dynamic group management -- Easing-driven offset calculation - -### [Sequence Creation](get-sequence.md) - -- `getSequence()` - Create a Sequence from target/options pairs -- `createAnimationGroups()` - Build AnimationGroups without a Sequence wrapper - -### [Type Definitions](types.md) - -- Complete TypeScript interfaces -- Animation option types -- Named effect definitions -- Utility types - -## Quick Reference - -### Basic Animation Creation - -```typescript -import { getWebAnimation } from '@wix/motion'; - -const animation = getWebAnimation( - element, // HTMLElement | string | null - animationOptions, // TimeAnimationOptions | ScrubAnimationOptions - trigger?, // TriggerVariant (for scroll/mouse) - options? // Record -); -``` - -### Scroll Scene Creation - -```typescript -import { getScrubScene } from '@wix/motion'; - -const scene = getScrubScene( - element, // HTMLElement | string | null - animationOptions, // ScrubAnimationOptions - trigger, // TriggerVariant - sceneOptions? // Record -); -``` - -### CSS Animation Generation - -```typescript -import { getCSSAnimation } from '@wix/motion'; - -const cssRules = getCSSAnimation( - target, // string | null (element ID) - animationOptions, // AnimationOptions - trigger? // TriggerVariant -); -``` - -### Animation Preparation - -```typescript -import { prepareAnimation } from '@wix/motion'; - -prepareAnimation( - target, // HTMLElement | string | null - animation, // AnimationOptions - callback? // () => void -); -``` - -### Sequence Creation - -```typescript -import { getSequence } from '@wix/motion'; - -const sequence = getSequence( - { offset: 200, offsetEasing: 'quadIn' }, - items.map((el) => ({ - target: el, - options: { name: 'FadeIn', duration: 600 }, - })), -); -sequence.play(); -``` - -## Types Overview - -### Main Interfaces - -```typescript -// Time-based animation options -interface TimeAnimationOptions { - type: 'TimeAnimationOptions'; - namedEffect?: NamedEffect; - keyframeEffect?: MotionKeyframeEffect; - customEffect?: CustomEffect; - duration?: number; - delay?: number; - easing?: string; - iterations?: number; - // ... more properties -} - -// Scroll/mouse-driven animation options -interface ScrubAnimationOptions { - type: 'ScrubAnimationOptions'; - namedEffect?: NamedEffect; - keyframeEffect?: MotionKeyframeEffect; - customEffect?: CustomEffect; - startOffset?: RangeOffset; - endOffset?: RangeOffset; - // ... more properties -} - -// Animation control group -interface AnimationGroup { - animations: Animation[]; - ready: Promise; - play(callback?: () => void): Promise; - pause(): void; - cancel(): void; - progress(p: number): void; - // ... more methods -} - -// Sequence options -interface SequenceOptions { - delay?: number; - offset?: number; - offsetEasing?: string | ((p: number) => number); -} - -// Arguments for building animation groups -interface AnimationGroupArgs { - target: HTMLElement | HTMLElement[] | string | null; - options: AnimationOptions; - context?: Record; -} - -// Indexed group for addGroups() -interface IndexedGroup { - index: number; - group: AnimationGroup; -} -``` - -### Named Effect Types - -```typescript -// Entrance animations -type EntranceAnimation = FadeIn | ArcIn | BounceIn | SlideIn | FlipIn | DropIn | ExpandIn | GlideIn; -// ... all entrance types - -// Ongoing animations -type OngoingAnimation = Pulse | Breathe | Spin | Wiggle | Flash | Bounce | Swing | Poke; -// ... all ongoing types - -// Scroll animations -type ScrollAnimation = - | ParallaxScroll - | FadeScroll - | GrowScroll - | RevealScroll - | TiltScroll - | MoveScroll; -// ... all scroll types - -// Mouse animations -type MouseAnimation = TrackMouse | Tilt3DMouse | ScaleMouse | BlurMouse | SwivelMouse | SpinMouse; -// ... all mouse types - -// Background scroll animations -type BackgroundScrollAnimation = - | BgParallax - | BgZoom - | BgFade - | BgRotate - | BgPan - | BgCloseUp - | BgSkew - | BgPullBack; -// ... all background types -``` - ---- - -Explore each section for detailed documentation and examples. +Index of everything `@wix/motion` exports โ€” functions, classes, and types โ€” with links to the full reference for each. + +## Functions + +| Function | Purpose | Returns | Reference | +| ------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | +| `getWebAnimation()` | Create a WAAPI-backed animation โ€” time-based, scroll-linked, or pointer-driven | `AnimationGroup \| MouseAnimationInstance \| null` | [core-functions.md#getwebanimation](./core-functions.md#getwebanimation) | +| `getCSSAnimation()` | Generate CSS animation descriptors for stylesheet / SSR rendering | `Array<{ target, animation, name, keyframes, ... }>` | [core-functions.md#getcssanimation](./core-functions.md#getcssanimation) | +| `getScrubScene()` | Build scroll-polyfill or pointer-driven scrub scenes | `ScrubScrollScene[] \| ScrubPointerScene \| ScrubPointerScene[] \| null` | [core-functions.md#getscrubscene](./core-functions.md#getscrubscene) | +| `getAnimation()` | Reuse an existing CSS animation if present, else fall back to `getWebAnimation()` | `AnimationGroup \| MouseAnimationInstance \| null` | [core-functions.md#getanimation](./core-functions.md#getanimation) | +| `prepareAnimation()` | Run an effect's `prepare()` hook (measure/mutate via `fastdom`) before animating | `void` | [core-functions.md#prepareanimation](./core-functions.md#prepareanimation) | +| `registerEffects()` | Register named effect modules into the global registry | `void` | [core-functions.md#registereffects](./core-functions.md#registereffects) | +| `getEasing()` | Resolve a named/raw easing to a CSS easing string | `string` | [core-functions.md#geteasing--getjseasing](./core-functions.md#geteasing--getjseasing) | +| `getJsEasing()` | Resolve a named/raw easing to a JS easing function | `((t: number) => number) \| undefined` | [core-functions.md#geteasing--getjseasing](./core-functions.md#geteasing--getjseasing) | +| `getSequence()` | Coordinate multiple `AnimationGroup`s with staggered offsets | `Sequence` | [get-sequence.md#getsequence](./get-sequence.md#getsequence) | +| `createAnimationGroups()` | Build `AnimationGroup`s from target/options pairs without a `Sequence` wrapper | `AnimationGroup[]` | [get-sequence.md#createanimationgroups](./get-sequence.md#createanimationgroups) | + +## Classes + +| Class | Purpose | Reference | +| ---------------- | ------------------------------------------------------------------------------------- | ------------------------------------------ | +| `AnimationGroup` | Controls one or more `Animation`s as a unit โ€” play, pause, reverse, cancel, progress | [animation-group.md](./animation-group.md) | +| `Sequence` | Extends `AnimationGroup` to stagger multiple groups using easing-driven delay offsets | [sequence.md](./sequence.md) | + +## Types + +See [Type Definitions](./types.md) for the full `AnimationOptions`, trigger, and scrub-scene type reference. + +## Guides + +- [Custom Effects](../guides/custom-effects.md) โ€” the `registerEffects()` / `EffectModule` contract, authoring `customEffect` callbacks, and driving scrub scenes. +- [SSR & CSS Generation](../guides/ssr-css.md) โ€” `getCSSAnimation()` descriptors and the FOUC-free rendering contract. +- [Performance](../guides/performance.md) โ€” `fastdom` batching and choosing between the CSS and WAAPI paths. + +## Core mental model + +> Every `AnimationOptions` object is discriminated **structurally** โ€” there is no top-level `type` field. Pick exactly one effect mode: +> +> - **`keyframeEffect: { name, keyframes }`** โ€” inline WAAPI/CSS keyframes, zero registration. +> - **`customEffect: (element, progress) => void`** โ€” a per-frame JS callback; the only programmatic mode. Called with `progress: null` on cancel. +> - **`namedEffect: { type, ...params }`** โ€” references an effect registered via `registerEffects()`. `type` here is the _preset name_, not a discriminator on the options object. +> +> Combine with a **trigger** (the 3rd argument to `getWebAnimation()` / `getScrubScene()`): +> +> - trigger omitted โ†’ time-based (`duration` / `easing` / `iterations`). +> - `{ trigger: 'view-progress' }` โ†’ scroll-driven. +> - `{ trigger: 'pointer-move' }` โ†’ pointer-driven. + +## See also + +- [Getting Started](../getting-started.md) โ€” install and build your first animation. +- [Core Concepts](../core-concepts.md) โ€” effect modes, triggers, and the mental model in depth. +- [Package README](../../README.md) โ€” overview and quick start. diff --git a/packages/motion/docs/api/animation-group.md b/packages/motion/docs/api/animation-group.md index 13ad1f2f..df53cb70 100644 --- a/packages/motion/docs/api/animation-group.md +++ b/packages/motion/docs/api/animation-group.md @@ -1,48 +1,43 @@ # AnimationGroup -The `AnimationGroup` class manages multiple related animations as a cohesive unit, providing centralized control over timing, playback, and state management. +The `AnimationGroup` class wraps a set of native Web Animations API `Animation` instances and lets you control them as one unit โ€” play, pause, reverse, scrub, and observe completion together. It's the return type of `getWebAnimation()` and `getAnimation()` for time-based and scroll-driven (non-pointer) animations. ## Overview -`AnimationGroup` is the primary return type from `getWebAnimation()` for time-based animations and scroll-driven animations. It orchestrates multiple `Animation` instances, allowing you to control complex animation sequences with simple method calls. +`AnimationGroup` is a plain wrapper: it doesn't create animations itself, it just holds an array of `Animation` objects and fans out control calls to each of them. `Sequence` (see [`sequence.md`](./sequence.md)) extends `AnimationGroup` to additionally stagger the animations' delays. ### Key Features -- **Unified Control** - Play, pause, and control multiple animations together -- **Progress Tracking** - Monitor overall animation progress -- **State Management** - Access current playback state across all animations -- **Event Handling** - React to animation completion and state changes -- **Performance Optimized** - Efficient management of multiple animation instances +- **Unified control** โ€” `play`, `pause`, `reverse`, `cancel`, `setPlaybackRate`, and `progress` operate on every animation in the group at once. +- **Completion events** โ€” `onFinish` / `onAbort` dispatch DOM events on the target element and invoke a callback. +- **Progress inspection** โ€” `getProgress()` / `getTimingOptions()` read back computed timing from the group's longest-running animation. ## Class Definition ```typescript class AnimationGroup { - animations: (Animation & { - start?: RangeOffset; - end?: RangeOffset; - })[]; + animations: (Animation & { start?: RangeOffset; end?: RangeOffset })[]; options?: AnimationGroupOptions; ready: Promise; + isCSS: boolean; + longestAnimation: Animation; constructor(animations: Animation[], options?: AnimationGroupOptions); - // Control methods - async play(callback?: () => void): Promise; + play(callback?: () => void): Promise; pause(): void; - async reverse(callback?: () => void): Promise; - cancel(): void; - - // Progress and state - getProgress(): number; + reverse(callback?: () => void): Promise; progress(p: number): void; + cancel(): void; setPlaybackRate(rate: number): void; + getProgress(): number; + onFinish(callback: () => void): Promise; + onAbort(callback: () => void): Promise; + hasAnimationName(name: string): boolean; + hasAnimationId(id: string): boolean; + getTimingOptions(): { delay: number; duration: number; iterations: number }[]; - // Event handling - async onFinish(callback: () => void): Promise; - - // State properties - get finished(): Promise; + get finished(): Promise; get playState(): AnimationPlayState; } ``` @@ -52,764 +47,261 @@ class AnimationGroup { ### Signature ```typescript -constructor( - animations: Animation[], - options?: AnimationGroupOptions -) +constructor(animations: Animation[], options?: AnimationGroupOptions) ``` ### Parameters #### `animations` (required) -Array of Web Animations API `Animation` instances to manage. +Array of native Web Animations API `Animation` instances (e.g. from `element.animate(...)`, or `CSSAnimation`s already running on the element). #### `options` (optional) -Configuration options for the group: - ```typescript -interface AnimationGroupOptions { - trigger?: Partial; - startOffsetAdd?: string; - endOffsetAdd?: string; +type AnimationGroupOptions = AnimationOptions & { + trigger?: Partial | undefined; + startOffsetAdd?: string | undefined; + endOffsetAdd?: string | undefined; measured?: Promise; -} +}; ``` -## Properties +`options.measured`, if provided, becomes the group's `ready` promise (see below). -### `animations` +### Example ```typescript -animations: (Animation & { - start?: RangeOffset; - end?: RangeOffset; -})[] -``` +import { AnimationGroup } from '@wix/motion'; -Array of managed animations with optional scroll range information. +const el = document.querySelector('#box') as HTMLElement; -```typescript -const group = getWebAnimation(element, options); -console.log(`Managing ${group.animations.length} animations`); +const fade = el.animate({ opacity: [0, 1] }, { duration: 600, fill: 'both' }); +const move = el.animate( + { transform: ['translateY(20px)', 'translateY(0)'] }, + { duration: 600, fill: 'both' }, +); -// Access individual animations -group.animations.forEach((animation, index) => { - console.log(`Animation ${index} state:`, animation.playState); -}); +const group = new AnimationGroup([fade, move]); ``` -### `options` +## Properties + +### `animations` ```typescript -options?: AnimationGroupOptions +animations: (Animation & { start?: RangeOffset; end?: RangeOffset })[] ``` -Configuration options passed during construction. +The wrapped animations, in the order passed to the constructor. For scroll-driven animations, `start` / `end` carry the resolved [`RangeOffset`](./types.md#rangeoffset) for that animation. + +### `options` ```typescript -const group = getWebAnimation(element, options); -if (group.options?.trigger) { - console.log('Trigger type:', group.options.trigger); -} +options?: AnimationGroupOptions ``` +The options object passed to the constructor, stored as-is. `options.effectId` (inherited from `AnimationExtraOptions`) is used as the `detail.effectId` on the `animationend` event dispatched by `onFinish` (falling back to the first animation's `id`). + ### `ready` ```typescript ready: Promise; ``` -Promise that resolves when all animations are prepared and ready to play. +Resolves once the group's targets have been measured/mutated. Set from `options?.measured`, or `Promise.resolve()` if no `measured` promise was supplied. `play()` and `reverse()` both `await ready` before starting playback. + +### `isCSS` ```typescript -const group = getWebAnimation(element, options); +isCSS: boolean; +``` -// Wait for animations to be ready -await group.ready; -console.log('All animations are prepared'); +`true` if `animations[0]` is a `CSSAnimation` (i.e. the group wraps an existing CSS `animation` rather than a WAAPI `KeyframeEffect`). Used internally to decide whether `onFinish`/`onAbort` can safely read `effect.target` off a `KeyframeEffect`. -// Now safe to play -await group.play(); -``` +### `longestAnimation` -## Control Methods +```typescript +longestAnimation: Animation; +``` -### `play()` +The animation in `animations` with the greatest computed `effect.getComputedTiming().endTime`, computed once in the constructor. `getProgress()` reads from this animation. -Starts playback of all animations in the group. +## Methods -#### Signature +### `play()` ```typescript async play(callback?: () => void): Promise ``` -#### Parameters - -- **`callback`** (optional) - Function called when playback starts - -#### Returns - -Promise that resolves when all animations begin playing. +Awaits `ready`, calls `.play()` on every animation in the group, then awaits each animation's `.ready`, then invokes `callback` (if given). -#### Examples +> **`play()` resolves once playback has STARTED, not once it has finished.** It does not wait for the animations to complete. To observe completion, use [`onFinish(cb)`](#onfinish) or `await group.finished` โ€” never `await group.play()`. ```typescript -// Basic playback -const animation = getWebAnimation('#element', { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 1000, -}); - -await animation.play(); -console.log('Animation started'); -``` +import { getWebAnimation } from '@wix/motion'; -```typescript -// With callback -await animation.play(() => { - console.log('Animation playback initiated'); - // Add loading states, analytics, etc. +const group = getWebAnimation('#hero', { + keyframeEffect: { name: 'fade-in', keyframes: [{ opacity: 0 }, { opacity: 1 }] }, + duration: 800, + fill: 'both', }); -``` -```typescript -// Sequential animations -const intro = getWebAnimation('#intro', introOptions); -const main = getWebAnimation('#main', mainOptions); - -await intro.play(); -await main.play(); +await group?.play(); +console.log('playback has started (not finished)'); ``` ### `pause()` -Pauses all animations in the group. - -#### Signature - ```typescript pause(): void ``` -#### Examples - -```typescript -const animation = getWebAnimation(element, options); - -// Start animation -await animation.play(); - -// Pause after 2 seconds -setTimeout(() => { - animation.pause(); - console.log('Animation paused'); -}, 2000); -``` - -```typescript -// Pause on user interaction -document.addEventListener('visibilitychange', () => { - if (document.hidden) { - animation.pause(); - } else { - animation.play(); - } -}); -``` +Calls `.pause()` on every animation in the group. ### `reverse()` -Reverses the direction of all animations and starts playback. - -#### Signature - ```typescript async reverse(callback?: () => void): Promise ``` -#### Parameters - -- **`callback`** (optional) - Function called when reverse playback starts - -#### Returns - -Promise that resolves when reverse playback begins. +Same shape as `play()`: awaits `ready`, calls `.reverse()` on every animation, awaits each animation's `.ready`, then invokes `callback`. Also resolves once reverse playback has **started**, not finished. -#### Examples +### `progress()` ```typescript -const animation = getWebAnimation('#modal', { - type: 'TimeAnimationOptions', - namedEffect: { type: 'DropIn' }, - duration: 400, -}); - -// Show modal -await animation.play(); - -// Hide modal (reverse animation) -setTimeout(async () => { - await animation.reverse(); - console.log('Modal hidden'); -}, 3000); +progress(p: number): void ``` -```typescript -// Hover effect with reverse -const button = document.querySelector('#interactive-button'); -const hoverAnimation = getWebAnimation(button, hoverOptions); - -button.addEventListener('mouseenter', () => { - hoverAnimation.play(); -}); - -button.addEventListener('mouseleave', () => { - hoverAnimation.reverse(); -}); -``` +Scrubs every animation in the group to normalized progress `p` (`0`โ€“`1`) by directly setting each animation's `currentTime` based on its own `delay`, `duration`, and `iterations`. ### `cancel()` -Cancels all animations and resets elements to their initial state. - -#### Signature - ```typescript cancel(): void ``` -#### Examples - -```typescript -const animation = getWebAnimation(element, options); - -// Start animation -await animation.play(); +Calls `.cancel()` on every animation in the group, resetting elements to their pre-animation state. -// Cancel if needed -if (shouldCancel) { - animation.cancel(); - console.log('Animation cancelled and reset'); -} -``` +### `setPlaybackRate()` ```typescript -// Component cleanup -class AnimatedComponent { - private animation: AnimationGroup; - - constructor(element: HTMLElement) { - this.animation = getWebAnimation(element, options); - } - - destroy() { - // Always cancel animations on cleanup - this.animation.cancel(); - } -} +setPlaybackRate(rate: number): void ``` -## Progress and State Methods +Sets `playbackRate` on every animation in the group (`1` = normal speed, `2` = double speed, `0.5` = half speed). ### `getProgress()` -Returns the current progress of the animation group as a value between 0 and 1. - -#### Signature - ```typescript getProgress(): number ``` -#### Returns - -Number between 0 (start) and 1 (complete). - -#### Examples - -```typescript -const animation = getWebAnimation(element, options); -await animation.play(); - -// Monitor progress -const progressInterval = setInterval(() => { - const progress = animation.getProgress(); - console.log(`Animation ${Math.round(progress * 100)}% complete`); - - if (progress >= 1) { - clearInterval(progressInterval); - } -}, 100); -``` - -```typescript -// Progress-based UI updates -const progressBar = document.querySelector('#progress-bar'); -const animation = getWebAnimation('#content', options); - -// Update progress bar during animation -const updateProgress = () => { - const progress = animation.getProgress(); - progressBar.style.width = `${progress * 100}%`; - - if (progress < 1) { - requestAnimationFrame(updateProgress); - } -}; - -animation.play(); -updateProgress(); -``` - -### `progress()` - -Manually sets the progress of all animations to a specific value. +Returns `longestAnimation.effect.getComputedTiming().progress`, or `0` if unavailable. -#### Signature +### `onFinish()` ```typescript -progress(p: number): void +async onFinish(callback: () => void): Promise ``` -#### Parameters +Awaits `Promise.all(animations.map(a => a.finished))`. On success: -- **`p`** - Progress value between 0 and 1 +- If the group is **not** CSS-backed (`!isCSS`) and the first animation's effect has a `target`, dispatches a `CustomEvent('animationend', { detail: { effectId } })` on that target (`effectId` is `options?.effectId ?? animations[0].id`). +- Then calls `callback`. -#### Examples +If any animation's `finished` promise rejects (the animation was interrupted), `onFinish` logs a warning and does **not** call `callback`. ```typescript -const animation = getWebAnimation(element, options); - -// Jump to 50% progress -animation.progress(0.5); -``` - -```typescript -// Create a scrub controller -const scrubSlider = document.querySelector('#scrub-slider'); -const animation = getWebAnimation('#animated-element', options); - -scrubSlider.addEventListener('input', (e) => { - const progress = e.target.value / 100; // Slider value 0-100 - animation.progress(progress); +await group.onFinish(() => { + console.log('every animation in the group finished'); }); ``` -```typescript -// Scroll-based manual control -window.addEventListener('scroll', () => { - const scrollProgress = Math.min( - window.scrollY / (document.body.scrollHeight - window.innerHeight), - 1, - ); - - animation.progress(scrollProgress); -}); -``` - -### `setPlaybackRate()` - -Changes the playback speed of all animations. - -#### Signature +### `onAbort()` ```typescript -setPlaybackRate(rate: number): void +async onAbort(callback: () => void): Promise ``` -#### Parameters - -- **`rate`** - Playback rate multiplier (1 = normal, 2 = double speed, 0.5 = half speed) - -#### Examples - -```typescript -const animation = getWebAnimation(element, options); - -// Start at normal speed -await animation.play(); - -// Speed up animation -animation.setPlaybackRate(2); - -// Slow down animation -animation.setPlaybackRate(0.5); - -// Pause (alternative to pause()) -animation.setPlaybackRate(0); -``` +Awaits `Promise.all(animations.map(a => a.finished))`. If that rejects with an error whose `name` is `'AbortError'` (i.e. the animation was cancelled), and the group is not CSS-backed, dispatches an `Event('animationcancel')` on the first animation's target and calls `callback`. Any other rejection is swallowed without calling `callback`. ```typescript -// Dynamic speed control -const speedControl = document.querySelector('#speed-control'); -const animation = getWebAnimation('#element', options); - -speedControl.addEventListener('change', (e) => { - const speed = parseFloat(e.target.value); - animation.setPlaybackRate(speed); +await group.onAbort(() => { + console.log('animation was cancelled'); }); -await animation.play(); +group.cancel(); // triggers the AbortError path above ``` -## Event Handling - -### `onFinish()` - -Registers a callback to be called when all animations complete. - -#### Signature +### `hasAnimationName()` ```typescript -async onFinish(callback: () => void): Promise +hasAnimationName(name: string): boolean ``` -#### Parameters - -- **`callback`** - Function to call when animations finish +`true` if any animation in the group is a `CSSAnimation` whose `animationName` matches `name`. -#### Returns - -Promise that resolves when the callback is registered. - -#### Examples +### `hasAnimationId()` ```typescript -const animation = getWebAnimation('#element', options); - -// Register completion callback -await animation.onFinish(() => { - console.log('Animation completed!'); - // Perform cleanup, trigger next animation, etc. -}); - -await animation.play(); +hasAnimationId(id: string): boolean ``` -```typescript -// Chain animations -const intro = getWebAnimation('#intro', introOptions); -const main = getWebAnimation('#main', mainOptions); +`true` if any animation in the group has `id === id`. -await intro.onFinish(async () => { - console.log('Intro finished, starting main animation'); - await main.play(); -}); - -await intro.play(); -``` +### `getTimingOptions()` ```typescript -// Modal close cleanup -const modalAnimation = getWebAnimation('#modal', exitOptions); - -await modalAnimation.onFinish(() => { - // Remove modal from DOM after animation - document.querySelector('#modal').remove(); -}); - -await modalAnimation.play(); +getTimingOptions(): { delay: number; duration: number; iterations: number }[] ``` -## State Properties - -### `finished` +Maps every animation to its effective `{ delay, duration, iterations }`, read from `effect.getTiming()` (defaulting `delay` to `0`, `duration` to `0` if not numeric, `iterations` to `1`). -Promise that resolves when all animations complete. +## Getters -#### Signature +### `finished` ```typescript -get finished(): Promise +get finished(): Promise ``` -#### Returns - -Promise that resolves with the last animation to complete. - -#### Examples +`Promise.all(animations.map(a => a.finished))` โ€” **resolves to an array of `Animation`, not a single `Animation`.** This is the primary way to `await` full completion of a group: ```typescript -const animation = getWebAnimation(element, options); -await animation.play(); - -// Wait for completion -await animation.finished; -console.log('All animations completed'); -``` - -```typescript -// Handle completion with error handling -const animation = getWebAnimation(element, options); - -try { - await animation.play(); - await animation.finished; - console.log('Animation completed successfully'); -} catch (error) { - console.log('Animation was cancelled or failed'); -} +await group.play(); +await group.finished; // resolves once every animation in the group has finished ``` ### `playState` -Current playback state of the animation group. - -#### Signature - ```typescript get playState(): AnimationPlayState ``` -#### Returns +`'running'` if **any** animation in the group is currently running; otherwise, the `playState` of `animations[0]`. This means a group can report `'running'` even while some of its animations have already finished, and a group with zero running animations reports whatever state the _first_ animation happens to be in โ€” it is not a strict aggregate of all animations. -- `'idle'` - Not started -- `'running'` - Currently playing -- `'paused'` - Paused -- `'finished'` - Completed +## How to observe completion -#### Examples +Because `play()` and `reverse()` resolve as soon as playback **starts**, don't await them to know when an animation is done. Use one of: ```typescript -const animation = getWebAnimation(element, options); - -console.log('Initial state:', animation.playState); // 'idle' - -await animation.play(); -console.log('After play:', animation.playState); // 'running' - -animation.pause(); -console.log('After pause:', animation.playState); // 'paused' -``` - -```typescript -// State-based UI updates -const playButton = document.querySelector('#play-button'); -const animation = getWebAnimation('#element', options); - -function updateUI() { - const state = animation.playState; - - switch (state) { - case 'idle': - playButton.textContent = 'Play'; - break; - case 'running': - playButton.textContent = 'Pause'; - break; - case 'paused': - playButton.textContent = 'Resume'; - break; - case 'finished': - playButton.textContent = 'Replay'; - break; - } -} - -// Update UI when state changes -setInterval(updateUI, 100); -``` - -## Advanced Usage Patterns - -### Animation Sequencing - -```typescript -class AnimationSequence { - private animations: AnimationGroup[] = []; - private currentIndex = 0; - - add(animation: AnimationGroup) { - this.animations.push(animation); - return this; - } - - async play() { - for (const animation of this.animations) { - await animation.play(); - await animation.finished; - } - } - - async playParallel() { - const promises = this.animations.map(async (animation) => { - await animation.play(); - return animation.finished; - }); - - await Promise.all(promises); - } - - pause() { - this.animations.forEach((animation) => animation.pause()); - } - - cancel() { - this.animations.forEach((animation) => animation.cancel()); - } -} - -// Usage -const sequence = new AnimationSequence() - .add(getWebAnimation('#intro', introOptions)) - .add(getWebAnimation('#main', mainOptions)) - .add(getWebAnimation('#outro', outroOptions)); - -await sequence.play(); -``` - -### Progress Synchronization - -```typescript -class SynchronizedAnimations { - private groups: AnimationGroup[] = []; - - add(group: AnimationGroup) { - this.groups.push(group); - return this; - } - - async play() { - // Start all animations - await Promise.all(this.groups.map((group) => group.play())); - } - - syncProgress(progress: number) { - // Set all animations to same progress - this.groups.forEach((group) => { - group.progress(progress); - }); - } - - setSpeed(rate: number) { - this.groups.forEach((group) => { - group.setPlaybackRate(rate); - }); - } - - getAverageProgress(): number { - const total = this.groups.reduce((sum, group) => sum + group.getProgress(), 0); - return total / this.groups.length; - } -} -``` - -### State Machine Integration +// Callback style +await group.onFinish(() => { + // all animations in the group have finished +}); -```typescript -type AnimationState = 'idle' | 'entering' | 'active' | 'exiting'; - -class StatefulComponent { - private state: AnimationState = 'idle'; - private enterAnimation: AnimationGroup; - private exitAnimation: AnimationGroup; - - constructor(element: HTMLElement) { - this.enterAnimation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 300, - }); - - this.exitAnimation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, // Will be reversed - duration: 300, - }); - - this.setupAnimationHandlers(); - } - - private async setupAnimationHandlers() { - await this.enterAnimation.onFinish(() => { - this.state = 'active'; - this.onEnterComplete(); - }); - - await this.exitAnimation.onFinish(() => { - this.state = 'idle'; - this.onExitComplete(); - }); - } - - async show() { - if (this.state !== 'idle') return; - - this.state = 'entering'; - await this.enterAnimation.play(); - } - - async hide() { - if (this.state !== 'active') return; - - this.state = 'exiting'; - await this.exitAnimation.reverse(); - } - - private onEnterComplete() { - console.log('Component fully visible'); - } - - private onExitComplete() { - console.log('Component fully hidden'); - } -} +// Promise style +await group.play(); +await group.finished; ``` -## Performance Optimization - -### Efficient State Monitoring +## See also -```typescript -class OptimizedAnimationMonitor { - private rafId: number | null = null; - private callbacks: ((progress: number) => void)[] = []; - - constructor(private group: AnimationGroup) {} - - addProgressCallback(callback: (progress: number) => void) { - this.callbacks.push(callback); - this.startMonitoring(); - } - - private startMonitoring() { - if (this.rafId) return; - - const monitor = () => { - const progress = this.group.getProgress(); - - this.callbacks.forEach((callback) => { - try { - callback(progress); - } catch (error) { - console.warn('Progress callback error:', error); - } - }); - - if (progress < 1 && this.group.playState === 'running') { - this.rafId = requestAnimationFrame(monitor); - } else { - this.rafId = null; - } - }; - - this.rafId = requestAnimationFrame(monitor); - } - - destroy() { - if (this.rafId) { - cancelAnimationFrame(this.rafId); - this.rafId = null; - } - this.callbacks = []; - } -} -``` +- [`Sequence`](./sequence.md) โ€” an `AnimationGroup` subclass that coordinates multiple groups with staggered offsets. +- [`Types`](./types.md) โ€” `AnimationOptions`, `RangeOffset`, `TriggerVariant`, and the other types referenced above. --- -**Next**: Explore [Type Definitions](types.md) for complete TypeScript interfaces, or return to [Core Functions](core-functions.md) for animation creation methods. +Return to [API Reference](./README.md). diff --git a/packages/motion/docs/api/core-functions.md b/packages/motion/docs/api/core-functions.md index 896f3489..7806f645 100644 --- a/packages/motion/docs/api/core-functions.md +++ b/packages/motion/docs/api/core-functions.md @@ -1,401 +1,275 @@ # Core Functions -Complete reference for Wix Motion's primary animation creation and management functions. +Reference for the functions Wix Motion exports to create and drive animations directly โ€” WAAPI, CSS, scroll-driven, and pointer-driven. -## Overview +> **Gotchas** +> +> - There is **no top-level `type` field** on the options object. `AnimationOptions` is discriminated **structurally** by the presence of `keyframeEffect` / `namedEffect` / `customEffect` (`namedEffect.type` is valid โ€” that's the registered preset name). +> - `getWebAnimation()`, `getScrubScene()`, and `getAnimation()` can all return `null`. Don't type a const as `AnimationGroup` โ€” check the result before calling methods on it. +> - `getWebAnimation()` takes a **single** `AnimationOptions` object, never an array. To coordinate multiple elements/effects, use `getSequence()`. +> - `getCSSAnimation()` returns an **array of descriptor objects**, not a string. +> - `startOffset` / `endOffset` live on the animation **options**, not the trigger. Pointer `axis` lives on the **trigger**, not the effect. +> - `iterations: 0` means infinite (not just `Infinity`, though that also works). +> - Named easings are limited to the sets listed under [`getEasing` / `getJsEasing`](#geteasing--getjseasing) below โ€” `cubic-bezier(...)` is hyphenated, not `cubicBezier(...)`. +> +> Looking for `getSequence()` or `createAnimationGroups()`? They're documented in [Sequence Creation](./get-sequence.md) rather than duplicated here. -Wix Motion provides four core functions for creating and managing animations: +## getWebAnimation -- **`getWebAnimation()`** - Create Web Animations API instances -- **`getScrubScene()`** - Generate scroll/pointer-driven scenes -- **`getCSSAnimation()`** - Generate CSS animation rules -- **`prepareAnimation()`** - Pre-calculate measurements where needed for CSS Animations - -## getWebAnimation() - -Creates Web Animations API instances for time-based and scrub-based animations. +Creates a WAAPI-backed animation for a single element โ€” time-based, scroll-linked (native `ViewTimeline`), or pointer-driven. ### Signature ```typescript function getWebAnimation( target: HTMLElement | string | null, - animationOptions: TimeAnimationOptions | ScrubAnimationOptions, + animationOptions: AnimationOptions, trigger?: Partial & { element?: HTMLElement }, options?: Record, ownerDocument?: Document, -): AnimationGroup | MouseAnimationInstance; +): AnimationGroup | MouseAnimationInstance | null; ``` ### Parameters -#### `target` (required) - -The element to animate. Can be: - -- **HTMLElement** - Direct element reference -- **string** - Element ID or CSS selector -- **null** - For measurement-only operations +| Parameter | Type | Description | +| ------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `target` | `HTMLElement \| string \| null` | Element to animate. A `string` is resolved as an element id or CSS selector. | +| `animationOptions` | `AnimationOptions` | A single animation configuration โ€” never an array. Discriminated structurally by `keyframeEffect` / `namedEffect` / `customEffect`; see [Type Definitions](./types.md). | +| `trigger` | `Partial & { element?: HTMLElement }` | Optional. Omitted (or without `trigger.trigger`) produces a time-based animation. `{ trigger: 'view-progress' }` links to a scroll `ViewTimeline`. `{ trigger: 'pointer-move', axis?: 'x' \| 'y' }` drives on pointer movement โ€” `axis` picks which pointer axis feeds a `keyframeEffect`. | +| `options` | `Record` | Optional. The engine reads `{ reducedMotion }` from this bag. `effectId` is **not** read here โ€” set `animationOptions.effectId` instead. | +| `ownerDocument` | `Document` | Optional. Document context; defaults to `document`. | -```typescript -// Direct element reference -const element = document.getElementById('myElement'); -getWebAnimation(element, options); - -// Element ID -getWebAnimation('myElement', options); +### Returns -// CSS selector -getWebAnimation('.my-class', options); -``` +`AnimationGroup | MouseAnimationInstance | null` -#### `animationOptions` (required) +- Returns `null` when no effect data can be generated โ€” e.g. an unregistered `namedEffect`, reduced motion dropping a multi-iteration animation, or a pointer factory that couldn't be built. +- Returns `MouseAnimationInstance` only on the `pointer-move` + non-`keyframeEffect` path (i.e. a `namedEffect`/`customEffect` driven by the pointer); every other path returns an `AnimationGroup`. +- With `{ trigger: 'view-progress' }` and `window.ViewTimeline` present, the animation is linked to a native `ViewTimeline` (`duration: 'auto'`) and plays automatically. +- With `view-progress` but no `window.ViewTimeline`, the animation is created with a scrubbable `duration: 99.99ms` / `delay: 0.01ms`, meant to be driven via [`getScrubScene()`](#getscrubscene). -Animation configuration object. Must be either `TimeAnimationOptions` for time-based animations or `ScrubAnimationOptions` for scrub-based animations. +### Example ```typescript -// Time-based animation -const timeOptions: TimeAnimationOptions = { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 1000, - easing: 'easeOut', -}; - -// Scrub-based animation -const scrubOptions: ScrubAnimationOptions = { - type: 'ScrubAnimationOptions', - namedEffect: { type: 'ParallaxScroll', speed: 0.5 }, - startOffset: { name: 'cover', offset: { value: 0, unit: 'percentage' } }, - endOffset: { name: 'cover', offset: { value: 100, unit: 'percentage' } }, -}; -``` - -#### `trigger` (optional) - -Trigger configuration for scrub-based animations: +import { getWebAnimation } from '@wix/motion'; -```typescript -// Scroll trigger -{ - trigger: 'view-progress', - element: containerElement -} +const group = getWebAnimation(document.getElementById('hero'), { + keyframeEffect: { + name: 'fade-up', + keyframes: [ + { opacity: 0, transform: 'translateY(20px)' }, + { opacity: 1, transform: 'translateY(0)' }, + ], + }, + duration: 600, + easing: 'ease-out', +}); -// Mouse trigger -{ - trigger: 'pointer-move', - element: containerElement -} +group?.play(); ``` -#### `options` (optional) +## getCSSAnimation -Additional configuration options: +Generates CSS animation descriptors for stylesheet-based rendering โ€” the SSR / FOUC-free path. + +### Signature ```typescript -{ - effectId: 'custom-effect-id', - measurementCallback: () => console.log('Measured!') -} +function getCSSAnimation( + target: string | null, + animationOptions: AnimationOptions, + trigger?: TriggerVariant, +): Array<{ + target: string; + animation: string; + composition?: CompositeOperation; + custom?: Record; + name: string; + keyframes: Record[]; + id: string | undefined; + animationTimeline: string; + animationRange: string; +}>; ``` -#### `ownerDocument` (optional) - -Document context for the animation (defaults to `document`). +### Parameters -### Return Value +| Parameter | Type | Description | +| ------------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `target` | `string \| null` | Element id or CSS selector. CSS rules target selectors, so โ€” unlike `getWebAnimation` โ€” an `HTMLElement` reference is not accepted here. | +| `animationOptions` | `AnimationOptions` | Same shape as `getWebAnimation`. | +| `trigger` | `TriggerVariant` | Optional. `view-progress` animations always resolve to `duration: 'auto'` through this function, regardless of runtime `ViewTimeline` support (the SSR-safe `forCSS` path). | -Returns either: +### Returns -- **`AnimationGroup`** - For time-based animations and scroll animations -- **`MouseAnimationInstance`** - For mouse-driven animations +**An array of descriptor objects โ€” never a string.** Each entry describes one `@keyframes` block plus the `animation` shorthand that applies it: -### Examples +| Field | Type | Description | +| ------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | +| `target` | `string` | Selector for the animated element or sub-part, e.g. `"#hero"` or `"#hero[data-motion-part~='icon']"`; `""` if no target resolved. | +| `animation` | `string` | The CSS `animation` shorthand value. **Paused by default** โ€” toggle `animation-play-state` or add a class to start it. | +| `composition` | `CompositeOperation \| undefined` | Composite operation, if set. | +| `custom` | `Record \| undefined` | Custom property values referenced by the keyframes. | +| `name` | `string` | The `@keyframes` name. | +| `keyframes` | `Record[]` | Ordered keyframe declarations, rendered as `@keyframes` steps. | +| `id` | `string \| undefined` | Effect id, if provided. | +| `animationTimeline` | `string` | `` `--${trigger.id}` `` for `view-progress` triggers, else `""`. | +| `animationRange` | `string` | e.g. `"cover 0% cover 100%"` for `view-progress` triggers, else `""`. | -#### Basic Entrance Animation +### Example ```typescript -import { getWebAnimation } from '@wix/motion'; +import { getCSSAnimation } from '@wix/motion'; -const animation = getWebAnimation('#hero-title', { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'FadeIn', +const descriptors = getCSSAnimation('hero', { + keyframeEffect: { + name: 'fade-up', + keyframes: [ + { opacity: 0, transform: 'translateY(20px)' }, + { opacity: 1, transform: 'translateY(0)' }, + ], }, - duration: 800, - easing: 'easeOut', + duration: 600, + easing: 'ease-out', }); +// descriptors: Array<{ target, animation, name, keyframes, ... }> -// Play the animation -await animation.play(); -``` - -#### Scroll-Driven Animation - -```typescript -const element = document.querySelector('#parallax-element'); -const scrollAnimation = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 0.3, - }, - }, - { - trigger: 'view-progress', - element, - }, -); - -// Animation automatically responds to scroll -``` - -#### Mouse Interaction - -```typescript -const mouseAnimation = getWebAnimation( - '#interactive-card', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 15, - perspective: 800, - }, - }, - { - trigger: 'pointer-move', - element: document.querySelector('#card-container'), - }, -); +const sheet = new CSSStyleSheet(); -// Animation responds to mouse movement -``` +descriptors.forEach(({ target, animation, name, keyframes }) => { + const steps = keyframes + .map((frame, i) => { + const percent = (i / (keyframes.length - 1)) * 100; + const decls = Object.entries(frame) + .map(([prop, value]) => `${prop}: ${value};`) + .join(' '); + return `${percent}% { ${decls} }`; + }) + .join(' '); + + sheet.insertRule(`@keyframes ${name} { ${steps} }`); + sheet.insertRule(`${target || '#hero'} { animation: ${animation}; }`); +}); -#### Complex Multi-Effect Animation +document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; -```typescript -const multiAnimation = getWebAnimation( - '#complex-element', - { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'bottom', - }, - duration: 1200, - delay: 300, - easing: 'backOut', - }, - undefined, - { - effectId: 'hero-entrance', - }, -); +// Or render to a - -// OR render to a style tag on server-side using a framework (e.g. React) - +group?.play(); ``` -## prepareAnimation() +## prepareAnimation -Pre-calculates measurements and prepares elements for using CSS Animations where additionl measurements are necessary. +Runs an effect's optional `prepare(options, domApi)` hook โ€” measure/mutate via `fastdom` โ€” before an animation plays. ### Signature @@ -409,218 +283,124 @@ function prepareAnimation( ### Parameters -#### `target` (required) - -Element to prepare (same as `getWebAnimation`). - -#### `animation` (required) - -Animation configuration to prepare for. - -#### `callback` (optional) +| Parameter | Type | Description | +| ----------- | ------------------------------- | ---------------------------------------------------------------------- | +| `target` | `HTMLElement \| string \| null` | Element to prepare (same resolution as `getWebAnimation`). | +| `animation` | `AnimationOptions` | The animation configuration to prepare for. | +| `callback` | `() => void` | Optional. Called inside a `fastdom.mutate` once preparation completes. | -Function called when preparation is complete. +### Returns -### Examples +`void` -#### Basic Preparation +### Example ```typescript -import { prepareAnimation, getElementCSSAnimation } from '@wix/motion'; +import { prepareAnimation } from '@wix/motion'; -// Prepare element before animating prepareAnimation( - '#complex-element', + document.getElementById('hero'), { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'bottom', + keyframeEffect: { + name: 'fade-up', + keyframes: [{ opacity: 0 }, { opacity: 1 }], }, }, () => { - console.log('Element prepared for animation'); - - // Now create the animation - const animation = getElementCSSAnimation('#complex-element', animationOptions); - animation.play(); + console.log('Prepared โ€” safe to play the CSS animation now'); }, ); ``` -#### Batch Preparation for Multiple Elements +## registerEffects + +Merges effect modules into the global registry, keyed by name, so they can be referenced via `namedEffect: { type: '' }`. + +### Signature + +```typescript +function registerEffects(effects: Record): void; +``` + +### Parameters + +| Parameter | Type | Description | +| --------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------- | +| `effects` | `Record` | Map of effect name โ†’ `EffectModule` (`{ web, getNames, style?, prepare? }`). See [Type Definitions](./types.md). | + +### Returns + +`void` + +If a `namedEffect.type` isn't registered, `getRegisteredEffect(name)` logs a warning and returns `null` โ€” which is why an unregistered `namedEffect` makes `getWebAnimation` (and friends) return `null`. + +### Example ```typescript -const elements = document.querySelectorAll('.animate-on-scroll'); -const preparations = elements.map((element) => { - return new Promise((resolve) => { - prepareAnimation( - element, +import { registerEffects } from '@wix/motion'; + +registerEffects({ + FadeUp: { + getNames: () => ['FadeUp'], + web: () => [ { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'RevealScroll', - direction: 'bottom', - }, + name: 'fade-up', + keyframes: [ + { opacity: 0, transform: 'translateY(20px)' }, + { opacity: 1, transform: 'translateY(0)' }, + ], }, - resolve, - ); - }); + ], + }, }); -// Wait for all preparations to complete -Promise.all(preparations).then(() => { - // Create animations after all measurements are done - elements.forEach((element) => { - getScrubScene(element, animationOptions, trigger); - }); -}); +// Now usable anywhere as: +// getWebAnimation(el, { namedEffect: { type: 'FadeUp' }, duration: 600 }); ``` -#### Performance-Critical Preparation +`@wix/motion-presets` ships a large catalog of ready-made modules built to this same contract โ€” see its docs for the preset list. Motion only documents the registry contract, not the presets themselves. -```typescript -// Prepare during idle time -if ('requestIdleCallback' in window) { - requestIdleCallback(() => { - prepareAnimation('#hero-element', heroAnimationConfig, () => { - // Ready to animate when needed - }); - }); -} else { - // Fallback for browsers without requestIdleCallback - setTimeout(() => { - prepareAnimation('#hero-element', heroAnimationConfig); - }, 0); -} -``` +## getEasing / getJsEasing -## Common Patterns and Best Practices +Resolve a named or raw easing value into a CSS easing string or a JS easing function. -### Animation Lifecycle Management +### Signature ```typescript -class AnimationManager { - private animations: Map = new Map(); - - createAnimation(id: string, element: HTMLElement, options: TimeAnimationOptions) { - // Clean up existing animation - this.destroyAnimation(id); - - // Prepare element - prepareAnimation(element, options, () => { - // Create new animation - const animation = getWebAnimation(element, options); - this.animations.set(id, animation); - }); - } - - async playAnimation(id: string) { - const animation = this.animations.get(id); - if (animation) { - await animation.play(); - } - } - - destroyAnimation(id: string) { - const animation = this.animations.get(id); - if (animation) { - animation.cancel(); - this.animations.delete(id); - } - } - - destroyAll() { - this.animations.forEach((animation) => animation.cancel()); - this.animations.clear(); - } -} +function getEasing(easing?: string): string; +function getJsEasing(easing?: string): ((t: number) => number) | undefined; ``` -### Responsive Animation Creation - -```typescript -function createResponsiveAnimation(element: HTMLElement) { - const isMobile = window.innerWidth < 768; - const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - - let options: TimeAnimationOptions; - - if (prefersReducedMotion) { - // Minimal animation for accessibility - options = { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 200, - }; - } else if (isMobile) { - // Lighter animation for mobile - options = { - type: 'TimeAnimationOptions', - namedEffect: { type: 'SlideIn', direction: 'bottom' }, - duration: 600, - }; - } else { - // Full animation for desktop - options = { - type: 'TimeAnimationOptions', - namedEffect: { type: 'ArcIn', direction: 'bottom' }, - duration: 1000, - easing: 'backOut', - }; - } - - return getWebAnimation(element, options); -} -``` +### Parameters -## Performance Considerations +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `easing` | `string` | Optional. A named easing key, a raw `cubic-bezier(x1, y1, x2, y2)` string (hyphenated), or โ€” for `getJsEasing` only โ€” a CSS `linear(...)` string. | -### Function Selection Guidelines +### Returns -- **Use `getWebAnimation()`** for: - - Interactive animations requiring control - - Complex timing sequences - - Dynamic parameter changes - - Event-driven animations - - When server-side rendering is not available +- `getEasing(easing?)` โ†’ `string` โ€” resolves a named key via the CSS easing map, falling back to the raw string if unresolved, else `'linear'`. +- `getJsEasing(easing?)` โ†’ `((t: number) => number) | undefined` โ€” resolves a named key via the JS easing map, else parses a `cubic-bezier(...)` string, else parses a CSS `linear(...)` string, else falls back to the linear JS easing. Returns `undefined` only when `easing` is falsy. -- **Use `getCSSAnimation()`** for: - - Simple, fire-and-forget animations - - Mobile-first applications - - Animations that run during page load - - Performance-critical scenarios - - When server-side rendering is available for a more performent usage +Valid named keys: -- **Use `getScrubScene()`** for: - - Controling pointer-driven animations - - Polyfilling ViewTimelines for scroll animations - - Custom effects driven by scroll or pointer movement +- **JS easings** (Penner functions, usable via `getJsEasing` and as `Sequence`'s `offsetEasing`): `linear`, `sineIn`, `sineOut`, `sineInOut`, `quadIn`, `quadOut`, `quadInOut`, `cubicIn`, `cubicOut`, `cubicInOut`, `quartIn`, `quartOut`, `quartInOut`, `quintIn`, `quintOut`, `quintInOut`, `expoIn`, `expoOut`, `expoInOut`, `circIn`, `circOut`, `circInOut`, `backIn`, `backOut`, `backInOut`. +- **CSS easings** (usable via `getEasing` and the `easing` option): `linear`, `ease`, `easeIn`, `easeOut`, `easeInOut`, plus every JS key above (except `linear`/`ease*`) mapped to a `cubic-bezier(...)` value. -- **Use `prepareAnimation()`** for: - - Preparing CSS animations requiring measurements which are not possible via CSS +Names like `easeOutCubic`, `elasticOut`, `bounceOut`, and `bounceIn` don't exist. (`elastic` / `bounce` exist only as `ScrubTransitionEasing` values for pointer smoothing โ€” a different field.) -### Memory Management +### Example ```typescript -// Always clean up animations when done -class ComponentWithAnimations { - private animations: AnimationGroup[] = []; - - createAnimations() { - this.animations.push(getWebAnimation(this.element, options)); - } - - destroy() { - // Clean up all animations - this.animations.forEach((animation) => { - animation.cancel(); - }); - this.animations = []; - } -} +import { getEasing, getJsEasing } from '@wix/motion'; + +getEasing('quadIn'); // โ†’ a 'cubic-bezier(...)' string +getEasing(); // โ†’ 'linear' (default) + +const ease = getJsEasing('backOut'); // โ†’ (t: number) => number +getJsEasing(); // โ†’ undefined (falsy input) ``` --- -**Next**: Explore the [AnimationGroup API](animation-group.md) for advanced animation control, or check out [Type Definitions](types.md) for complete TypeScript reference. +**Next**: See [Sequence Creation](./get-sequence.md) for `getSequence()` / `createAnimationGroups()`, or return to the [API Reference](./README.md). diff --git a/packages/motion/docs/api/sequence.md b/packages/motion/docs/api/sequence.md index de33f155..a2d48180 100644 --- a/packages/motion/docs/api/sequence.md +++ b/packages/motion/docs/api/sequence.md @@ -39,7 +39,7 @@ class Sequence extends AnimationGroup { getProgress(): number; progress(p: number): void; setPlaybackRate(rate: number): void; - get finished(): Promise; + get finished(): Promise; get playState(): AnimationPlayState; } ``` @@ -336,10 +336,10 @@ Returns the current playback state from the first animation (`'idle'`, `'running ### `finished` ```typescript -get finished(): Promise +get finished(): Promise ``` -Promise that resolves when the last animation completes. +Promise that resolves (via `Promise.all`) when every animation across all child groups completes. --- diff --git a/packages/motion/docs/api/types.md b/packages/motion/docs/api/types.md index 9d1bb0c5..eb0f34ef 100644 --- a/packages/motion/docs/api/types.md +++ b/packages/motion/docs/api/types.md @@ -1,948 +1,394 @@ -# Type Definitions +# Types -Complete TypeScript type reference for Wix Motion's animation system. This guide covers all interfaces, types, and enums used throughout the library. +TypeScript reference for the types `@wix/motion` itself defines and exports (`src/types.ts`). These are the shapes accepted by `getWebAnimation()`, `getCSSAnimation()`, `getScrubScene()`, `getSequence()`, `createAnimationGroups()`, and `registerEffects()`. -## Overview +## Animation options -Wix Motion provides comprehensive TypeScript support with detailed type definitions for: +### `AnimationOptions` -- **Core Configuration Types** - Animation options and parameters -- **Named Effect Types** - All 82+ animation presets with their specific options -- **Animation Control Types** - Return types and control interfaces -- **Utility Types** - Measurements, directions, and helper types -- **Advanced Types** - Custom effects and trigger configurations - -## Core Configuration Types - -### `TimeAnimationOptions` - -Configuration for time-based animations (entrance and ongoing animations). +The options object accepted by `getWebAnimation`, `getCSSAnimation`, `getScrubScene`, `getAnimation`, and used inside `AnimationGroupArgs`. ```typescript -interface TimeAnimationOptions { - type: 'TimeAnimationOptions'; - keyframeEffect?: MotionKeyframeEffect; // Native KeyframeEffects - namedEffect?: NamedEffect; // Pre-registered named effects - customEffect?: CustomEffect; // JS-based custom effect - duration?: number; // Milliseconds - delay?: number; // Milliseconds - endDelay?: number; // Milliseconds - easing?: string; // CSS or JS easing function - iterations?: number; // Number of repeats (Infinity or 0 for infinite) - alternate?: boolean; // Alternating effect direction on each iteration - fill?: AnimationFillMode; // 'none' | 'backwards' | 'forwards' | 'both' - reversed?: boolean; // Play in reverse -} +type AnimationOptions = (TimeAnimationOptions | ScrubAnimationOptions) & AnimationExtraOptions; ``` -#### Examples - -```typescript -// Basic entrance animation -const fadeInOptions: TimeAnimationOptions = { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 800, - easing: 'easeOut', - fill: 'backwards', -}; - -// Complex bounce animation -const bounceOptions: TimeAnimationOptions = { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'BounceIn', - direction: 'bottom', - }, - duration: 1200, - delay: 300, - easing: 'backOut', - fill: 'backwards', -}; +> **No top-level `type` field.** `AnimationOptions` is a union of `TimeAnimationOptions` and `ScrubAnimationOptions`, but the engine never reads a discriminant string off it. Which branch applies is determined **structurally**: by which of `keyframeEffect` / `namedEffect` / `customEffect` is present, and by whether a scroll/pointer `trigger` argument was passed. Do not add a `type: 'TimeAnimationOptions'` (or similar) field โ€” it is ignored. +> +> ```typescript +> // โœ… correct โ€” no top-level `type` +> getWebAnimation(el, { namedEffect: { type: 'FadeIn' }, duration: 1000 }); +> ``` +> +> (`namedEffect.type` _is_ real โ€” see [`NamedEffect`](#namedeffect) below. It's the top-level `type` on the options object that doesn't exist.) -// Infinite pulse animation -const pulseOptions: TimeAnimationOptions = { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - intensity: 0.8, - }, - duration: 2000, - iterations: Infinity, - alternate: true, -}; -``` - -### `ScrubAnimationOptions` +### `TimeAnimationOptions` -Configuration for scrub-based animations (scroll, mouse, and background scroll). +Options for a time-based (duration/delay-driven) animation โ€” used when no scroll/pointer trigger is given. ```typescript -interface ScrubAnimationOptions { - type: 'ScrubAnimationOptions'; +type TimeAnimationOptions = { + id?: string; keyframeEffect?: MotionKeyframeEffect; namedEffect?: NamedEffect; customEffect?: CustomEffect; - startOffset?: RangeOffset; // animation-range-start for usage with view() timelines - endOffset?: RangeOffset; // animation-range-end for usage with view() timelines - playbackRate?: number; // Speed multiplier - easing?: string; // Transition easing - iterations?: number; // Usually 1 for scrub animations - fill?: AnimationFillMode; + duration?: number; // ms + delay?: number; // ms + endDelay?: number; // ms + easing?: string; // named key or CSS easing string + iterations?: number; // 0 โ‡’ Infinity; undefined โ‡’ 1 alternate?: boolean; + fill?: AnimationFillMode; // 'none' | 'backwards' | 'forwards' | 'both' reversed?: boolean; - transitionDuration?: number; // For mouse animations (ms) - transitionDelay?: number; // For mouse animations (ms) - transitionEasing?: ScrubTransitionEasing; - centeredToTarget?: boolean; // For mouse animations - duration?: LengthPercentage; // Scroll-based duration -} -``` - -#### Examples - -```typescript -// Scroll parallax animation -const parallaxOptions: ScrubAnimationOptions = { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 0.3, - }, - startOffset: { name: 'cover' }, - endOffset: { name: 'exit' }, -}; - -// Mouse tilt animation -const mouseOptions: ScrubAnimationOptions = { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 15, - perspective: 800, - }, - transitionDuration: 200, - transitionEasing: 'easeOut', -}; - -// Background zoom animation -const bgOptions: ScrubAnimationOptions = { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgZoom', - direction: 'in', - zoom: 40, - }, }; ``` -## Named Effect Types - -### Entrance Animations +- `keyframeEffect`, `namedEffect`, and `customEffect` are mutually exclusive ways to describe what animates โ€” see [Effects](#effects). +- `duration`, `delay`, and `endDelay` are all in **milliseconds**. +- `iterations: 0` means infinite repeats; an omitted `iterations` means `1`. -Time-based animations for element reveals and transitions. +### `ScrubAnimationOptions` -#### Base Interface +Options for a scrub-driven animation โ€” used when a `view-progress` (scroll) or `pointer-move` trigger is passed. ```typescript -type BaseDataItemLike = { +type ScrubAnimationOptions = { id?: string; - type: Type; + keyframeEffect?: MotionKeyframeEffect; + namedEffect?: NamedEffect; + customEffect?: CustomEffect; + startOffset?: RangeOffset; + endOffset?: RangeOffset; + playbackRate?: number; + easing?: string; + iterations?: number; + fill?: AnimationFillMode; + alternate?: boolean; + reversed?: boolean; + transitionDuration?: number; // pointer smoothing (ms) + transitionDelay?: number; + transitionEasing?: ScrubTransitionEasing; // 'linear' | 'hardBackOut' | 'easeOut' | 'elastic' | 'bounce' + centeredToTarget?: boolean; + duration?: LengthPercentage; // NOTE: length/percentage, not ms }; ``` -#### Common Types +- **`duration` here is a [`LengthPercentage`](#length--percentage--lengthpercentage), not a millisecond number** โ€” unlike `TimeAnimationOptions.duration`. It expresses a portion of the scroll/scrub range. +- `startOffset` / `endOffset` live on these options, not on the trigger. +- `transitionDuration` / `transitionDelay` / `transitionEasing` / `centeredToTarget` only apply to pointer-driven (`pointer-move`) scrubbing. -```typescript -// Simple fade entrance -type FadeIn = BaseDataItemLike<'FadeIn'>; +### `AnimationExtraOptions` -// Directional arc entrance -type ArcIn = BaseDataItemLike<'ArcIn'> & { - direction: EffectFourDirections; // 'top' | 'right' | 'bottom' | 'left' -}; - -// Bouncing entrance -type BounceIn = BaseDataItemLike<'BounceIn'> & { - direction: EffectFourDirections | 'center'; - distanceFactor?: number; -}; - -// Custom directional entrance -type GlideIn = BaseDataItemLike<'GlideIn'> & { - direction: number; // Angle in degrees - distance: UnitLengthPercentage; // Movement distance - startFromOffScreen?: boolean; -}; -``` - -#### Examples +Mixed into both `TimeAnimationOptions` and `ScrubAnimationOptions` to form `AnimationOptions`. ```typescript -// Simple fade -const fadeIn: FadeIn = { type: 'FadeIn' }; - -// Arc from right -const arcIn: ArcIn = { - type: 'ArcIn', - direction: 'right', -}; - -// Bounce from bottom -const bounceIn: BounceIn = { - type: 'BounceIn', - direction: 'bottom', -}; - -// Custom glide at 45 degrees -const glideIn: GlideIn = { - type: 'GlideIn', - direction: 45, - distance: { value: 100, unit: 'px' }, +type AnimationExtraOptions = { + effectId?: string; + effect?: (progress: () => number | { x: number | undefined; y: number | undefined }) => void; + measures?: Record; }; ``` -### Ongoing Animations +- `effectId` โ€” an id surfaced on the `animationend` `CustomEvent` dispatched by `AnimationGroup.onFinish` (see [`animation-group.md`](./animation-group.md)). +- `effect` โ€” an optional per-frame progress reader, given a function that returns the current progress. +- `measures` โ€” arbitrary measured values an effect's `prepare()` hook can stash for `web()`/`style()` to consume. -Continuous looping animations for attention and ambient motion. +## Effects -#### Common Types +`keyframeEffect`, `namedEffect`, and `customEffect` are the three mutually-exclusive ways to describe what an `AnimationOptions` object actually animates. -```typescript -// Pulsing scale effect -type Pulse = BaseDataItemLike<'Pulse'> & { - intensity?: number; // 0.1 - 2.0 multiplier -}; +### `NamedEffect` -// Breathing movement -type Breathe = BaseDataItemLike<'Breathe'> & { - direction: 'vertical' | 'horizontal' | 'center'; - distance: UnitLengthPercentage; -}; - -// Spinning rotation -type Spin = BaseDataItemLike<'Spin'> & { - direction: 'clockwise' | 'counter-clockwise'; -}; - -// Directional poking -type Poke = BaseDataItemLike<'Poke'> & { - direction: EffectFourDirections; - intensity?: number; -}; -``` - -#### Examples +References a preset registered via `registerEffects()`. ```typescript -// Soft pulse -const pulse: Pulse = { - type: 'Pulse', - intensity: 0.6, -}; - -// Vertical breathing -const breathe: Breathe = { - type: 'Breathe', - direction: 'vertical', - distance: { value: 10, unit: 'px' }, -}; - -// Clockwise spinning -const spin: Spin = { - type: 'Spin', - direction: 'clockwise', -}; +type NamedEffect = { type: string } & Record; ``` -### Scroll Animations - -Scroll-driven effects synchronized to viewport position. +`type` here **is** a real field โ€” it's the registered preset name (e.g. `'FadeIn'`). This is the one place in motion's option types where a `type` string is meaningful; it is unrelated to the (non-existent) top-level `type` field discussed under [`AnimationOptions`](#animationoptions). Everything else on a `NamedEffect` is preset-specific and defined by whichever package registered it (see [Types owned elsewhere](#types-owned-elsewhere)). -#### Common Types +### `CustomEffect` ```typescript -// Basic parallax scrolling -type ParallaxScroll = BaseDataItemLike<'ParallaxScroll'> & { - speed: number; // Movement speed multiplier - range?: EffectScrollRange; // 'in' | 'out' | 'continuous' -}; - -// Fade based on scroll -type FadeScroll = BaseDataItemLike<'FadeScroll'> & { - range: EffectScrollRange; - opacity: number; // Target opacity -}; - -// Movement on scroll -type MoveScroll = BaseDataItemLike<'MoveScroll'> & { - angle: number; // Movement direction - range?: EffectScrollRange; - distance?: UnitLengthPercentage; -}; - -// Scaling on scroll -type GrowScroll = BaseDataItemLike<'GrowScroll'> & { - direction: EffectNineDirections; // Includes corners + center - range?: EffectScrollRange; - scale?: number; - speed?: number; // Y-axis movement -}; +type CustomEffect = + | { ranges: { name: string; min: number; max: number; step?: number }[] } + | ((element: Element | null, progress: number | null) => void); ``` -#### Examples - -```typescript -// Slow parallax background -const parallax: ParallaxScroll = { - type: 'ParallaxScroll', - speed: 0.3, -}; +`CustomEffect` is a union of two shapes, but only the **function** form does anything at runtime in `@wix/motion`: -// Fade in on scroll -const fadeScroll: FadeScroll = { - type: 'FadeScroll', - range: 'in', - opacity: 1, -}; +- **Function form (primary)** โ€” `(element, progress) => void`. Wrapped in a `CustomAnimation` that runs a `requestAnimationFrame` loop, calling your function whenever computed progress changes. On cancellation it is called once with `progress === null` โ€” handle this as a reset/cleanup signal. -// Move diagonally -const moveScroll: MoveScroll = { - type: 'MoveScroll', - angle: 225, - distance: { value: 200, unit: 'px' }, - range: 'in', -}; -``` + ```typescript + const customEffect: CustomEffect = (element, progress) => { + if (progress === null) { + // cancelled โ€” reset any applied styles here + return; + } + if (element instanceof HTMLElement) { + element.style.setProperty('opacity', String(progress)); + } + }; + ``` -### Mouse Animations +- **`{ ranges }` object form** โ€” accepted by the type, but **inert** when used with `@wix/motion` alone; it produces no visible effect on its own. Don't rely on it unless a higher-level package (e.g. `@wix/interact`) interprets it. -Interactive pointer-driven effects. +### `MotionKeyframeEffect` -#### Common Types +Inline WAAPI/CSS keyframes โ€” no registration required. ```typescript -// Element following mouse -type TrackMouse = BaseDataItemLike<'TrackMouse'> & { - distance?: UnitLengthPercentage; - axis?: MouseEffectAxis; // 'both' | 'horizontal' | 'vertical' - inverted?: boolean; -}; - -// 3D tilt based on mouse -type Tilt3DMouse = BaseDataItemLike<'Tilt3DMouse'> & { - angle?: number; // Maximum tilt angle - perspective?: number; // 3D perspective distance - inverted?: boolean; -}; - -// Scale based on mouse proximity -type ScaleMouse = BaseDataItemLike<'ScaleMouse'> & { - distance?: UnitLengthPercentage; - axis?: MouseEffectAxis; - scale?: number; // Maximum scale - scaleDirection: EffectScaleDirection; // 'up' | 'down' - inverted?: boolean; +type MotionKeyframeEffect = { + name: string; + keyframes: Keyframe[]; }; ``` -#### Examples - -```typescript -// Track mouse movement -const trackMouse: TrackMouse = { - type: 'TrackMouse', - distance: { value: 50, unit: 'px' }, - axis: 'both', -}; +No `type` field. `name` becomes the animation/`@keyframes` name; `keyframes` is a standard WAAPI `Keyframe[]`. -// 3D tilt effect -const tiltMouse: Tilt3DMouse = { - type: 'Tilt3DMouse', - angle: 15, - perspective: 800, -}; - -// Scale on hover -const scaleMouse: ScaleMouse = { - type: 'ScaleMouse', - scale: 1.1, - scaleDirection: 'up', -}; -``` +## Triggers & scrub -### Background Scroll Animations +### `TriggerVariant` -Specialized effects for background media elements. - -#### Common Types +The shape of the (optional) 3rd argument to `getWebAnimation` / `getScrubScene` / `getAnimation`. ```typescript -// Background parallax -type BgParallax = BaseDataItemLike<'BgParallax'> & { - speed?: number; -}; - -// Background zoom -type BgZoom = BaseDataItemLike<'BgZoom'> & { - direction: 'in' | 'out'; - zoom?: number; // Zoom intensity -}; - -// Background fade -type BgFade = BaseDataItemLike<'BgFade'> & { - range: 'in' | 'out'; -}; - -// Complex 3D background effect -type BgFake3D = BaseDataItemLike<'BgFake3D'> & { - stretch?: number; // Y-axis stretch - zoom?: number; // 3D zoom distance +type TriggerVariant = { + id: string; + trigger: 'view-progress' | 'pointer-move'; + componentId: string; }; ``` -#### Examples +- Callers actually pass `Partial & { element?: HTMLElement }` โ€” and for pointer triggers, `axis?: PointerMoveAxis` as well. `element` and `axis` are **not** fields of `TriggerVariant` itself; they're read directly off the object passed as the trigger argument. +- Omitting `trigger` (or passing neither `'view-progress'` nor `'pointer-move'`) produces a time-based animation. -```typescript -// Background parallax -const bgParallax: BgParallax = { - type: 'BgParallax', - speed: 0.5, -}; +### `RangeOffset` -// Zoom in effect -const bgZoom: BgZoom = { - type: 'BgZoom', - direction: 'in', - zoom: 30, -}; - -// Complex 3D effect -const bg3D: BgFake3D = { - type: 'BgFake3D', - stretch: 1.3, - zoom: 20, +```typescript +type RangeOffset = { + name?: 'entry' | 'exit' | 'contain' | 'cover' | 'entry-crossing' | 'exit-crossing'; + offset?: LengthPercentage; }; ``` -## Utility Types +Used for `startOffset` / `endOffset` on `ScrubAnimationOptions`, and shows up on `AnimationGroup.animations[i].start` / `.end` for scroll-driven groups. -### Measurements and Units +### `ScrubTransitionEasing` ```typescript -// Length with unit -type Length = { - value: number; - unit: 'px' | 'em' | 'rem' | 'vh' | 'vw' | 'vmin' | 'vmax'; -}; - -// Percentage -type Percentage = { - value: number; - unit: 'percentage'; -}; - -// Combined length/percentage -type LengthPercentage = Length | Percentage; -type UnitLengthPercentage = LengthPercentage; - -// 2D point -type Point = [number, number]; +type ScrubTransitionEasing = 'linear' | 'hardBackOut' | 'easeOut' | 'elastic' | 'bounce'; ``` -#### Examples +Named easing curves for smoothing pointer-driven scrub transitions (`transitionEasing` on `ScrubAnimationOptions`). These are a distinct, closed set from the general `easing` string โ€” `'elastic'` and `'bounce'` exist only here, not as named JS/CSS easings. -```typescript -// Different measurement types -const pixelDistance: Length = { value: 100, unit: 'px' }; -const remDistance: Length = { value: 2, unit: 'rem' }; -const viewportDistance: Length = { value: 50, unit: 'vh' }; -const percentDistance: Percentage = { value: 75, unit: 'percentage' }; - -// Points for coordinates -const centerPoint: Point = [0.5, 0.5]; -const topLeft: Point = [0, 0]; -``` - -### Directions and Positions +### `PointerMoveAxis` ```typescript -// Four main directions -type EffectFourDirections = 'top' | 'right' | 'bottom' | 'left'; - -// Eight directions (includes corners) -type EffectEightDirections = - | EffectFourDirections - | 'top-right' - | 'top-left' - | 'bottom-right' - | 'bottom-left'; - -// Nine directions (includes center) -type EffectNineDirections = EffectEightDirections | 'center'; - -// Two-way directions -type EffectTwoSides = 'left' | 'right'; - -// Corner positions -type EffectFourCorners = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'; - -// Scroll ranges -type EffectScrollRange = 'in' | 'out' | 'continuous'; - -// Scale directions -type EffectScaleDirection = 'up' | 'down'; +type PointerMoveAxis = 'x' | 'y'; ``` -### Mouse-Specific Types +Set on the **trigger** object (e.g. `{ trigger: 'pointer-move', axis: 'y' }`), not on the effect or options. It selects whether a keyframe pointer effect reads `progress.x` or `progress.y`. + +### `Progress` ```typescript -// Mouse effect axes -type MouseEffectAxis = 'both' | 'horizontal' | 'vertical'; - -// Mouse pivot points -type MousePivotAxis = 'top' | 'bottom' | 'right' | 'left' | 'center-horizontal' | 'center-vertical'; - -// Mouse progress information -type Progress = { - x: number; // Normalized X position (0-1) - y: number; // Normalized Y position (0-1) - v?: { x: number; y: number }; // Velocity vector - active?: boolean; // Is interaction active -}; +type Progress = { x: number; y: number; v?: { x: number; y: number }; active?: boolean }; ``` -## Animation Control Types +The payload driving pointer-based scrubbing: normalized `x`/`y` position, an optional velocity vector `v`, and whether the interaction is currently `active`. -### `AnimationGroup` +### `MouseAnimationInstance` / `CustomMouseAnimationInstance` -Main control interface returned by `getWebAnimation()`. +Returned by `getWebAnimation` on the `pointer-move` + non-`keyframeEffect` path (instead of an `AnimationGroup`). ```typescript -interface AnimationGroup { - animations: (Animation & { - start?: RangeOffset; - end?: RangeOffset; - })[]; - options?: AnimationGroupOptions; - ready: Promise; - - // Control methods - getProgress(): number; - play(callback?: () => void): Promise; - pause(): void; - reverse(callback?: () => void): Promise; - progress(p: number): void; - cancel(): void; - setPlaybackRate(rate: number): void; - onFinish(callback: () => void): Promise; - - // State properties - get finished(): Promise; - get playState(): AnimationPlayState; -} -``` - -### Mouse Animation Interfaces - -```typescript -// Basic mouse animation interface interface MouseAnimationInstance { target: HTMLElement; - play(): void; - progress(progress: Progress): void; - cancel(): void; + play: () => void; + progress: (progress: Progress) => void; + cancel: () => void; } -// Extended interface for custom mouse animations interface CustomMouseAnimationInstance extends MouseAnimationInstance { - getProgress(): Progress; + getProgress: () => Progress; } - -// Factory function type -type MouseAnimationFactory = (element: HTMLElement) => MouseAnimationInstance; ``` -### Scroll Scene Interfaces +- `play()` arms the instance; `progress(p)` feeds it a `Progress` sample (typically from a pointer-move listener); `cancel()` tears it down. +- `CustomMouseAnimationInstance` adds `getProgress()` for reading back the last-applied `Progress`. -```typescript -// Scroll-driven scene -interface ScrubScrollScene { - start: RangeOffset; - end: RangeOffset; - viewSource: HTMLElement; - ready: Promise; - getProgress(): number; - effect(timestamp: any, progress: number): void; - disabled: boolean; - destroy(): void; - groupId?: string; -} +## Sequences -// Pointer-driven scene -interface ScrubPointerScene { - target?: HTMLElement; - centeredToTarget?: boolean; - transitionDuration?: number; - transitionEasing?: ScrubTransitionEasing; - getProgress(): Progress; - effect(progress: Progress): void; - disabled: boolean; - destroy(): void; - allowActiveEvent?: boolean; -} -``` - -## Advanced Configuration Types - -### Range Offsets - -Precise control over scroll animation timing. - -```typescript -type RangeOffset = { - name?: 'entry' | 'exit' | 'contain' | 'cover' | 'entry-crossing' | 'exit-crossing'; - offset?: LengthPercentage; -}; -``` +### `SequenceOptions` -#### Examples +Constructor options for `Sequence` / the first argument to `getSequence()`. See [`sequence.md`](./sequence.md) for the full stagger-offset formula and examples. ```typescript -// Start when element is 20% visible -const startOffset: RangeOffset = { - name: 'entry', - offset: { value: 20, unit: 'percentage' }, -}; - -// End 100px before element exits -const endOffset: RangeOffset = { - name: 'exit', - offset: { value: 100, unit: 'px' }, +type SequenceOptions = { + delay?: number; // ms base delay, default 0 + offset?: number; // ms stagger interval, default 0 + offsetEasing?: string | ((p: number) => number); }; - -// Cover entire viewport interaction -const coverRange: RangeOffset = { name: 'cover' }; ``` -### Trigger Variants +### `AnimationGroupArgs` -Configuration for how animations are triggered. +One entry in the array passed to `getSequence()` / `createAnimationGroups()`. ```typescript -type TriggerVariant = { - id: string; - trigger: 'view-progress' | 'pointer-move'; - componentId: string; +type AnimationGroupArgs = { + target: HTMLElement | HTMLElement[] | string | null; + options: AnimationOptions; + context?: Record; }; ``` -#### Examples - -```typescript -// Scroll trigger -const scrollTrigger: TriggerVariant = { - id: 'scroll-animation-1', - trigger: 'view-progress', - componentId: 'hero-section', -}; - -// Mouse trigger -const mouseTrigger: TriggerVariant = { - id: 'mouse-animation-1', - trigger: 'pointer-move', - componentId: 'interactive-card', -}; -``` +- `target` โ€” element(s) to animate. `HTMLElement[]` and `string` selectors expand to one group per matched element. +- `options` โ€” the `AnimationOptions` to apply to that target. +- `context` โ€” forwarded to animation creation (e.g. `{ reducedMotion: true }`). -### Custom Effects +## Units & fill mode -For creating completely custom animations. +### `Length` / `Percentage` / `LengthPercentage` ```typescript -type CustomEffect = { - ranges: { - name: string; - min: number; - max: number; - step?: number; - }[]; -}; - -type MotionKeyframeEffect = BaseDataItemLike<'KeyframeEffect'> & { - name: string; - keyframes: Keyframe[]; -}; +type Length = { value: number; unit: 'px' | 'em' | 'rem' | 'vh' | 'vw' | 'vmin' | 'vmax' }; +type Percentage = { value: number; unit: 'percentage' }; +type LengthPercentage = Length | Percentage; ``` -#### Examples +Used anywhere motion needs a dimension that could be either an absolute length or a percentage โ€” e.g. `RangeOffset.offset` and `ScrubAnimationOptions.duration`. -```typescript -// Custom progress ranges -const customEffect: CustomEffect = { - ranges: [ - { name: 'opacity', min: 0, max: 1, step: 0.01 }, - { name: 'scale', min: 0.5, max: 1.5, step: 0.01 }, - { name: 'rotation', min: 0, max: 360, step: 1 }, - ], -}; - -// Custom keyframe effect -const keyframeEffect: MotionKeyframeEffect = { - type: 'KeyframeEffect', - name: 'customBounce', - keyframes: [ - { transform: 'scale(0) rotate(0deg)', opacity: 0 }, - { transform: 'scale(1.2) rotate(180deg)', opacity: 0.8 }, - { transform: 'scale(1) rotate(360deg)', opacity: 1 }, - ], -}; -``` - -### Easing Types +### `AnimationFillMode` ```typescript -// Scrub transition easing options -type ScrubTransitionEasing = 'linear' | 'hardBackOut' | 'easeOut' | 'elastic' | 'bounce'; - -// Animation fill modes type AnimationFillMode = 'none' | 'backwards' | 'forwards' | 'both'; ``` -## Union Types +Standard WAAPI fill mode, used by both `TimeAnimationOptions.fill` and `ScrubAnimationOptions.fill`. -### Combined Named Effects +## Authoring / effect modules -```typescript -// All entrance animations -type EntranceAnimation = FadeIn | ArcIn | BounceIn | SlideIn | FlipIn | - DropIn | ExpandIn | GlideIn | SpinIn | PunchIn | /* ... and more */; - -// All ongoing animations -type OngoingAnimation = Pulse | Breathe | Spin | Wiggle | Flash | - Bounce | Swing | Poke | /* ... and more */; - -// All scroll animations -type ScrollAnimation = ParallaxScroll | FadeScroll | MoveScroll | - GrowScroll | RevealScroll | /* ... and more */; +These types define the contract for writing and registering your own effect modules via `registerEffects()`. Most consumers of `@wix/motion` won't need them unless they're authoring presets. -// All mouse animations -type MouseAnimation = TrackMouse | Tilt3DMouse | ScaleMouse | - BlurMouse | /* ... and more */; +### `AnimationData` -// All background scroll animations -type BackgroundScrollAnimation = BgParallax | BgZoom | BgFade | - BgRotate | /* ... and more */; - -// Union of all named effects -type NamedEffect = EntranceAnimation | OngoingAnimation | ScrollAnimation | - MouseAnimation | BackgroundScrollAnimation; -``` - -### Animation Options Union +The shape an effect module's `web()` / `style()` hook returns โ€” one entry per animated part/property group. ```typescript -// Main options type -type AnimationOptions = (TimeAnimationOptions | ScrubAnimationOptions) & AnimationExtraOptions; - -// Extra options for custom behavior -type AnimationExtraOptions = { - effectId?: string; - effect?: ( - progress: () => - | number - | { - x: number | undefined; - y: number | undefined; - }, - ) => void; +type AnimationData = (TimeAnimationOptions | AnimationDataForScrub) & { + name?: string; + keyframes: Record[]; + custom?: Record; + composite?: CompositeOperation; + part?: string; + timing?: Partial; }; ``` -## Type Guards and Utilities +- `keyframes` โ€” plain property-bag keyframes (not a WAAPI `Keyframe[]`) that motion turns into a `KeyframeEffect` or CSS `@keyframes` block. +- `part` โ€” targets a sub-element via `[data-motion-part~=""]` instead of the root target. +- `AnimationDataForScrub` mirrors `ScrubAnimationOptions`'s fields, with `duration?: LengthPercentage | number` plus internal `startOffsetAdd` / `endOffsetAdd` string fields. -### Type Guard Functions +### `AnimationEffectAPI` -```typescript -// Check if options are time-based -function isTimeAnimation(options: AnimationOptions): options is TimeAnimationOptions { - return options.type === 'TimeAnimationOptions'; -} +The primary contract for a registered effect module. -// Check if options are scrub-based -function isScrubAnimation(options: AnimationOptions): options is ScrubAnimationOptions { - return options.type === 'ScrubAnimationOptions'; -} +```typescript +type AnimationOptionsTypes = { + time: TimeAnimationOptions & AnimationExtraOptions; + scrub: ScrubAnimationOptions & AnimationExtraOptions; +}; -// Check if effect is entrance animation -function isEntranceAnimation(effect: NamedEffect): effect is EntranceAnimation { - return ['FadeIn', 'ArcIn', 'BounceIn', 'SlideIn' /* ... */].includes(effect.type); -} +type AnimationEffectAPI = { + web: ( + animationOptions: AnimationOptionsTypes[Enum], + dom?: DomApi, + options?: Record, + ) => AnimationData[]; + getNames: (animationOptions: AnimationOptionsTypes[Enum]) => string[]; + style?: (options: AnimationOptionsTypes[Enum]) => AnimationData[]; // enables the CSS path (getCSSAnimation) + prepare?: (options: AnimationOptionsTypes[Enum], dom?: DomApi) => void; // measure/mutate before animating +}; ``` -### Utility Type Functions +- `Enum` (`'time'` or `'scrub'`) selects which options shape the module handles. +- `web` builds the `AnimationData[]` used for WAAPI playback; `style`, if present, enables `getCSSAnimation`'s CSS-generation path; `prepare` runs a measure/mutate step via `fastdom` before animating. -```typescript -// Extract effect type from options -type ExtractEffectType = T extends { namedEffect: infer E } ? E : never; - -// Create typed animation options -function createTimeAnimation( - namedEffect: T, - options?: Partial>, -): TimeAnimationOptions { - return { - type: 'TimeAnimationOptions', - namedEffect, - duration: 1000, - ...options, - }; -} +### `DomApi` / `MeasureCallback` -function createScrubAnimation< - T extends ScrollAnimation | MouseAnimation | BackgroundScrollAnimation, ->( - namedEffect: T, - options?: Partial>, -): ScrubAnimationOptions { - return { - type: 'ScrubAnimationOptions', - namedEffect, - ...options, - }; -} -``` - -#### Usage Examples +Passed to an effect module's `web`/`style`/`prepare` hooks for batched, layout-thrash-free DOM access. ```typescript -// Type-safe entrance animation -const fadeAnimation = createTimeAnimation({ type: 'FadeIn' }, { duration: 800, easing: 'easeOut' }); - -// Type-safe scroll animation -const parallaxAnimation = createScrubAnimation( - { type: 'ParallaxScroll', speed: 0.3 }, - { startOffset: { name: 'cover' } }, -); - -// Type guards in use -function handleAnimation(options: AnimationOptions) { - if (isTimeAnimation(options)) { - // TypeScript knows this is TimeAnimationOptions - console.log('Duration:', options.duration); - } else if (isScrubAnimation(options)) { - // TypeScript knows this is ScrubAnimationOptions - console.log('Start offset:', options.startOffset); - } -} +type MeasureCallback = (fn: (target: HTMLElement | null) => void) => void; +type DomApi = { measure: MeasureCallback; mutate: MeasureCallback }; ``` -## Sequence Types +Both `measure` and `mutate` schedule `fn` through `fastdom`'s read/write batching. -### `SequenceOptions` +### `EffectModule` -Configuration for stagger timing when creating a `Sequence`. +The union of shapes `registerEffects()` accepts. ```typescript -type SequenceOptions = { - delay?: number; // Base delay (ms) for all groups. Default: 0 - offset?: number; // Stagger interval (ms) between groups. Default: 0 - offsetEasing?: string | ((p: number) => number); // Easing for offset distribution. Default: linear -}; +type EffectModule = + | AnimationEffectAPI<'time'> + | AnimationEffectAPI<'scrub'> + | ScrollEffectModule // { web(options, dom?): AnimationData[] } + | MouseEffectModule // { web(options): (el: HTMLElement) => object } + | WebAnimationEffectFactory<'scrub'>; ``` -Named easing strings (`'quadIn'`, `'sineOut'`, `'cubicBezier(0.4, 0, 0.2, 1)'`, etc.) are resolved to JS functions via `getJsEasing()`. Invalid strings fall back to `linear`. - -#### Examples - -```typescript -// Linear 200ms stagger -const linearOpts: SequenceOptions = { offset: 200 }; - -// Quadratic-in stagger with base delay -const quadOpts: SequenceOptions = { - delay: 100, - offset: 200, - offsetEasing: 'quadIn', -}; - -// Custom cubic easing function -const customOpts: SequenceOptions = { - offset: 300, - offsetEasing: (p) => p * p * p, -}; -``` +Most presets implement `AnimationEffectAPI`; `ScrollEffectModule` and `MouseEffectModule` are narrower shapes for scroll-only / mouse-only modules. -### `AnimationGroupArgs` +### `ScrubScrollScene` -Declarative target/options pair used by `getSequence()` and `createAnimationGroups()` to build `AnimationGroup` instances. +One entry of the array returned by `getScrubScene()` on the polyfilled (no `window.ViewTimeline`) scroll path. ```typescript -type AnimationGroupArgs = { - target: HTMLElement | HTMLElement[] | string | null; - options: AnimationOptions; - context?: Record; -}; +interface ScrubScrollScene { + start: RangeOffset; + end: RangeOffset; + viewSource: HTMLElement; + ready: Promise; + getProgress(): number; + effect(__: any, p: number): void; // p is 0..1 scroll progress + disabled: boolean; + destroy(): void; + groupId?: string; +} ``` -**Properties:** +Callers drive `effect(_, progress)` from their own scroll/IntersectionObserver logic (`@wix/interact` does this via its bundled [`fizban`](https://github.com/wix-incubator/fizban) polyfill) and call `destroy()` to clean up. -- `target` โ€” Element(s) to animate. `HTMLElement[]` and `string` selectors expand to one group per element. -- `options` โ€” Animation configuration (`TimeAnimationOptions` or `ScrubAnimationOptions`). -- `context` โ€” Optional context forwarded to animation creation (e.g. `{ reducedMotion: true }`). +### `ScrubPointerScene` -### `IndexedGroup` - -Specifies a group and its insertion position for `Sequence.addGroups()`. +Returned by `getScrubScene()` on the `pointer-move` path. ```typescript -type IndexedGroup = { - index: number; - group: AnimationGroup; -}; +interface ScrubPointerScene { + target?: HTMLElement; + centeredToTarget?: boolean; + transitionDuration?: number; + transitionEasing?: ScrubTransitionEasing; + getProgress(): Progress | number; + effect(__: any, p: Progress): void; // p is the pointer Progress payload + disabled: boolean; + destroy(): void; + allowActiveEvent?: boolean; + ready?: Promise; +} ``` -**Properties:** - -- `index` โ€” Position in the `animationGroups` array where the group should be inserted. -- `group` โ€” The `AnimationGroup` instance to insert. - -## Advanced Type Patterns +Callers drive `effect(_, progress)` from their own `pointermove`/`mousemove` listener with a `Progress` sample. -### Generic Animation Factory +## Types owned elsewhere -```typescript -interface AnimationFactory { - create(effect: T, options?: Partial): AnimationGroup; - getDefaults(effect: T): Partial; -} +`@wix/motion` only defines the types above. It does **not** own: -class TypedAnimationFactory implements AnimationFactory { - create(effect: T, options: Partial = {}): AnimationGroup { - const animationOptions: AnimationOptions = { - type: this.getAnimationType(effect), - namedEffect: effect, - ...this.getDefaults(effect), - ...options, - } as AnimationOptions; - - return getWebAnimation(this.target, animationOptions); - } - - private getAnimationType(effect: T): 'TimeAnimationOptions' | 'ScrubAnimationOptions' { - // Implementation would check effect type and return appropriate type - return isEntranceAnimation(effect) || isOngoingAnimation(effect) - ? 'TimeAnimationOptions' - : 'ScrubAnimationOptions'; - } - - getDefaults(effect: T): Partial { - // Return sensible defaults based on effect type - return {}; - } -} -``` +- **Per-effect/preset types** (e.g. entrance, ongoing, scroll, mouse, or background-scroll preset option shapes) โ€” those belong to `@wix/motion-presets`, which registers them via `registerEffects()`. +- **Declarative config types** (triggerโ†’effect wiring, component/interaction config) โ€” those belong to `@wix/interact`. --- -**Complete**: You now have comprehensive TypeScript type definitions for Wix Motion. Return to the [API Overview](README.md) or explore [Advanced Usage Patterns](../guides/advanced-patterns.md) for implementation examples. +Return to [API Reference](./README.md). diff --git a/packages/motion/docs/categories/README.md b/packages/motion/docs/categories/README.md deleted file mode 100644 index cef4e40e..00000000 --- a/packages/motion/docs/categories/README.md +++ /dev/null @@ -1,147 +0,0 @@ -# Animation Categories - -Wix Motion organizes its 82+ animation presets into 5 distinct categories, each designed for specific use cases and interaction patterns. - -## ๐ŸŽญ [Entrance Animations](entrance-animations.md) (24 presets) - -Perfect for element reveals and page transitions. These animations bring elements into view with impact and style. - -**Popular Presets**: FadeIn, ArcIn, BounceIn, SlideIn, FlipIn, GlideIn, DropIn, ExpandIn - -## ๐Ÿ”„ [Ongoing Animations](ongoing-animations.md) (16 presets) - -Continuous looping animations that add life and draw attention to elements. - -**Popular Presets**: Pulse, Breathe, Spin, Wiggle, Float, Bounce, Flash, Swing - -## ๐Ÿ“œ [Scroll Animations](scroll-animations.md) (19 presets) - -Scroll-driven effects that respond to viewport position for immersive storytelling. - -**Popular Presets**: ParallaxScroll, FadeScroll, GrowScroll, RevealScroll, TiltScroll, MoveScroll - -## ๐Ÿ–ฑ๏ธ [Mouse Animations](mouse-animations.md) (12 presets) - -Interactive pointer-driven effects that respond to mouse movement and hover states. - -**Popular Presets**: TrackMouse, Tilt3DMouse, ScaleMouse, BlurMouse, SwivelMouse - -## ๐Ÿ–ผ๏ธ [Background Scroll Animations](background-scroll-animations.md) (12 presets) - -Specialized effects designed specifically for background media elements and hero sections. - -**Popular Presets**: BgParallax, BgZoom, BgFade, BgRotate, BgPan, BgCloseUp - ---- - -## Quick Category Comparison - -| Category | Trigger | Duration | Use Case | Target Elements | -| ---------- | ---------------- | --------------- | ------------------------- | -------------------------- | -| Entrance | Viewport entry | 300-1500ms | Reveals, transitions | Any element | -| Ongoing | Manual/Auto | 1-4s (loop) | Attention, ambiance | Buttons, icons, decorative | -| Scroll | Scroll position | Based on scroll | Storytelling, parallax | Content blocks, media | -| Mouse | Pointer movement | Real-time | Interactivity, hover | Interactive elements | -| Background | Scroll position | Based on scroll | Hero effects, backgrounds | Background media only | - -## Choosing the Right Category - -### For Element Introductions - -Use **Entrance animations** when you want to reveal content with impact: - -- Page load animations -- Modal/popup appearances -- Content that appears after user actions -- Progressive disclosure patterns - -### For Continuous Effects - -Use **Ongoing animations** for elements that need constant attention: - -- Call-to-action buttons -- Loading indicators -- Decorative elements -- Brand/logo emphasis - -### For Scroll-Driven Storytelling - -Use **Scroll animations** to create engaging scroll experiences: - -- Article/blog content -- Product showcases -- Landing page narratives -- Progressive content reveals - -### For Interactive Feedback - -Use **Mouse animations** to enhance user interactions: - -- Hover effects on cards/buttons -- Cursor-following elements -- 3D interactive components -- Gaming-style interfaces - -### For Background Media - -Use **Background scroll animations** for immersive backgrounds: - -- Hero sections -- Full-screen videos/images -- Parallax landscapes -- Cinematic effects - -## Common Patterns Across Categories - -### Directional Controls - -Many animations support directional parameters: - -- **Four directions**: `top`, `right`, `bottom`, `left` -- **Eight directions**: Includes corners like `top-right`, `bottom-left` -- **Two sides**: `left`, `right` or `horizontal`, `vertical` -- **Angles**: Numeric degrees (0ยฐ = up, 90ยฐ = right, etc.) - -### Easing Functions - -All categories support both CSS and JavaScript easing: - -- **CSS**: `linear`, `ease`, `easeIn`, `easeOut`, `easeInOut` -- **Advanced**: `backOut`, `elasticOut`, `bounceOut` -- **Custom**: Cubic bezier curves for precise control - -## Performance Guidelines - -### Category-Specific Performance Tips - -#### Entrance Animations - -- Batch multiple entrances with staggered delays -- Consider CSS mode for simple fades/slides - -#### Ongoing Animations - -- Limit concurrent ongoing animations -- Reduce intensity on mobile -- Prefer CSS animations for infinite loops - -#### Scroll Animations - -- Leverage ViewTimeline API when available -- Use `fastdom` for scroll-dependent measurements -- Throttle scroll events appropriately - -#### Mouse Animations - -- Use transform-only properties when possible -- Consider disabling on touch devices - -#### Background Scroll - -- Pre-measure container dimensions -- Use GPU-accelerated properties -- Optimize for large viewport sizes - ---- - -**Ready to explore?** Click on any category above to see detailed guides with all available presets, configuration options, and usage examples. diff --git a/packages/motion/docs/categories/background-scroll-animations.md b/packages/motion/docs/categories/background-scroll-animations.md deleted file mode 100644 index dd289f3a..00000000 --- a/packages/motion/docs/categories/background-scroll-animations.md +++ /dev/null @@ -1,631 +0,0 @@ -# Background Scroll Animations - -Specialized effects designed for background media elements and hero sections. Perfect for creating immersive backgrounds, parallax landscapes, and cinematic scroll experiences. - -## Overview - -Background scroll animations are **scrub-based** animations specifically optimized for background media elements. They target elements with `data-motion-part` attributes and automatically measure container dimensions for accurate calculations. These animations create immersive, large-scale visual effects. - -### Key Characteristics - -- **Purpose**: Hero sections, background media, immersive experiences -- **Trigger**: Scroll position with viewport intersection -- **Target**: Background media layers (`BG_LAYER`, `BG_MEDIA`, `BG_IMG`) -- **Measurement**: Auto-calculated component dimensions -- **Scale**: Designed for large viewport interactions - -## Animation Categories - -### ๐ŸŒŠ **Parallax & Movement** - -Classic parallax scrolling and directional movement effects. - -### ๐Ÿ” **Zoom & Scale** - -Dynamic scaling and perspective zoom effects. - -### ๐ŸŽญ **Fade & Opacity** - -Sophisticated opacity transitions and layered fading. - -### ๐Ÿ”„ **Rotation & Transform** - -Rotational effects and complex transformations. - -### ๐ŸŽจ **Advanced 3D** - -Complex perspective effects with multi-layer interactions. - -## Complete Preset Reference - -| Animation | Category | Complexity | Direction Support | Measurement | Description | -| ----------------- | --------- | ---------- | ----------------- | ----------- | ------------------------------------ | -| **BgParallax** | Parallax | Simple | - | โœ“ | Classic background parallax movement | -| **ImageParallax** | Parallax | Medium | - | โœ“ | Enhanced image parallax with options | -| **BgPan** | Movement | Simple | 2-way | โœ“ | Horizontal panning movement | -| **BgZoom** | Zoom | Complex | 2-way | โœ“ | Dynamic zoom in/out effects | -| **BgCloseUp** | Zoom | Medium | - | โœ“ | Perspective zoom with fade | -| **BgPullBack** | Zoom | Medium | - | โœ“ | 3D pull-back effect | -| **BgFade** | Fade | Simple | - | โœ“ | Directional fade transitions | -| **BgFadeBack** | Fade | Medium | - | โœ“ | Scale + fade combination | -| **BgRotate** | Transform | Simple | 2-way | - | Smooth rotation effects | -| **BgSkew** | Transform | Medium | 2-way | โœ“ | Skewing transformation | -| **BgFake3D** | 3D | Complex | - | โœ“ | Multi-layer 3D parallax | -| **BgReveal** | Special | Simple | - | โœ“ | Measurement-only reveal | - -## Target Elements - -Background scroll animations target specific element parts: - -### Data Attributes - -```html - -
- -
- -
- -
-
-
-``` - -### Motion Parts - -- **`BG_LAYER`** - Background container layer -- **`BG_MEDIA`** - Main background media container -- **`BG_IMG`** - Background image element - -## Configuration Patterns - -### Basic Background Animation - -```typescript -const bgScene = getWebAnimation( - '#hero-background', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.3, // 30% of scroll speed - }, - }, - { - trigger: 'view-progress', - element: document.body, - }, -); -``` - -### Speed Control - -```typescript -{ - type: 'BgParallax', - speed: 0.2 // 0.1 = slow, 0.5 = medium, 1.0 = fast -} - -{ - type: 'BgPan', - speed: 0.4 // Panning speed multiplier -} -``` - -### Direction Control - -```typescript -// Two-way directions -{ type: 'BgPan', direction: 'left' | 'right' } -{ type: 'BgRotate', direction: 'clockwise' | 'counter-clockwise' } -{ type: 'BgSkew', direction: 'clockwise' | 'counter-clockwise' } - -// Zoom directions -{ type: 'BgZoom', direction: 'in' | 'out' } -``` - -### Scale and Intensity - -```typescript -{ - type: 'BgZoom', - zoom: 40, // Zoom amount - direction: 'in' // Zoom direction -} - -{ - type: 'BgCloseUp', - scale: 80 // Perspective scale value -} - -{ - type: 'BgSkew', - angle: 20 // Skew angle in degrees -} -``` - -## Detailed Animation Guides - -### ๐ŸŒŠ Parallax & Movement - -#### BgParallax - -**Best for**: Hero backgrounds, landscape imagery, layered compositions - -```typescript -{ - type: 'BgParallax', - speed: 0.3 // Background moves at 30% of scroll speed -} -// Classic parallax: background moves slower than scroll -``` - -#### ImageParallax - -**Best for**: Enhanced parallax with additional control options - -```typescript -{ - type: 'ImageParallax', - speed: 1.5, // Parallax speed multiplier - reverse: false, // Reverse movement direction - isPage: false // Page-level vs component-level -} -// Enhanced parallax with directional and scope control -``` - -#### BgPan - -**Best for**: Wide backgrounds, horizontal movement, cinematic panning - -```typescript -{ - type: 'BgPan', - direction: 'left', // 'left' | 'right' - speed: 0.2 // Panning speed -} -// Horizontal panning movement across viewport -``` - -### ๐Ÿ” Zoom & Scale - -#### BgZoom - -**Best for**: Hero sections, dramatic reveals, immersive experiences - -```typescript -{ - type: 'BgZoom', - direction: 'in', // 'in' | 'out' - zoom: 40 // Zoom intensity -} -// Dynamic zoom with perspective and Y-axis movement -``` - -#### BgCloseUp - -**Best for**: Perspective reveals, depth effects, layered backgrounds - -```typescript -{ - type: 'BgCloseUp', - scale: 80 // Perspective scale amount -} -// Combines perspective zoom with fade on background layer -``` - -#### BgPullBack - -**Best for**: 3D reveal effects, depth transitions - -```typescript -{ - type: 'BgPullBack', - scale: 50 // Pull-back distance -} -// 3D pull-back effect with perspective transformation -``` - -### ๐ŸŽญ Fade & Opacity - -#### BgFade - -**Best for**: Simple background transitions, overlay effects - -```typescript -{ - type: 'BgFade', - range: 'in' // 'in' | 'out' -} -// Directional fade transitions on background layer -``` - -#### BgFadeBack - -**Best for**: Scale + fade combinations, gentle transitions - -```typescript -{ - type: 'BgFadeBack', - scale: 0.7 // Scale amount during fade -} -// Combines scaling and opacity changes -``` - -### ๐Ÿ”„ Rotation & Transform - -#### BgRotate - -**Best for**: Rotating backgrounds, dynamic orientations - -```typescript -{ - type: 'BgRotate', - direction: 'counter-clockwise', // Rotation direction - angle: 22 // Rotation angle -} -// Smooth rotation effect on background media -``` - -#### BgSkew - -**Best for**: Dynamic layouts, creative distortions - -```typescript -{ - type: 'BgSkew', - direction: 'counter-clockwise', // Skew direction - angle: 20 // Skew angle -} -// Skewing transformation with oscillating motion -``` - -### ๐ŸŽจ Advanced 3D - -#### BgFake3D - -**Best for**: Complex 3D effects, multi-layer parallax, showcase backgrounds - -```typescript -{ - type: 'BgFake3D', - stretch: 1.3, // Y-axis stretch amount - zoom: 16.67 // 3D zoom distance -} -// Multi-layer 3D effect with stretch, zoom, and Y-movement -``` - -#### BgReveal - -**Best for**: Measurement-based reveals, container preparations - -```typescript -{ - type: 'BgReveal'; - // No visual effect - performs measurements for other animations -} -// Utility animation for measurement and preparation -``` - -## Advanced Configuration - -### Multi-Layer Effects - -```typescript -// Layer 1: Background parallax -getWebAnimation( - '#bg-layer-1', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.2, - }, - }, - triggerConfig, -); - -// Layer 2: Faster parallax -getWebAnimation( - '#bg-layer-2', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.5, - }, - }, - triggerConfig, -); - -// Layer 3: Zoom effect -getWebAnimation( - '#bg-layer-3', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgZoom', - direction: 'in', - zoom: 30, - }, - }, - triggerConfig, -); -``` - -### Custom Range Control - -```typescript -{ - type: 'BgFade', - range: 'in', - startOffset: { - name: 'cover', - offset: { value: 0, unit: 'percentage' } - }, - endOffset: { - name: 'cover', - offset: { value: 50, unit: 'percentage' } - } -} -``` - -### Measurement Integration - -```typescript -// Auto-measurement for responsive effects -{ - type: 'BgFake3D', - stretch: 1.3, - zoom: 100 / 6 // Calculated based on component size -} -// Animation automatically measures component height -``` - -## Performance Optimization - -### Efficient Background Animations - -```typescript -// Best performance: Transform-only animations -const efficientAnimations = [ - 'BgParallax', // translateY only - 'BgPan', // translateX only - 'BgRotate', // rotate only - 'BgZoom', // perspective + translateZ -]; - -// Moderate performance: Multi-property animations -const moderateAnimations = [ - 'BgFake3D', // Multiple transform layers - 'BgCloseUp', // Perspective + opacity - 'BgSkew', // Skew + position -]; -``` - -### Measurement Batching - -```typescript -// Batch measurements for multiple background animations -import { prepareAnimation } from '@wix/motion'; - -elements.forEach((element) => { - prepareAnimation(element, backgroundAnimationConfig); -}); - -// Then create animations after measurements complete -elements.forEach((element) => { - getWebAnimation(element, backgroundAnimationConfig, trigger); -}); -``` - -## Common Patterns - -### Hero Section Setup - -```html -
-
-
- -
-
-
-
-

Hero Title

-
-
-``` - -```typescript -// Background parallax -getWebAnimation( - '.hero-background', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.3, - }, - }, - { - trigger: 'view-progress', - element: document.body, - }, -); - -// Overlay fade -getWebAnimation( - '.hero-background', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgFade', - range: 'out', - }, - }, - { - trigger: 'view-progress', - element: document.querySelector('#hero'), - }, -); -``` - -### Full-Page Background - -```typescript -const fullPageBg = getWebAnimation( - '#page-background', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ImageParallax', - speed: 1.2, - isPage: true, // Full page scope - }, - }, - { - trigger: 'view-progress', - element: document.body, - }, -); -``` - -### Gallery Background Effects - -```typescript -document.querySelectorAll('.gallery-item').forEach((item) => { - getWebAnimation( - item, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgZoom', - direction: 'in', - zoom: 25, - }, - }, - { - trigger: 'view-progress', - element: item, - }, - ); -}); -``` - -### Complex 3D Scene - -```typescript -const scene3D = getWebAnimation( - '#showcase-bg', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgFake3D', - stretch: 1.4, - zoom: 20, - }, - }, - { - trigger: 'view-progress', - element: document.body, - }, -); -``` - -## Mobile and Responsive Considerations - -### Mobile Optimization - -```typescript -const isMobile = window.innerWidth < 768; - -const bgConfig = { - type: 'ScrubAnimationOptions', - namedEffect: isMobile - ? { type: 'BgFade', range: 'in' } // Simple fade on mobile - : { type: 'BgFake3D', stretch: 1.3 }, // Complex 3D on desktop -}; -``` - -### Reduced Motion Support - -```typescript -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -if (prefersReducedMotion) { - // Use simple, non-moving backgrounds - const config = { type: 'BgReveal' }; -} else { - // Full background animation experience - const config = { type: 'BgParallax', speed: 0.3 }; -} -``` - -### Viewport-Aware Effects - -```typescript -// Scale effects based on viewport size -const viewportMultiplier = Math.min(window.innerWidth / 1920, 1); - -{ - type: 'BgZoom', - zoom: 40 * viewportMultiplier, // Scale zoom for smaller screens - direction: 'in' -} -``` - -## CSS Integration - -### Style Requirements - -```css -.hero-background { - position: relative; - height: 100vh; - overflow: hidden; -} - -[data-motion-part='BG_MEDIA'] { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - will-change: transform; -} - -[data-motion-part='BG_IMG'] { - width: 100%; - height: 100%; - object-fit: cover; -} - -[data-motion-part='BG_LAYER'] { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(to bottom, transparent, rgb(0 0 0 / 0.5)); -} -``` - -### Custom Properties Integration - -```css -/* Motion generates CSS custom properties */ -[data-motion-part='BG_MEDIA'] { - transform: translateY(var(--motion-translate-y, 0)); -} - -/* Component height is automatically measured */ -.hero-section { - --motion-comp-height: 100vh; /* Set by animation */ -} -``` - ---- - -**Complete**: You've explored all animation categories! Return to the [Category Overview](README.md) or check out [Performance Optimization](../guides/performance.md) for advanced usage patterns. diff --git a/packages/motion/docs/categories/entrance-animations.md b/packages/motion/docs/categories/entrance-animations.md deleted file mode 100644 index 2eea5ae3..00000000 --- a/packages/motion/docs/categories/entrance-animations.md +++ /dev/null @@ -1,408 +0,0 @@ -# Entrance Animations - -Entrance animations bring elements into view with impact and style. Perfect for element reveals, page transitions, and progressive disclosure patterns. - -## Overview - -Entrance animations are **time-based** animations designed to reveal content with visual impact. They typically run once when triggered and range from 300-1500ms in duration. All entrance animations start with elements in a hidden or transformed state and animate them to their final visible position. - -### Key Characteristics - -- **Purpose**: Element reveals and transitions -- **Duration**: 300-1500ms (configurable) -- **Trigger**: Manual, intersection observer, or page load -- **Target**: Any DOM element -- **State**: One-time animations (not loops) - -## Animation Categories - -### ๐ŸŒŸ **Fade & Opacity** - -Simple opacity-based reveals for subtle entrances. - -### ๐ŸŽฏ **Directional Movement** - -Elements slide, glide, or float in from specific directions. - -### ๐Ÿ”„ **3D Transforms** - -Sophisticated perspective-based animations with rotation and depth. - -### โšก **Dynamic & Bouncy** - -Spring-based animations with elastic movement and bouncing. - -### ๐ŸŽจ **Shape & Clip** - -Creative reveals using clip-path and shape morphing. - -## Complete Preset Reference - -| Animation | Category | Complexity | Directions | Description | -| -------------- | -------- | ---------- | -------------- | --------------------------------- | -| **FadeIn** | Fade | Simple | - | Clean opacity transition | -| **ArcIn** | 3D | Complex | 4-way | Curved motion with 3D rotation | -| **BounceIn** | Dynamic | Medium | 5-way + center | Spring-based entrance with bounce | -| **SlideIn** | Movement | Medium | 4-way | Slide from edge with clip reveal | -| **GlideIn** | Movement | Medium | 360ยฐ | Smooth directional movement | -| **FlipIn** | 3D | Medium | 4-way | 3D flip rotation entrance | -| **DropIn** | Dynamic | Simple | - | Scale-based drop with easing | -| **ExpandIn** | Dynamic | Medium | 9-way | Scale from specific origin points | -| **FloatIn** | Movement | Simple | 4-way | Gentle floating movement | -| **SpinIn** | Dynamic | Medium | 2-way | Rotation with scale entrance | -| **FoldIn** | 3D | Complex | 4-way | 3D fold with perspective | -| **TurnIn** | 3D | Complex | 4-corner | Complex 3D corner rotation | -| **CurveIn** | 3D | Complex | 4-way | Curved 3D perspective entrance | -| **RevealIn** | Clip | Medium | 4-way | Clean clip-path reveal | -| **TiltIn** | 3D | Complex | 2-way | 3D tilt with clip reveal | -| **WinkIn** | Clip | Medium | 2-way | Accordion-style reveal | -| **ShapeIn** | Clip | Medium | 5 shapes | Morphing shape reveals | -| **ShuttersIn** | Clip | Complex | 4-way | Multi-segment shutter effect | -| **BlurIn** | Filter | Simple | - | Blur-to-focus transition | - -## Configuration Patterns - -### Basic Usage - -```typescript -const animation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 800, - easing: 'easeOut', -}); -``` - -### Directional Controls - -Animations with directional support: - -```typescript -// Four directions -{ type: 'SlideIn', direction: 'left' | 'right' | 'top' | 'bottom' } - -// Eight directions (includes corners) -{ type: 'ExpandIn', direction: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' } - -// Angles (0ยฐ = up, 90ยฐ = right, 180ยฐ = down, 270ยฐ = left) -{ type: 'GlideIn', direction: 45 } - -// Special directions -{ type: 'BounceIn', direction: 'center' } // From center outward -``` - -### Custom Parameters - -Fine-tune animations with specific parameters: - -```typescript -// Distance control -{ - type: 'GlideIn', - direction: 270, - distance: { value: 100, unit: 'px' }, - startFromOffScreen: true -} - -// Scale control -{ - type: 'DropIn', - initialScale: 2.0, -} - -// Shape selection -{ - type: 'ShapeIn', - shape: 'circle' | 'rectangle' | 'diamond' | 'ellipse' | 'window' -} -``` - -## Detailed Animation Guides - -### ๐ŸŒŸ Simple Fades - -#### FadeIn - -**Best for**: Subtle content reveals, overlays, modals - -```typescript -{ - type: 'FadeIn', - // No additional parameters - pure opacity transition -} -``` - -#### BlurIn - -**Best for**: Focus transitions, image reveals - -```typescript -{ - type: 'BlurIn', - blur: 25, // Blur amount in pixels -} -``` - -### ๐ŸŽฏ Directional Movement - -#### SlideIn - -**Best for**: Navigation, panels, content blocks - -```typescript -{ - type: 'SlideIn', - direction: 'left', // 'top', 'right', 'bottom', 'left' - initialTranslate: 1 // Custom distance multiplier -} -``` - -#### GlideIn - -**Best for**: Hero elements, cards, flexible directional reveals - -```typescript -{ - type: 'GlideIn', - direction: 225, // Angle in degrees - distance: { value: 150, unit: 'px' }, // Movement distance - startFromOffScreen: false // Start from viewport edge -} -``` - -#### FloatIn - -**Best for**: Lightweight content, tooltips, notifications - -```typescript -{ - type: 'FloatIn', - direction: 'bottom' // 'top', 'right', 'bottom', 'left' - // Fixed 120px distance, gentle easing -} -``` - -### ๐Ÿ”„ 3D Transforms - -#### ArcIn - -**Best for**: Hero content, featured elements, dramatic reveals - -```typescript -{ - type: 'ArcIn', - direction: 'right', // 'top', 'right', 'bottom', 'left' -} -``` - -#### FlipIn - -**Best for**: Cards, panels, interactive elements - -```typescript -{ - type: 'FlipIn', - direction: 'top', // Flip axis direction - initialRotate: 180 // Custom rotation angle -} -``` - -#### FoldIn - -**Best for**: Paper-like elements, creative reveals - -```typescript -{ - type: 'FoldIn', - direction: 'top', // Fold direction - initialRotate: 90 // Fold angle -} -``` - -### โšก Dynamic & Bouncy - -#### BounceIn - -**Best for**: Buttons, icons, playful elements - -```typescript -{ - type: 'BounceIn', - direction: 'bottom', // 'top', 'right', 'bottom', 'left', 'center' - distanceFactor: 1 // Movement distance -} -``` - -#### DropIn - -**Best for**: Modals, dropdowns, floating elements - -```typescript -{ - type: 'DropIn', - initialScale: 1.6 // Starting scale (>1 = larger) -} -``` - -### ๐ŸŽจ Shape & Clip Effects - -#### ShapeIn - -**Best for**: Creative reveals, masked content, artistic elements - -```typescript -{ - type: 'ShapeIn', - shape: 'circle' // 'circle', 'rectangle', 'diamond', 'ellipse', 'window' -} -``` - -#### RevealIn - -**Best for**: Content blocks, images, clean reveals - -```typescript -{ - type: 'RevealIn', - direction: 'left' // Reveal direction -} -``` - -#### ShuttersIn - -**Best for**: Dynamic reveals, segmented content - -```typescript -{ - type: 'ShuttersIn', - direction: 'right', // Shutter direction - shutters: 12, // Number of segments - staggered: true, // Offset timing -} -``` - -## Timing and Easing - -### Recommended Durations - -- **Quick reveals**: 300-500ms (FadeIn, BlurIn) -- **Standard entrances**: 600-900ms (SlideIn, GlideIn, DropIn) -- **Complex 3D**: 800-1200ms (ArcIn, FlipIn, FoldIn) -- **Bouncy effects**: 1000-1500ms (BounceIn) - -### Easing Recommendations - -```typescript -// Smooth and natural -easing: 'easeOutQuart'; // Quick start, slow end - -// Bouncy and playful -easing: 'backOut'; // Overshoot effect - -// Dramatic and strong -easing: 'elasticOut'; // Spring effect - -// Clean and linear -easing: 'cubicInOut'; // Balanced acceleration -``` - -## Performance Tips - -### Batch Multiple Entrances - -```typescript -// Staggered entrance sequence -const elements = document.querySelectorAll('.reveal-item'); -elements.forEach((el, index) => { - const animation = getWebAnimation(el, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'SlideIn', direction: 'bottom' }, - duration: 600, - delay: index * 100, // 100ms stagger - }); - animation.play(); -}); -``` - -### CSS Mode for Simple Effects - -```typescript -// Use CSS animations for better mobile performance -import { getCSSAnimation } from '@wix/motion'; - -const cssRules = getCSSAnimation('elementId', { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 500, -}); -``` - -## Common Patterns - -### Modal Entrance - -```typescript -const modalAnimation = getWebAnimation(modal, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'DropIn', - }, - duration: 400, - easing: 'backOut', -}); -``` - -### Hero Section Reveal - -```typescript -const heroAnimation = getWebAnimation(heroElement, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'bottom', - }, - duration: 1200, - easing: 'quintOut', -}); -``` - -### Card Grid Stagger - -```typescript -document.querySelectorAll('.card').forEach((card, i) => { - getWebAnimation(card, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'GlideIn', - direction: 45, - distance: { value: 100, unit: 'px' }, - }, - duration: 800, - delay: i * 150, - easing: 'cubicOut', - }).play(); -}); -``` - -## Accessibility Considerations - -### Respect Motion Preferences - -```typescript -const respectsReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -const duration = respectsReducedMotion ? 200 : 800; -const animationType = respectsReducedMotion ? 'FadeIn' : 'BounceIn'; -``` - -### Focus Management - -```typescript -// Ensure focus is visible after animation -await animation.play(); -element.focus(); -``` - ---- - -**Next**: Explore [Ongoing Animations](ongoing-animations.md) for continuous looping effects, or return to the [Category Overview](README.md). diff --git a/packages/motion/docs/categories/mouse-animations.md b/packages/motion/docs/categories/mouse-animations.md deleted file mode 100644 index 25a3f7c9..00000000 --- a/packages/motion/docs/categories/mouse-animations.md +++ /dev/null @@ -1,535 +0,0 @@ -# Mouse Animations - -Interactive pointer-driven effects that respond to mouse movement and hover states. Perfect for creating engaging user interactions, 3D effects, and cursor-following elements. - -## Overview - -Mouse animations are **scrub-based** animations that respond to pointer movement in real-time. They create dynamic, interactive experiences by translating cursor position into element transformations, providing immediate visual feedback and enhancing user engagement. - -### Key Characteristics - -- **Purpose**: Interactivity, hover effects, cursor following -- **Trigger**: Pointer movement (`pointer-move` events) -- **Duration**: Real-time responsiveness with optional transitions -- **Target**: Interactive elements, cards, buttons, media -- **State**: Continuous tracking with smooth transitions - -## Animation Categories - -### ๐ŸŽฏ **Position Tracking** - -Elements that follow or respond to cursor position. - -### ๐Ÿ”„ **3D Transformations** - -Perspective-based effects with depth and rotation. - -### ๐Ÿ“ **Scale & Deformation** - -Size changes and shape deformations based on pointer. - -### โœจ **Visual Effects** - -Blur, transparency, and special visual responses. - -### ๐ŸŽจ **Custom Behaviors** - -Programmable effects for unique interactions. - -## Complete Preset Reference - -| Animation | Category | Complexity | Axis Control | Description | -| ---------------- | --------- | ---------- | ------------ | --------------------------------- | -| **TrackMouse** | Tracking | Simple | โœ“ | Element follows cursor movement | -| **Track3DMouse** | 3D | Medium | โœ“ | 3D tracking with perspective | -| **Tilt3DMouse** | 3D | Medium | - | 3D tilt based on pointer position | -| **SwivelMouse** | 3D | Complex | - | Pivot-point 3D rotation | -| **ScaleMouse** | Scale | Medium | โœ“ | Dynamic scaling on hover | -| **BlobMouse** | Scale | Medium | - | Organic blob-like scaling | -| **SkewMouse** | Deform | Medium | โœ“ | Skew transformation tracking | -| **BlurMouse** | Visual | Complex | - | Blur filter with 3D effects | -| **AiryMouse** | Tracking | Medium | โœ“ | Lightweight floating movement | -| **SpinMouse** | Transform | Simple | โœ“ | Rotation based on movement | -| **BounceMouse** | Tracking | Simple | โœ“ | Elastic cursor following | -| **CustomMouse** | Custom | Variable | - | Programmable mouse effects | - -## Configuration Patterns - -### Basic Mouse Animation - -```typescript -const mouseAnimation = getWebAnimation( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'TrackMouse', - distance: { value: 50, unit: 'px' }, - }, - transitionDuration: 300, - transitionEasing: 'easeOut', - }, - { - trigger: 'pointer-move', - element: containerElement, - }, -); -``` - -### Axis Control - -Restrict movement to specific axes: - -```typescript -{ - type: 'TrackMouse', - axis: 'both', // 'horizontal', 'vertical', 'both' - distance: { value: 100, unit: 'px' } -} -``` - -### Inversion and Direction - -```typescript -{ - type: 'AiryMouse', - inverted: true, // Reverse movement direction - angle: 45, // Movement bias angle - axis: 'horizontal' // Constraint axis -} -``` - -### Transition Control - -```typescript -{ - type: 'ScaleMouse', - transitionDuration: 200, // Smooth transition time - transitionEasing: 'bounce' // 'linear', 'easeOut', 'elastic', 'bounce' -} -``` - -## Detailed Animation Guides - -### ๐ŸŽฏ Position Tracking - -#### TrackMouse - -**Best for**: Cursor followers, floating elements, parallax cursors - -```typescript -{ - type: 'TrackMouse', - distance: { value: 80, unit: 'px' }, // Movement range - axis: 'both', // Movement constraint - inverted: false // Movement direction -} -// Element follows cursor with specified constraints -``` - -#### AiryMouse - -**Best for**: Lightweight hover effects, subtle interactions - -```typescript -{ - type: 'AiryMouse', - distance: { value: 200, unit: 'px' }, // Effect range - angle: 30, // Movement bias - axis: 'both', // Axis constraint - inverted: false // Direction control -} -// Gentle floating movement with directional bias -``` - -#### BounceMouse - -**Best for**: Playful elements, elastic interactions - -```typescript -{ - type: 'BounceMouse', - distance: { value: 80, unit: 'px' }, // Bounce range - axis: 'both' // Movement axis -} -// Elastic cursor following with spring-like behavior -``` - -### ๐Ÿ”„ 3D Transformations - -#### Tilt3DMouse - -**Best for**: Cards, panels, interactive UI elements - -```typescript -{ - type: 'Tilt3DMouse', - angle: 15, // Maximum tilt angle - perspective: 800 // 3D perspective depth -} -// 3D tilting effect following pointer position -``` - -#### Track3DMouse - -**Best for**: 3D showcases, product displays, immersive elements - -```typescript -{ - type: 'Track3DMouse', - distance: { value: 100, unit: 'px' }, // Movement range - angle: 10, // Rotation amount - axis: 'both', // Movement constraint - perspective: 600 // 3D depth -} -// 3D tracking with perspective transformation -``` - -#### SwivelMouse - -**Best for**: Rotating showcases, dial controls, directional indicators - -```typescript -{ - type: 'SwivelMouse', - angle: 25, // Rotation range - perspective: 800, // 3D perspective - pivotAxis: 'center-horizontal' // Pivot point -} -// Pivot-based 3D rotation around specified axis -``` - -### ๐Ÿ“ Scale & Deformation - -#### ScaleMouse - -**Best for**: Buttons, interactive cards, zoom effects - -```typescript -{ - type: 'ScaleMouse', - distance: { value: 150, unit: 'px' }, // Effect range - axis: 'both', // Scale constraint - scale: 1.2, // Maximum scale - scaleDirection: 'up' // 'up' or 'down' -} -// Dynamic scaling based on cursor proximity -``` - -#### BlobMouse - -**Best for**: Organic elements, creative interfaces, morphing shapes - -```typescript -{ - type: 'BlobMouse', - distance: { value: 120, unit: 'px' }, // Effect range - scale: 1.5 // Maximum scale change -} -// Organic blob-like scaling with smooth transitions -``` - -#### SkewMouse - -**Best for**: Creative layouts, artistic elements, dynamic typography - -```typescript -{ - type: 'SkewMouse', - distance: { value: 100, unit: 'px' }, // Effect range - angle: 10, // Maximum skew angle - axis: 'both' // Skew constraint -} -// Skew transformation following pointer movement -``` - -### โœจ Visual Effects - -#### BlurMouse - -**Best for**: Motion effects, speed indicators, dynamic focus - -```typescript -{ - type: 'BlurMouse', - distance: { value: 80, unit: 'px' }, // Movement range - angle: 5, // Blur direction - scale: 0.3, // Scale during blur - blur: 20, // Blur amount (px) - perspective: 600 // 3D perspective -} -// Motion blur with 3D transformation -``` - -#### SpinMouse - -**Best for**: Loading indicators, directional feedback, rotary controls - -```typescript -{ - type: 'SpinMouse', - axis: 'both' // Rotation trigger axis -} -// Rotation based on mouse movement velocity -``` - -### ๐ŸŽจ Custom Behaviors - -#### CustomMouse - -**Best for**: Unique interactions, specialized behaviors, complex effects - -```typescript -{ - type: 'CustomMouse', - // Custom implementation with progress callback - effect: (progress) => { - const { x, y, v, active } = progress(); - // Custom transformation logic - element.style.transform = ` - translate(${x * 100}px, ${y * 100}px) - rotate(${x * 360}deg) - `; - } -} -// Fully programmable mouse interaction -``` - -## Advanced Configuration - -### Pivot Points for 3D Effects - -```typescript -{ - type: 'SwivelMouse', - pivotAxis: 'top', // Pivot from top edge - // Options: 'top', 'bottom', 'left', 'right', - // 'center-horizontal', 'center-vertical' -} -``` - -### Transition Easing Options - -```typescript -{ - transitionEasing: 'elastic', // Spring-like motion - // Options: 'linear', 'easeOut', 'elastic', 'bounce', 'hardBackOut' -} -``` - -### Distance and Range Control - -```typescript -{ - distance: { value: 200, unit: 'px' }, // Pixel-based range - distance: { value: 50, unit: 'percentage' }, // Percentage-based range - distance: { value: 10, unit: 'vh' } // Viewport-based range -} -``` - -## Performance Optimization - -### Efficient Mouse Tracking - -```typescript -// Use transform-only properties for best performance -const efficientAnimations = [ - 'TrackMouse', // transform: translate - 'SpinMouse', // transform: rotate - 'ScaleMouse', // transform: scale - 'AiryMouse', // transform: translate -]; - -// Minimize layout-triggering effects -const heavierAnimations = [ - 'BlurMouse', // Uses filter property - 'SkewMouse', // Complex transform calculations -]; -``` - -### Throttle Pointer Events - -```typescript -// Custom throttling for intensive effects -let throttleTimer: number; -element.addEventListener('pointermove', (e) => { - if (throttleTimer) return; - - throttleTimer = requestAnimationFrame(() => { - mouseAnimation.progress({ - x: e.clientX / window.innerWidth, - y: e.clientY / window.innerHeight, - v: { x: e.movementX, y: e.movementY }, - active: true, - }); - throttleTimer = 0; - }); -}); -``` - -### Touch Device Considerations - -```typescript -// Disable mouse animations on touch devices -const isTouchDevice = window.matchMedia('not (hover: hover)').matches; - -if (isTouchDevice) { - // Use alternative hover states or disable mouse animations - element.addEventListener('pointerenter', handleTouchInteraction); -} else { - // Enable full mouse animation experience - const mouseAnimation = getWebAnimation(element, mouseConfig, trigger); -} -``` - -## Common Patterns - -### Interactive Card Hover - -```typescript -const cardHover = getWebAnimation( - card, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 12, - perspective: 1000, - }, - transitionDuration: 200, - transitionEasing: 'easeOut', - }, - { - trigger: 'pointer-move', - element: card, - }, -); - -// Enhanced with additional effects -card.addEventListener('pointerenter', () => { - card.style.boxShadow = '0 20px 40px rgb(0 0 0 / 0.1)'; -}); - -card.addEventListener('pointerleave', () => { - card.style.boxShadow = ''; -}); -``` - -### Cursor Following Element - -```typescript -const follower = getWebAnimation( - cursorElement, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'TrackMouse', - distance: { value: 20, unit: 'px' }, - axis: 'both', - }, - transitionDuration: 100, - transitionEasing: 'easeOut', - }, - { - trigger: 'pointer-move', - element: document.body, - }, -); -``` - -### 3D Product Showcase - -```typescript -const productShowcase = getWebAnimation( - product, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Track3DMouse', - distance: { value: 50, unit: 'px' }, - angle: 15, - perspective: 800, - }, - transitionDuration: 300, - transitionEasing: 'easeOut', - }, - { - trigger: 'pointer-move', - element: productContainer, - }, -); -``` - -### Interactive Button Effects - -```typescript -const buttonEffect = getWebAnimation( - button, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ScaleMouse', - distance: { value: 100, unit: 'px' }, - scale: 1.05, - scaleDirection: 'up', - }, - transitionDuration: 150, - transitionEasing: 'bounce', - }, - { - trigger: 'pointer-move', - element: button, - }, -); - -// Combined with color transitions -button.addEventListener('mouseenter', () => { - button.style.backgroundColor = '#007bff'; -}); -``` - -## Accessibility and UX - -### Respect Motion Preferences - -```typescript -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -if (prefersReducedMotion) { - // Disable mouse animations or use minimal effects - console.log('Mouse animations disabled for accessibility'); -} else { - // Enable full mouse animation experience - initMouseAnimations(); -} -``` - -### Performance Boundaries - -```typescript -// Limit concurrent mouse animations -const MAX_MOUSE_ANIMATIONS = 5; -let activeMouseAnimations = 0; - -function createMouseAnimation(element, config) { - if (activeMouseAnimations >= MAX_MOUSE_ANIMATIONS) { - console.warn('Max mouse animations reached'); - return null; - } - - activeMouseAnimations++; - const animation = getWebAnimation(element, config); - - return { - ...animation, - cancel: () => { - animation.cancel(); - activeMouseAnimations--; - }, - }; -} -``` - ---- - -**Next**: Explore [Background Scroll Animations](background-scroll-animations.md) for specialized background effects, or return to the [Category Overview](README.md). diff --git a/packages/motion/docs/categories/ongoing-animations.md b/packages/motion/docs/categories/ongoing-animations.md deleted file mode 100644 index c0f26f00..00000000 --- a/packages/motion/docs/categories/ongoing-animations.md +++ /dev/null @@ -1,466 +0,0 @@ -# Ongoing Animations - -Continuous looping animations that add life and draw attention to elements. Perfect for call-to-action emphasis, loading states, and ambient motion. - -## Overview - -Ongoing animations are **time-based** looping animations designed to create continuous movement and draw user attention. They typically run with infinite iterations and use alternating or repeating patterns. Duration ranges from 1-4 seconds with automatic looping. - -### Key Characteristics - -- **Purpose**: Attention, emphasis, ambient motion -- **Duration**: 1-4 seconds per cycle -- **Iterations**: Infinite loops (configurable) -- **Trigger**: Manual start/stop control -- **Target**: Buttons, icons, decorative elements -- **State**: Continuous until stopped - -## Animation Categories - -### ๐Ÿ’“ **Rhythmic Scaling** - -Breathing, pulsing, and organic size changes. - -### ๐Ÿƒ **Movement & Position** - -Translation-based animations with directional flow. - -### ๐Ÿ”„ **Rotation & Spin** - -Circular motion and rotation effects. - -### โšก **Dynamic Effects** - -Complex multi-property animations with elastic movement. - -### โœจ **Visual Effects** - -Opacity, visibility, and special visual transitions. - -## Complete Preset Reference - -| Animation | Category | Complexity | Directions | Description | -| ----------- | -------- | ---------- | ---------- | ----------------------------- | -| **Pulse** | Rhythmic | Simple | - | Smooth scale breathing effect | -| **Breathe** | Rhythmic | Medium | 3-way | Organic movement with scaling | -| **Bounce** | Dynamic | Medium | - | Vertical bouncing motion | -| **Spin** | Rotation | Simple | 2-way | Continuous rotation | -| **Wiggle** | Movement | Medium | - | Random shake movement | -| **Poke** | Movement | Medium | 4-way | Directional poking motion | -| **Flash** | Visual | Simple | - | Opacity blinking effect | -| **Swing** | Movement | Complex | 4-way | Pendulum swinging motion | -| **Flip** | Rotation | Medium | 2-way | 3D flip rotation | -| **Rubber** | Dynamic | Medium | - | Elastic scaling effect | -| **Fold** | Rotation | Complex | 4-way | 3D folding animation | -| **Jello** | Dynamic | Medium | - | Gelatinous wobble effect | -| **Cross** | Movement | Complex | 8-way | Multi-directional crossing | - -\*Currently disabled in production - -## Configuration Patterns - -### Basic Looping - -```typescript -const animation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'Pulse' }, - duration: 2000, - iterations: Infinity, // Loop forever - alternate: true, // Ping-pong effect -}); -``` - -### Intensity Control - -Fine-tune effect strength with intensity values: - -```typescript -{ - type: 'Bounce', - intensity: 0.8 // Effect strength multiplier -} -``` - -### Directional Support - -Many animations support directional parameters: - -```typescript -// Four directions -{ type: 'Poke', direction: 'top' | 'right' | 'bottom' | 'left' } - -// Eight directions (includes corners) -{ type: 'Cross', direction: 'top-left' | 'bottom-right' | /* etc */ } - -// Two-way rotation -{ type: 'Spin', direction: 'clockwise' | 'counter-clockwise' } - -// Axis selection -{ type: 'Breathe', direction: 'vertical' | 'horizontal' | 'center' } -``` - -## Detailed Animation Guides - -### ๐Ÿ’“ Rhythmic Scaling - -#### Pulse - -**Best for**: Call-to-action buttons, notifications, heartbeat effects - -```typescript -{ - type: 'Pulse', - intensity: 1.0 // Multiplier (0.1-2.0) -} -// Creates smooth scale from 1.0 to 1.1 and back -``` - -#### Breathe - -**Best for**: Organic elements, meditation apps, ambient motion - -```typescript -{ - type: 'Breathe', - direction: 'vertical', // Movement direction - distance: { value: 10, unit: 'px' } // Movement amount -} -// Combines gentle translation with subtle scaling -``` - -### ๐Ÿƒ Movement & Position - -#### Wiggle - -**Best for**: Error states, playful elements, attention grabbing - -```typescript -{ - type: 'Wiggle', - intensity: 0.5 // Movement amount -} -// Random horizontal shaking motion -``` - -#### Poke - -**Best for**: Interactive hints, directional cues, button emphasis - -```typescript -{ - type: 'Poke', - direction: 'right', // Poke direction - intensity: 1.2 // Effect multiplier -} -// Short directional movement and return -``` - -#### Cross - -**Best for**: Complex UI elements, dashboard widgets - -```typescript -{ - type: 'Cross', - direction: 'top-right' // Eight-way directional movement -} -// Crossing movement pattern to corners -``` - -### ๐Ÿ”„ Rotation & Spin - -#### Spin - -**Best for**: Loading indicators, refresh buttons, processing states - -```typescript -{ - type: 'Spin', - direction: 'clockwise' // 'clockwise' | 'counter-clockwise' -} -// Continuous smooth rotation -``` - -#### Flip - -**Best for**: Cards, panels, toggle states - -```typescript -{ - type: 'Flip', - direction: 'horizontal' // 'horizontal' | 'vertical' -} -// 3D flip rotation effect -``` - -#### Fold - -**Best for**: Paper-like elements, origami effects - -```typescript -{ - type: 'Fold', - direction: 'top', // Fold axis direction - angle: 45 // Custom fold angle -} -// 3D folding motion with perspective -``` - -### โšก Dynamic Effects - -#### Bounce - -**Best for**: Playful elements, game UI, spring animations - -```typescript -{ - type: 'Bounce', - intensity: 1.5 // Effect multiplier -} -// Vertical bouncing with gravity simulation -``` - -#### Rubber - -**Best for**: Elastic elements, cartoon-style effects - -```typescript -{ - type: 'Rubber', - intensity: 0.8 // Effect strength -} -// Elastic stretching and snapping -``` - -#### Jello - -**Best for**: Gelatinous effects, organic motion, fun elements - -```typescript -{ - type: 'Jello', - intensity: 1.0 // Effect multiplier -} -// Multi-directional wobbling motion -``` - -#### Swing - -**Best for**: Hanging elements, pendulum effects, natural motion - -```typescript -{ - type: 'Swing', - swing: 15, // Custom swing angle (degrees) - direction: 'left' // Swing axis point -} -// Pendulum-style swinging motion -``` - -### โœจ Visual Effects - -#### Flash - -**Best for**: Alerts, notifications, blinking indicators - -```typescript -{ - type: 'Flash'; - // Simple opacity blinking with no additional parameters -} -// Quick opacity flash effect -``` - -## Timing and Control - -### Recommended Durations - -- **Fast attention**: 800-1200ms (Flash, Pulse) -- **Standard rhythm**: 1500-2500ms (Breathe, Bounce, Wiggle) -- **Slow ambient**: 3000-4000ms (Swing, Cross) -- **Loading states**: 1000-1500ms (Spin) - -### Loop Control - -```typescript -// Infinite looping (default) -iterations: Infinity; - -// Limited repetitions -iterations: 5; - -// Ping-pong effect -alternate: true; - -// Forward only -alternate: false; -``` - -### Start/Stop Control - -```typescript -const animation = getWebAnimation(element, options); - -// Start animation -await animation.play(); - -// Pause animation -animation.pause(); - -// Resume animation -animation.play(); - -// Stop and reset -animation.cancel(); -``` - -## Performance Optimization - -### CSS Mode for Simple Effects - -```typescript -// Use CSS animations for better performance -import { getCSSAnimation } from '@wix/motion'; - -const cssRules = getCSSAnimation('elementId', { - type: 'TimeAnimationOptions', - namedEffect: { type: 'Pulse' }, - duration: 2000, - iterations: Infinity, -}); -``` - -### Respect Reduced Motion - -```typescript -const respectsReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -if (respectsReducedMotion) { - // Disable ongoing animations or use gentler alternatives - const config = { type: 'Flash' }; // Instead of intense animations -} else { - const config = { type: 'Bounce' }; -} -``` - -## Common Patterns - -### Button Call-to-Action - -```typescript -const ctaAnimation = getWebAnimation(button, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - }, - duration: 1500, - iterations: Infinity, - alternate: true, -}); - -// Start on hover, stop on blur -button.addEventListener('mouseenter', () => ctaAnimation.play()); -button.addEventListener('mouseleave', () => ctaAnimation.pause()); -``` - -### Loading Spinner - -```typescript -const loadingAnimation = getWebAnimation(spinner, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Spin', - direction: 'clockwise', - }, - duration: 1000, - iterations: Infinity, -}); - -// Control with loading state -function setLoading(isLoading) { - if (isLoading) { - loadingAnimation.play(); - } else { - loadingAnimation.cancel(); - } -} -``` - -### Attention-Seeking Element - -```typescript -const attentionAnimation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Wiggle', - intensity: 0.6, - }, - duration: 500, - iterations: 3, // Limited repetitions - alternate: true, -}); - -// Trigger attention -attentionAnimation.play(); -``` - -### Ambient Background Motion - -```typescript -const ambientAnimation = getWebAnimation(backgroundElement, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Breathe', - direction: 'center', - distance: { value: 5, unit: 'px' }, - }, - duration: 4000, - iterations: Infinity, - alternate: true, -}); - -// Continuous ambient motion -ambientAnimation.play(); -``` - -## Battery and Performance Considerations - -### Mobile Optimization - -```typescript -// Check if device likely has battery constraints -const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( - navigator.userAgent, -); - -if (isMobile) { - // Use lighter animations - config = { type: 'Flash' }; -} else { - // Full-featured animations - config = { type: 'Rubber' }; -} -``` - -### Intersection Observer Integration - -```typescript -// Only animate visible elements -const observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - const animation = entry.target.animation; - if (entry.isIntersecting) { - animation.play(); - } else { - animation.pause(); - } - }); -}); - -elements.forEach((el) => observer.observe(el)); -``` - ---- - -**Next**: Explore [Scroll Animations](scroll-animations.md) for scroll-driven effects, or return to the [Category Overview](README.md). diff --git a/packages/motion/docs/categories/scroll-animations.md b/packages/motion/docs/categories/scroll-animations.md deleted file mode 100644 index 0b9dff87..00000000 --- a/packages/motion/docs/categories/scroll-animations.md +++ /dev/null @@ -1,588 +0,0 @@ -# Scroll Animations - -Scroll-driven effects that respond to viewport position for immersive storytelling and progressive disclosure. Perfect for creating engaging scroll experiences with parallax, reveals, and synchronized motion. - -## Overview - -Scroll animations are **scrub-based** animations that respond to scroll position using the ViewTimeline API or scroll event listeners. They create smooth, synchronized effects tied to the user's scroll progress, enabling immersive storytelling and progressive content reveals. - -### Key Characteristics - -- **Purpose**: Storytelling, parallax, progressive disclosure -- **Trigger**: Scroll position and viewport intersection -- **Duration**: Based on scroll distance, not time -- **Target**: Content blocks, media, layout elements -- **State**: Progress-driven (0-100% based on scroll) - -## Animation Categories - -### ๐Ÿ“ **Transform-Based** - -Position, scale, and rotation effects synchronized to scroll. - -### ๐ŸŽญ **Opacity & Visibility** - -Fade and reveal effects triggered by scroll position. - -### ๐Ÿ“บ **3D Perspective** - -Complex 3D transformations with depth and perspective. - -### โœ‚๏ธ **Clip & Shape** - -Creative reveals using clip-path and shape morphing. - -### ๐ŸŽข **Specialized Motion** - -Complex movement patterns and specialized scroll behaviors. - -## Complete Preset Reference - -| Animation | Category | Complexity | Range Support | Directions | Description | -| ------------------ | --------- | ---------- | ------------- | ---------- | --------------------------- | -| **ParallaxScroll** | Transform | Simple | โœ“ | - | Classic parallax movement | -| **FadeScroll** | Opacity | Simple | โœ“ | - | Opacity changes on scroll | -| **MoveScroll** | Transform | Medium | โœ“ | 360ยฐ | Directional movement | -| **GrowScroll** | Transform | Medium | โœ“ | 9-way | Scale from specific origins | -| **ShrinkScroll** | Transform | Medium | โœ“ | 9-way | Scale shrinking effects | -| **RevealScroll** | Clip | Medium | โœ“ | 4-way | Clean clip-path reveals | -| **SlideScroll** | Transform | Medium | โœ“ | 4-way | Sliding movement effects | -| **BlurScroll** | Filter | Simple | โœ“ | - | Blur-to-focus transitions | -| **ArcScroll** | 3D | Complex | โœ“ | 2-way | Curved 3D motion paths | -| **FlipScroll** | 3D | Medium | โœ“ | 2-way | 3D flip rotations | -| **SpinScroll** | Transform | Medium | โœ“ | 2-way | Rotation with scaling | -| **Spin3dScroll** | 3D | Complex | โœ“ | - | 3D rotation with depth | -| **TiltScroll** | 3D | Complex | โœ“ | 2-way | Perspective tilting effects | -| **TurnScroll** | 3D | Complex | โœ“ | 2-way | Complex 3D turning | -| **PanScroll** | Transform | Medium | โœ“ | 2-way | Horizontal panning motion | -| **SkewPanScroll** | Transform | Medium | โœ“ | 2-way | Skewed panning effects | -| **StretchScroll** | Transform | Medium | โœ“ | - | Vertical stretching effects | -| **ShapeScroll** | Clip | Complex | โœ“ | 5 shapes | Morphing shape reveals | -| **ShuttersScroll** | Clip | Complex | โœ“ | 4-way | Multi-segment reveals | - -## Configuration Patterns - -### Basic Scroll Animation - -```typescript -const scene = getScrubScene( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { type: 'ParallaxScroll', speed: 0.5 }, - startOffset: { name: 'cover', offset: { value: 0, unit: 'percentage' } }, - endOffset: { name: 'cover', offset: { value: 100, unit: 'percentage' } }, - }, - { - trigger: 'view-progress', - element: document.body, - }, -); -``` - -### Range Control - -Most scroll animations support range modifiers: - -```typescript -// In - Animation plays as element enters viewport -{ type: 'FadeScroll', range: 'in' } - -// Out - Animation plays as element exits viewport -{ type: 'FadeScroll', range: 'out' } - -// Continuous - Animation spans full scroll interaction -{ type: 'FadeScroll', range: 'continuous' } -``` - -### Speed and Intensity - -Fine-tune animation responsiveness: - -```typescript -// Parallax speed (0.1 = slow, 1.0 = normal, 2.0 = fast) -{ type: 'ParallaxScroll', speed: 0.3 } - -// Movement distance -{ - type: 'MoveScroll', - distance: { value: 200, unit: 'px' }, - angle: 45 -} - -// Scale amount -{ type: 'GrowScroll', scale: 1.5 } -``` - -### Directional Controls - -```typescript -// Four directions -{ type: 'RevealScroll', direction: 'left' | 'right' | 'top' | 'bottom' } - -// Nine positions (includes corners + center) -{ type: 'GrowScroll', direction: 'top-left' | 'center' | 'bottom-right' } - -// Angle-based (0ยฐ = up, 90ยฐ = right, etc.) -{ type: 'MoveScroll', angle: 225 } - -// Axis selection -{ type: 'ArcScroll', direction: 'vertical' | 'horizontal' } -``` - -## Detailed Animation Guides - -### ๐Ÿ“ Transform-Based - -#### ParallaxScroll - -**Best for**: Background elements, hero sections, layered content - -```typescript -{ - type: 'ParallaxScroll', - speed: 0.5, // Movement speed relative to scroll - range: 'continuous' // Full scroll interaction -} -// Element moves at 50% of scroll speed for smooth parallax -``` - -#### MoveScroll - -**Best for**: Directional reveals, dynamic positioning - -```typescript -{ - type: 'MoveScroll', - angle: 225, // Movement direction (degrees) - distance: { value: 300, unit: 'px' }, // Movement amount - range: 'in' // Animation timing -} -// Moves element along specified angle and distance -``` - -#### GrowScroll / ShrinkScroll - -**Best for**: Scale-based reveals, size transitions - -```typescript -{ - type: 'GrowScroll', - direction: 'center', // Scale origin point - scale: 1.8, // Target scale value - range: 'in', // When to animate - speed: 0.5 // Y-axis travel amount -} -// Scales element from/to specified size with optional Y movement -``` - -#### SlideScroll - -**Best for**: Content blocks, panel reveals - -```typescript -{ - type: 'SlideScroll', - direction: 'left', // Slide direction - range: 'in' // Animation timing -} -// Slides element from specified direction -``` - -### ๐ŸŽญ Opacity & Visibility - -#### FadeScroll - -**Best for**: Simple reveals, content blocks, images - -```typescript -{ - type: 'FadeScroll', - range: 'in', // Fade timing - opacity: 0.8 // Target opacity value -} -// Simple opacity transition based on scroll position -``` - -#### BlurScroll - -**Best for**: Focus transitions, image reveals, depth effects - -```typescript -{ - type: 'BlurScroll', - blur: 20, // Blur amount in pixels - range: 'in' // Animation timing -} -// Blur-to-focus or focus-to-blur transition -``` - -### ๐Ÿ“บ 3D Perspective - -#### ArcScroll - -**Best for**: Hero content, featured elements, dramatic reveals - -```typescript -{ - type: 'ArcScroll', - direction: 'horizontal', // 'horizontal' | 'vertical' - range: 'in' // Animation timing -} -// 3D curved motion with perspective transformation -``` - -#### FlipScroll - -**Best for**: Cards, panels, interactive elements - -```typescript -{ - type: 'FlipScroll', - direction: 'vertical', // Flip axis - rotate: 180, // Rotation amount (degrees) - range: 'in' // Animation timing -} -// 3D flip rotation effect -``` - -#### TiltScroll - -**Best for**: Interactive cards, 3D UI elements - -```typescript -{ - type: 'TiltScroll', - direction: 'left', // Tilt direction - distance: 15, // Tilt angle - range: 'continuous' // Animation timing -} -// Perspective-based tilting with Y-axis movement -``` - -#### Spin3dScroll - -**Best for**: Loading states, decorative elements, logos - -```typescript -{ - type: 'Spin3dScroll', - rotate: 360, // Rotation amount - range: 'continuous', // Animation timing - speed: 0.8 // Additional Y movement -} -// 3D rotation with depth and optional translation -``` - -### โœ‚๏ธ Clip & Shape Effects - -#### RevealScroll - -**Best for**: Content blocks, images, text reveals - -```typescript -{ - type: 'RevealScroll', - direction: 'right', // Reveal direction - range: 'in' // Animation timing -} -// Clean directional reveal using clip-path -``` - -#### ShapeScroll - -**Best for**: Creative reveals, artistic elements, featured content - -```typescript -{ - type: 'ShapeScroll', - shape: 'circle', // 'circle', 'rectangle', 'diamond', 'ellipse', 'window' - range: 'in', // Animation timing - intensity: 1.2 // Scale multiplier -} -// Morphing shape-based reveals -``` - -#### ShuttersScroll - -**Best for**: Dynamic reveals, segmented content, galleries - -```typescript -{ - type: 'ShuttersScroll', - direction: 'right', // Shutter direction - shutters: 8, // Number of segments - staggered: true, // Offset timing - range: 'in' // Animation timing -} -// Multi-segment shutter reveal effect -``` - -### ๐ŸŽข Specialized Motion - -#### PanScroll / SkewPanScroll - -**Best for**: Wide content, horizontal scrolling sections - -```typescript -{ - type: 'PanScroll', - direction: 'left', // Pan direction - distance: { value: 100, unit: 'px' }, // Pan amount - startFromOffScreen: false, // Start position - range: 'continuous' // Animation timing -} - -{ - type: 'SkewPanScroll', - direction: 'right', // Pan + skew direction - skew: 15, // Skew angle - range: 'continuous' // Animation timing -} -``` - -#### StretchScroll - -**Best for**: Elastic elements, rubber-band effects - -```typescript -{ - type: 'StretchScroll', - stretch: 1.3, // Stretch amount - range: 'in' // Animation timing -} -// Vertical stretching with elastic feel -``` - -#### TurnScroll - -**Best for**: Complex 3D interactions, showcase elements - -```typescript -{ - type: 'TurnScroll', - direction: 'left', // Turn direction - spin: 'clockwise', // Rotation direction - scale: 1.2, // Scale during turn - range: 'continuous' // Animation timing -} -// Complex 3D turning with scale and translation -``` - -## Viewport and Offset Control - -### Intersection Ranges - -```typescript -// Named viewport ranges -startOffset: { name: 'cover' } // Element covers viewport -startOffset: { name: 'contain' } // Element contained in viewport -startOffset: { name: 'entry' } // Element entering viewport -startOffset: { name: 'exit' } // Element leaving viewport - -// With percentage offsets -startOffset: { - name: 'cover', - offset: { value: 20, unit: 'percentage' } -} - -// With pixel offsets -endOffset: { - name: 'exit', - offset: { value: 100, unit: 'px' } -} -``` - -### Custom Scroll Ranges - -```typescript -// Start animation when element is 20% visible -const scene = getScrubScene(element, animationOptions, { - trigger: 'view-progress', - startOffset: { - name: 'entry', - offset: { value: 20, unit: 'percentage' }, - }, - endOffset: { - name: 'exit', - offset: { value: 0, unit: 'percentage' }, - }, -}); -``` - -## Performance Optimization - -### ViewTimeline API Usage - -```typescript -// Automatic detection and fallback -if (window.ViewTimeline) { - // Use native ViewTimeline for best performance - const animation = getWebAnimation(element, options, trigger); -} else { - // use a polyfill library - // Consider lighter animations for this case - const scene = getScrubScene(element, options, trigger); -} -``` - -### Efficient Scroll Animations - -```typescript -// Use transform-only properties for best performance -const efficientAnimations = [ - 'ParallaxScroll', // transform: translateY - 'MoveScroll', // transform: translate - 'SpinScroll', // transform: rotate + scale - 'FadeScroll', // opacity -]; - -// Avoid layout-triggering properties in scroll animations -const heavierAnimations = [ - 'StretchScroll', // Uses scaling that may demand extensive memory usage for large media - 'ShapeScroll', // Clip-path can be expensive if not hardware accelerated by the browser -]; -``` - -## Common Patterns - -### Hero Section Parallax - -```typescript -const heroBackground = getScrubScene( - '#hero-bg', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 0.3, // Slow background movement - }, - }, - { - trigger: 'view-progress', - element: document.body, - }, -); - -const heroContent = getScrubScene( - '#hero-content', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'FadeScroll', - range: 'out', - opacity: 0.8, - }, - }, - { - trigger: 'view-progress', - element: document.querySelector('#hero'), - }, -); -``` - -### Staggered Content Reveals - -```typescript -document.querySelectorAll('.reveal-item').forEach((item, index) => { - getScrubScene( - item, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'RevealScroll', - direction: 'bottom', - range: 'in', - }, - // Stagger timing with offset - startOffset: { - name: 'entry', - offset: { value: index * 10, unit: 'percentage' }, - }, - }, - { - trigger: 'view-progress', - element: item, - }, - ); -}); -``` - -### Gallery Scroll Effects - -```typescript -const galleryItems = document.querySelectorAll('.gallery-item'); -galleryItems.forEach((item, i) => { - getScrubScene( - item, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'GrowScroll', - direction: 'center', - range: 'in', - }, - }, - { - trigger: 'view-progress', - element: item, - }, - ); -}); -``` - -### Continuous Scroll Story - -```typescript -// Long-form content with continuous effects -const storyContainer = getScrubScene( - '#story', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'MoveScroll', - angle: 90, // Vertical movement - distance: { value: 500, unit: 'px' }, - range: 'continuous', - }, - }, - { - trigger: 'view-progress', - element: document.body, - }, -); -``` - -## Accessibility and UX - -### Respect Motion Preferences - -```typescript -const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - -if (prefersReducedMotion) { - // Use gentler scroll effects or disable entirely - const config = { type: 'FadeScroll', range: 'in' }; -} else { - const config = { type: 'ArcScroll', direction: 'horizontal' }; -} -``` - -### Progressive Enhancement - -```typescript -// Check for scroll animation support -const supportsScrollAnimations = 'ViewTimeline' in window; - -if (supportsScrollAnimations) { - // Enable full scroll animation experience - initScrollAnimations(); -} else { - // Fallback to simple reveals or static content - initStaticFallbacks(); -} -``` - ---- - -**Next**: Explore [Mouse Animations](mouse-animations.md) for interactive pointer effects, or return to the [Category Overview](README.md). diff --git a/packages/motion/docs/core-concepts.md b/packages/motion/docs/core-concepts.md index 62767255..d86f8676 100644 --- a/packages/motion/docs/core-concepts.md +++ b/packages/motion/docs/core-concepts.md @@ -1,412 +1,142 @@ # Core Concepts -Understanding the fundamental concepts of Wix Motion will help you make the most of its 82+ animation presets and powerful API. +The mental model behind `@wix/motion`: how it decides what to build from the options you pass in, and how effects, triggers, and easings fit together. Start with [Getting Started](./getting-started.md) if you haven't run your first animation yet. -## Animation Architecture +## The options shape -Wix Motion is built around several key concepts that work together to provide a flexible and powerful animation system. - -### Animation Types - -There are two main animation types in Wix Motion: - -#### 1. Time-Based Animations (`TimeAnimationOptions`) - -These are traditional animations that run for a specific duration: - -- **Entrance animations** - Elements appearing on screen -- **Ongoing animations** - Continuous looping effects +`AnimationOptions` is a union of two shapes โ€” there is **no top-level `type` field** to discriminate between them. The engine branches structurally: on which effect-definition field is present (`keyframeEffect` / `namedEffect` / `customEffect`) and on whether a `trigger` argument was passed. ```typescript -{ - type: 'TimeAnimationOptions', +// โœ… correct +getWebAnimation(element, { namedEffect: { type: 'FadeIn' }, - duration: 1000, // Duration in milliseconds - easing: 'easeOut', // Timing function - iterations: 1, // How many times to repeat - delay: 0 // Start delay -} -``` - -#### 2. Scrub-Based Animations (`ScrubAnimationOptions`) - -These animations are driven by external progress (scroll, mouse movement): - -- **Scroll animations** - Respond to scroll position -- **Mouse animations** - Follow pointer movement -- **Background scroll animations** - Specialized for background media - -```typescript -{ - type: 'ScrubAnimationOptions', - namedEffect: { type: 'ParallaxScroll', speed: 0.5 }, - startOffset: { name: 'cover', offset: { value: 0, unit: 'percentage' } }, - endOffset: { name: 'cover', offset: { value: 100, unit: 'percentage' } } -} -``` - -## Animation Categories - -### ๐ŸŽญ Entrance Animations - -**Purpose**: Reveal elements with impact and style -**Duration**: Typically 300-1500ms -**Use Cases**: Page loads, modal openings, content reveals - -**Common Patterns**: - -- **Directional**: `top`, `right`, `bottom`, `left`, `center` -- **Scale-based**: Start from different sizes -- **3D transforms**: Perspective and rotation effects - -```typescript -// Example: Arc entrance from the right -{ - type: 'TimeAnimationOptions', - namedEffect: { - type: 'ArcIn', - direction: 'right' - }, - duration: 800 -} -``` - -### ๐Ÿ”„ Ongoing Animations - -**Purpose**: Continuous effects for attention and life -**Duration**: Usually 1-4 seconds with infinite iterations -**Use Cases**: Call-to-action emphasis, breathing UI, ambient motion - -**Common Patterns**: - -- **Intensity control**: Scale the effect strength -- **Bidirectional**: Many support `alternate` for back-and-forth motion + duration: 1000, +}); -```typescript -// Example: Gentle pulsing effect -{ +// โŒ wrong โ€” there is no top-level `type` on the options object +getWebAnimation(element, { type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - intensity: 0.8 - }, - duration: 2000, - iterations: Infinity, - alternate: true -} -``` - -### ๐Ÿ“œ Scroll Animations - -**Purpose**: Scroll-synchronized effects for storytelling -**Triggers**: `view-progress` with ViewTimeline API -**Use Cases**: Parallax, reveal-on-scroll, progressive disclosure - -**Common Patterns**: - -- **Range control**: `in`, `out`, `continuous` -- **Speed modifiers**: Control animation rate relative to scroll -- **Viewport binding**: Animations tied to element visibility - -```typescript -// Example: Parallax background movement -{ - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 0.3, - range: 'continuous' - } -} -``` - -### ๐Ÿ–ฑ๏ธ Mouse Animations - -**Purpose**: Interactive pointer-driven effects -**Triggers**: `pointer-move` events -**Use Cases**: Hover effects, cursor following, 3D interactions - -**Common Patterns**: - -- **Distance control**: How far effects extend from pointer -- **Axis constraints**: `horizontal`, `vertical`, `both` -- **Inversion**: Opposite direction movement - -```typescript -// Example: 3D tilt following mouse -{ - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'Tilt3DMouse', - angle: 15, - perspective: 800 - } -} -``` - -### ๐Ÿ–ผ๏ธ Background Scroll Animations - -**Purpose**: Specialized effects for background media -**Targets**: Elements with `data-motion-part` attributes -**Use Cases**: Hero sections, full-screen backgrounds, video overlays - -**Common Patterns**: - -- **Multi-layer**: Target different background layers -- **Measurement-aware**: Auto-calculate component dimensions -- **Perspective effects**: Advanced 3D transformations - -```typescript -// Example: Background zoom on scroll -{ - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgZoom', - direction: 'in', - zoom: 40 - } -} + namedEffect: { type: 'FadeIn' }, + duration: 1000, +}); ``` -## Configuration Patterns +`namedEffect` itself **does** have a `type` field โ€” that's the registered preset name. It's only the top-level options object that has none. -### Named Effects vs Custom Effects +**Time-based options** (used when no `trigger` is passed): `duration` (ms), `delay`, `endDelay`, `easing`, `iterations` (`0` โ‡’ `Infinity`, `undefined` โ‡’ `1`), `alternate`, `fill`, `reversed` โ€” plus one of the three effect fields below. -#### Named Effects (Recommended) +**Scrub options** (used with a `view-progress` or `pointer-move` trigger): `startOffset` / `endOffset` (scroll range), `playbackRate`, `transitionDuration` / `transitionDelay` / `transitionEasing` / `centeredToTarget` (pointer smoothing), `easing`, `iterations`, `fill`, `alternate`, `reversed` โ€” plus one of the three effect fields. Note `duration` here is a `{ value, unit }` length/percentage, not milliseconds. -Use predefined animation presets: +## The three effect-definition modes -```typescript -namedEffect: { - type: 'BounceIn', - direction: 'bottom' -} -``` +Exactly one of these fields tells the engine what to animate: -#### Custom Keyframe Effects - -Define your own keyframes: - -```typescript -keyframeEffect: { - name: 'myCustomAnimation', - keyframes: [ - { opacity: 0, transform: 'scale(0)' }, - { opacity: 1, transform: 'scale(1)' } - ] -} -``` +1. **`keyframeEffect: { name, keyframes }`** โ€” inline WAAPI/CSS keyframes. Zero registration. -#### Custom Script Effects + ```typescript + { keyframeEffect: { name: 'fade-up', keyframes: [{ opacity: 0 }, { opacity: 1 }] } } + ``` -Full programmatic control: +2. **`customEffect: (element, progress) => void`** โ€” a per-frame JS callback, run on a `requestAnimationFrame` loop. This is the only programmatic mode. On cancel, the callback is invoked with `progress === null`, so handle it: -```typescript -customEffect: { - ranges: [ - { name: 'opacity', min: 0, max: 1 }, - { name: 'scale', min: 0, max: 1.2 }, - ]; -} -``` + ```typescript + { + customEffect: (element, progress) => { + if (progress === null) { + // cancelled โ€” reset/cleanup here + return; + } + element.style.opacity = String(progress); + }, + } + ``` -### Easing Functions + > `CustomEffect`'s type also allows a `{ ranges: [...] }` object form, but only the function form does anything โ€” the object form is inert on its own. -Wix Motion provides both CSS and JavaScript easing functions: +3. **`namedEffect: { type, ...params }`** โ€” references an effect registered via `registerEffects()`. If the name isn't registered, `getWebAnimation` returns `null`. -#### CSS Easings (Performance Optimized) + ```typescript + import { registerEffects } from '@wix/motion'; + import { FadeIn } from '@wix/motion-presets'; -```typescript -easing: 'easeInOut'; // CSS cubic-bezier -easing: 'linear'; // No acceleration -easing: 'backOut'; // Overshoot effect -easing: 'elasticOut'; // Bounce effect -``` + registerEffects({ FadeIn }); -#### JavaScript Easings (Full Control) + getWebAnimation(element, { namedEffect: { type: 'FadeIn' }, duration: 600 }); + ``` -```typescript -easing: 'quintInOut'; // Smooth acceleration/deceleration -easing: 'circOut'; // Circular motion feel -easing: 'expoIn'; // Exponential acceleration -``` + `@wix/motion` only owns the registry contract (`registerEffects()` and the structural `EffectModule` shape) โ€” the effect catalog itself lives in [`@wix/motion-presets`](https://github.com/wix/interact/tree/master/packages/motion-presets). See the [Custom Effects guide](./guides/custom-effects.md) for authoring your own registered effects. -### Units and Measurements +## Triggers -Wix Motion supports multiple unit types: +The trigger is the third argument to `getWebAnimation()` / `getScrubScene()`: ```typescript -// Distance units -distance: { value: 100, unit: 'px' } -distance: { value: 50, unit: 'percentage' } -distance: { value: 2, unit: 'em' } -distance: { value: 100, unit: 'vh' } - -// Angles (always in degrees) -angle: 45 -direction: 270 // 0ยฐ = up, 90ยฐ = right, 180ยฐ = down, 270ยฐ = left - -// Duration (milliseconds for time, percentage for scrub) -duration: 1000 // Time-based -duration: { value: 50, unit: 'percentage' } // Scrub-based +{ trigger?: 'view-progress' | 'pointer-move', id?: string, componentId?: string, element?: HTMLElement, axis?: 'x' | 'y' } ``` -## Rendering Modes +- **Omitted** (or neither value) โ†’ time-based animation. +- **`'view-progress'`** โ†’ scroll-driven; paired with scrub options. `startOffset`/`endOffset` live on the **options**, not the trigger. +- **`'pointer-move'`** โ†’ pointer-driven; the `axis` (`'x' | 'y'`) to read lives on the **trigger**, not on the effect options. -### Web Animations API (Default) +## Native ViewTimeline vs. the scrub polyfill -High-performance, JavaScript-controlled animations: +`view-progress` behaves differently depending on browser support: -```typescript -import { getWebAnimation } from '@wix/motion'; +- **`window.ViewTimeline` available** โ€” `getWebAnimation()` returns a WAAPI animation linked directly to a `ViewTimeline` (`duration: 'auto'`). It auto-plays as the element scrolls through view; you don't call `.play()`. +- **Not available** โ€” the underlying animation gets `duration: 99.99ms` / `delay: 0.01ms` so its progress is externally scrubbable. `getScrubScene()` is what turns that into `ScrubScrollScene[]` โ€” plain objects (`start`, `end`, `effect(scene, progress)`, `destroy()`) that you drive from your own `IntersectionObserver`/scroll listener. +- **`getCSSAnimation()`** always generates `duration: 'auto'` for `view-progress`, regardless of runtime `ViewTimeline` support โ€” it's the SSR/FOUC-free path. -const animation = getWebAnimation(element, options); -await animation.play(); -``` - -**Advantages**: - -- Fine-grained control -- Precise timing -- Dynamic modifications -- Event callbacks - -**Use When**: +If you're using `@wix/interact`, its bundled scroll polyfill, [`fizban`](https://github.com/wix-incubator/fizban), automates driving the polyfill path. -- Need animation control -- Complex timing requirements -- Interactive animations +## Easing system -### CSS Animations - -Stylesheet-based animations for maximum performance: +`getEasing` and `getJsEasing` are exported from `@wix/motion`: ```typescript -import { getCSSAnimation } from '@wix/motion'; - -const cssRules = getCSSAnimation('elementId', options); -// Insert rules into stylesheet +function getEasing(easing?: string): string; // CSS easing string, default 'linear' +function getJsEasing(easing?: string): ((t: number) => number) | undefined; // JS easing fn ``` -**Advantages**: - -- GPU acceleration -- Better mobile performance -- Runs on compositor thread -- Survives JavaScript freezes +- **JS easings** (Penner functions โ€” used by `getJsEasing` and as a `Sequence`'s `offsetEasing`): `linear`, `sineIn`, `sineOut`, `sineInOut`, `quadIn`, `quadOut`, `quadInOut`, `cubicIn`, `cubicOut`, `cubicInOut`, `quartIn`, `quartOut`, `quartInOut`, `quintIn`, `quintOut`, `quintInOut`, `expoIn`, `expoOut`, `expoInOut`, `circIn`, `circOut`, `circInOut`, `backIn`, `backOut`, `backInOut`. +- **CSS easings** (used by `getEasing` / the `easing` option): `linear`, `ease`, `easeIn`, `easeOut`, `easeInOut`, plus every JS key above (except `linear`/`ease*`) resolving to a `cubic-bezier(...)` string. +- Both also accept a raw `cubic-bezier(x1, y1, x2, y2)` string (hyphenated โ€” not `cubicBezier(...)`), and `getJsEasing` additionally parses CSS `linear(...)` strings. +- Standard CSS timing-function keywords (like `ease-out`) work as-is wherever `easing` is accepted โ€” they don't need to match one of the named keys above. -**Use When**: +There is no `easeOutCubic`, `elasticOut`, `bounceOut`, or `bounceIn` โ€” those names don't exist. (`elastic`/`bounce` exist only as `transitionEasing` values for pointer smoothing, a separate field.) -- Simple, fire-and-forget animations -- Mobile-first applications -- Performance is critical +## Reduced motion -## Advanced Concepts - -### Animation Groups - -Multiple related animations managed together: - -```typescript -const group = getWebAnimation(element, [ - { namedEffect: { type: 'FadeIn' }, duration: 500 }, - { namedEffect: { type: 'SlideIn' }, duration: 800, delay: 200 }, -]); - -// Control all animations together -await group.play(); -group.pause(); -group.setPlaybackRate(2); -``` - -### Measurement and Preparation - -Pre-calculate layout for better performance: +Pass `{ reducedMotion: true }` as the 4th argument to `getWebAnimation()` (or `context.reducedMotion` for `getAnimation()`/`getSequence()`). It only affects **time-based** (non-scrub) animations: ```typescript -import { prepareAnimation } from '@wix/motion'; - -// Measure element dimensions before animating -prepareAnimation(element, animationOptions, () => { - // Callback when measurements complete - console.log('Ready to animate!'); +// single-iteration โ†’ collapsed to duration: 1 (still runs, effectively instant) +getWebAnimation(element, { namedEffect: { type: 'FadeIn' }, duration: 600 }, undefined, { + reducedMotion: true, }); -``` - -### Scroll Ranges and Offsets - -Fine-tune when scroll animations trigger: - -```typescript -{ - type: 'ScrubAnimationOptions', - namedEffect: { type: 'FadeScroll' }, - startOffset: { - name: 'cover', // Viewport intersection - offset: { value: 20, unit: 'percentage' } // Start at 20% intersection - }, - endOffset: { - name: 'exit-crossing', // Element leaving viewport - offset: { value: 0, unit: 'percentage' } // End immediately - } -} -``` -### CSS Custom Properties - -Dynamic values through CSS variables: - -```typescript -// Animation generates: -// --motion-scale: 1.2 -// --motion-rotate: 45deg -// --motion-translate-x: 100px - -// Use in your CSS: -.my-element { - transform: - scale(var(--motion-scale, 1)) - rotate(var(--motion-rotate, 0deg)) - translateX(var(--motion-translate-x, 0px)); -} +// multi-iteration โ†’ dropped entirely, returns null +getWebAnimation( + element, + { namedEffect: { type: 'Spin' }, duration: 2000, iterations: 0 }, + undefined, + { reducedMotion: true }, +); // โ†’ null ``` -### Sequences & Staggering - -Sequences coordinate multiple AnimationGroups as a single timeline with easing-driven stagger delays. Instead of manually calculating `delay` offsets for each animation, a Sequence distributes timing automatically. +## Sequences -#### Offset Model - -The stagger offset for each group is calculated with: - -``` -offset[i] = easing(i / last) * last * offsetMs -``` - -Where `i` is the group index, `last` is the final group's index, and `offsetMs` is the configured stagger interval. This produces: - -- **Linear** โ€” even spacing (0, 200, 400, 600, 800) -- **quadIn** โ€” slow start then rapid (0, 50, 200, 450, 800) -- **sineOut** โ€” fast start then gradual (0, 306, 565, 739, 800) +`getSequence()` coordinates multiple `AnimationGroup`s under one staggered timeline, distributing start-time offsets across the group with an easing function: ```typescript import { getSequence } from '@wix/motion'; -const items = document.querySelectorAll('.card'); - const sequence = getSequence( { offset: 150, offsetEasing: 'quadIn' }, - Array.from(items).map((el) => ({ + Array.from(document.querySelectorAll('.card')).map((el) => ({ target: el, options: { duration: 600, - keyframeEffect: { - name: 'fade-up', - keyframes: [ - { opacity: 0, transform: 'translateY(20px)' }, - { opacity: 1, transform: 'translateY(0)' }, - ], - }, + keyframeEffect: { name: 'fade-up', keyframes: [{ opacity: 0 }, { opacity: 1 }] }, }, })), ); @@ -414,68 +144,18 @@ const sequence = getSequence( sequence.play(); ``` -#### Dynamic Groups - -Groups can be added or removed at runtime. Both operations trigger automatic offset recalculation: - -- **`addGroups(entries)`** โ€” inserts groups at specified indices (e.g. when new list items appear) -- **`removeGroups(predicate)`** โ€” removes matching groups, cancels their animations, and returns them (e.g. when DOM elements are removed) - -#### Relationship to AnimationGroup - -`Sequence` extends `AnimationGroup`, inheriting all playback controls (`play`, `pause`, `reverse`, `cancel`, `progress`, `setPlaybackRate`). The child `AnimationGroup` instances are stored in `animationGroups`, while the flattened `Animation` array is available via the inherited `animations` property. +See [`getSequence` API reference](./api/get-sequence.md) for the full stagger model. -## Performance Considerations - -### Animation Lifecycle - -1. **Preparation** - Measure elements, calculate values -2. **Creation** - Generate keyframes and effects -3. **Execution** - Run animations with optimal timing -4. **Cleanup** - Remove event listeners and references - -### Best Practices - -- Prefer CSS animations for simple, non-interactive effects -- Batch DOM measurements using `fastdom` -- Avoid creating animations in render loops -- Clean up animations when components unmount - -### Browser Compatibility - -- **Web Animations API**: Baseline - Wide Availability -- **ViewTimeline API**: Chrome 115+, Firefox/Safari with polyfill -- **CSS Animations**: Baseline - Wide Availability - -## TypeScript Integration - -Wix Motion is built with TypeScript and provides comprehensive type safety: - -```typescript -import type { - TimeAnimationOptions, - ScrubAnimationOptions, - EntranceAnimation, - ScrollAnimation, - AnimationGroup, -} from '@wix/motion'; - -// Type-safe configuration -const config: TimeAnimationOptions = { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' } as EntranceAnimation, - duration: 1000, -}; - -// Typed return values -const animation: AnimationGroup = getWebAnimation(element, config); -``` +## Package boundary -## Next Steps +| Need | Use | +| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| Declarative triggerโ†’effect wiring, config-driven orchestration, React/Web Component bindings | [`@wix/interact`](https://github.com/wix/interact/tree/master/packages/interact) | +| Ready-made effect catalog (entrance/scroll/ongoing/mouse presets) | [`@wix/motion-presets`](https://github.com/wix/interact/tree/master/packages/motion-presets) โ€” register via `registerEffects()` | +| Custom render callbacks, manual scrub-scene driving, programmatic sequences, SSR/CSS generation, inline keyframes | `@wix/motion` (this package) | -Now that you understand the core concepts: +## Next steps -- **[Explore Categories](categories/)** - Dive deep into each animation category -- **[API Reference](api/)** - Complete function documentation -- **[Performance Guide](guides/performance.md)** - Optimization techniques -- **[Advanced Patterns](guides/advanced-patterns.md)** - Complex animation scenarios +- [Getting Started](./getting-started.md) โ€” install and run your first animations. +- [API Reference](./api/README.md) โ€” full function signatures and types. +- [Custom Effects guide](./guides/custom-effects.md) โ€” the `registerEffects()`/`EffectModule` contract, authoring `customEffect` callbacks, and driving scrub scenes. diff --git a/packages/motion/docs/examples/README.md b/packages/motion/docs/examples/README.md deleted file mode 100644 index a660db65..00000000 --- a/packages/motion/docs/examples/README.md +++ /dev/null @@ -1,239 +0,0 @@ -# Examples - -Real-world animation patterns and implementations using Wix Motion. - -## ๐Ÿ“‹ [Common Patterns](common-patterns.md) - -Frequently used animation patterns and recipes for typical use cases. - -**Includes**: Sequential animations, staggered reveals, hover effects, loading states, page transitions - -## ๐Ÿข [Real-World Implementations](real-world-implementations.md) - -Complete examples from actual projects showing complex animation scenarios. - -**Includes**: Landing pages, e-commerce, portfolios, dashboards, mobile apps - -## ๐ŸŽฎ [Interactive Demos](interactive-demos.md) - -Hands-on examples you can run and modify to learn animation concepts. - -**Includes**: CodePen demos, playground configurations, step-by-step tutorials - ---- - -## Quick Examples - -### Hero Section Animation - -```typescript -import { getWebAnimation } from '@wix/motion'; - -// Staggered entrance for hero elements -async function animateHero() { - const title = getWebAnimation('#hero-title', { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 800, - }); - - const subtitle = getWebAnimation('#hero-subtitle', { - type: 'TimeAnimationOptions', - namedEffect: { type: 'SlideIn', direction: 'bottom' }, - duration: 600, - delay: 200, - }); - - const cta = getWebAnimation('#hero-cta', { - type: 'TimeAnimationOptions', - namedEffect: { type: 'BounceIn', direction: 'bottom' }, - duration: 800, - delay: 400, - }); - - // Play all animations - await Promise.all([title.play(), subtitle.play(), cta.play()]); -} -``` - -### Scroll-Driven Card Reveal - -```typescript -import { getScrubScene } from '@wix/motion'; - -// Cards reveal as they scroll into view -document.querySelectorAll('.card').forEach((card, index) => { - const scene = getScrubScene( - card, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'FadeScroll', - range: 'in', - opacity: 0.8, - }, - }, - { - trigger: 'view-progress', - element: card, - }, - ); -}); -``` - -### Interactive Button Hover - -```typescript -import { getWebAnimation } from '@wix/motion'; - -const button = document.querySelector('.interactive-btn'); -let hoverAnimation; - -button.addEventListener('mouseenter', () => { - hoverAnimation = getWebAnimation(button, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Pulse', - intensity: 1.1, - }, - duration: 300, - }); - hoverAnimation.play(); -}); - -button.addEventListener('mouseleave', () => { - if (hoverAnimation) { - hoverAnimation.reverse(); - } -}); -``` - -### Loading State Animation - -```typescript -import { getWebAnimation } from '@wix/motion'; - -function startLoadingAnimation(element) { - return getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Spin', - direction: 'clockwise', - }, - duration: 1000, - iterations: Infinity, - }); -} - -function stopLoadingAnimation(animation) { - animation.cancel(); -} - -// Usage -const loader = startLoadingAnimation('#loading-spinner'); -// ... wait for data ... -stopLoadingAnimation(loader); -``` - -### Parallax Background - -```typescript -import { getScrubScene } from '@wix/motion'; - -// Background moves slower than scroll speed -const bgScene = getScrubScene( - '#hero-background', - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'BgParallax', - speed: 0.3, // 30% of scroll speed - }, - }, - { - trigger: 'view-progress', - element: document.body, - }, -); -``` - -## Framework Examples - -### React Component - -```typescript -import React, { useEffect, useRef } from 'react'; -import { getWebAnimation } from '@wix/motion'; - -function AnimatedCard({ children, delay = 0 }) { - const cardRef = useRef(null); - - useEffect(() => { - if (!cardRef.current) return; - - const animation = getWebAnimation(cardRef.current, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 600, - delay - }); - - animation.play(); - - return () => animation.cancel(); - }, [delay]); - - return ( -
- {children} -
- ); -} -``` - -### Vue Component - -```vue - - - -``` - ---- - -**Want to see more?** Check out our [interactive playground](../../playground/) or browse the detailed pattern guides above. diff --git a/packages/motion/docs/getting-started.md b/packages/motion/docs/getting-started.md index c3513baa..b37db64c 100644 --- a/packages/motion/docs/getting-started.md +++ b/packages/motion/docs/getting-started.md @@ -1,342 +1,186 @@ -# Getting Started with Wix Motion +# Getting Started -Welcome to Wix Motion! This guide will get you up and running with your first animation in under 10 minutes. +`@wix/motion` is a low-level animation engine built directly on the Web Animations API and CSS Animations. This guide gets you from install to a working animation in each of its four modes. -## Prerequisites - -- Node.js 16+ and npm/yarn -- Basic knowledge of JavaScript/TypeScript -- A web project with DOM access - -## Installation - -### Option 1: NPM/Yarn +## Install ```bash npm install @wix/motion -# or -yarn add @wix/motion ``` -### Option 2: Script Tag (for quick prototyping) +Requires Node.js `>=18`. -```html - -``` +## Your first animation -### Installing Animation Presets - -`@wix/motion` provides core animation utilities and an effects registry, while `@wix/motion-presets` provides ready-to-use effect modules you can register and reference via `namedEffect`. - -```bash -npm install @wix/motion-presets -``` - -Before using named effects like `FadeIn`, you need to register the presets: +The fastest path is a time-based Web Animations API (WAAPI) animation, driven by `getWebAnimation()` with an inline `keyframeEffect` โ€” no preset registration required. ```typescript -import { registerEffects } from '@wix/motion'; -import { FadeIn } from '@wix/motion-presets'; - -// Register preset -registerEffects({ FadeIn }); -``` - -You can also register a custom-made effect module (as long as it matches the expected module shape): +import { getWebAnimation } from '@wix/motion'; -```typescript -import { registerEffects } from '@wix/motion'; +const element = document.getElementById('hero'); -registerEffects({ - CustomFadeIn: { - web: (options) => [ - { ...options, name: 'CustomFadeIn', keyframes: [{ opacity: 0 }, { opacity: 1 }] }, - ], - getNames: () => ['CustomFadeIn'], - style: (options) => [ - { ...options, name: 'CustomFadeIn', keyframes: [{ opacity: 0 }, { opacity: 1 }] }, +const animation = getWebAnimation(element, { + keyframeEffect: { + name: 'fade-up', + keyframes: [ + { opacity: 0, transform: 'translateY(20px)' }, + { opacity: 1, transform: 'translateY(0)' }, ], }, + duration: 600, + easing: 'ease-out', }); -``` - -## Your First Animation -Let's create a simple fade-in animation for an element: - -### 1. HTML Setup - -```html -
Hello, Motion!
- +// getWebAnimation can return null (e.g. an unregistered namedEffect, or a +// multi-iteration animation dropped by reduced motion) โ€” always guard it. +animation?.play(); ``` -### 2. JavaScript Implementation +`play()` resolves once playback has **started**, not once the animation finishes. See [Observing completion](#observing-completion) below for how to react when it's actually done. -```typescript -import { getWebAnimation } from '@wix/motion'; +## Generating CSS instead -// Get the element to animate -const element = document.getElementById('myElement'); -const button = document.getElementById('animateBtn'); +For simple, fire-and-forget effects you can generate CSS `@keyframes`/`animation` descriptors instead of driving WAAPI directly. `getCSSAnimation()` returns an **array** of descriptors โ€” not a string โ€” so you inject them into a stylesheet yourself: -// Create the animation -const fadeInAnimation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 1000, - easing: 'easeOutCubic', -}); +```typescript +import { getCSSAnimation } from '@wix/motion'; -// Play animation on button click -button.addEventListener('click', async () => { - await fadeInAnimation.play(); - console.log('Animation completed!'); +const cssAnimations = getCSSAnimation('hero', { + keyframeEffect: { + name: 'fade-up', + keyframes: [ + { opacity: 0, transform: 'translateY(20px)' }, + { opacity: 1, transform: 'translateY(0)' }, + ], + }, + duration: 600, + easing: 'ease-out', }); -``` - -๐ŸŽ‰ **That's it!** You've created your first Wix Motion animation. -## Understanding the Code +const style = document.createElement('style'); -Let's break down what happened: +style.textContent = cssAnimations + .map(({ target, animation, name, keyframes }) => { + const steps = keyframes + .map((keyframe, i) => { + const percent = (i / (keyframes.length - 1)) * 100; + const declarations = Object.entries(keyframe) + .map(([property, value]) => `${property}: ${value};`) + .join(' '); + return `${percent}% { ${declarations} }`; + }) + .join('\n'); -### 1. `getWebAnimation()` Function + return `@keyframes ${name} { ${steps} }\n${target} { animation: ${animation}; }`; + }) + .join('\n'); -This is the main function for creating time-based animations using the Web Animations API. - -```typescript -getWebAnimation( - target, // DOM element to animate - animationOptions, // Configuration object - trigger?, // Optional trigger settings - options? // Additional options -) +document.head.appendChild(style); ``` -### 2. Animation Options Object +Generated `animation` shorthand values are paused by default โ€” toggle `animation-play-state` (e.g. by adding a class) when you want the animation to run. -```typescript -{ - type: 'TimeAnimationOptions', // Animation type - namedEffect: { type: 'FadeIn' }, // Preset animation - duration: 1000, // Duration in milliseconds - easing: 'easeOutCubic' // Easing function -} -``` +## Scroll-driven animations -### 3. Named Effects - -Instead of defining keyframes manually, you use predefined animation presets: - -- `FadeIn` - Simple opacity transition -- `SlideIn` - Slide from a direction -- `BounceIn` - Spring-based entrance -- [See all presets โ†’](categories/README.md) - -## Try More Animations - -### Slide In Animation +Pass a `view-progress` trigger as the third argument to link an animation to scroll. When `window.ViewTimeline` is available, `getWebAnimation()` returns a WAAPI animation linked directly to the timeline โ€” it plays automatically, no `.play()` call needed: ```typescript -const slideAnimation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'SlideIn', - direction: 'left', - }, - duration: 800, -}); -``` +import { getWebAnimation } from '@wix/motion'; -### Bounce In Animation +const scrollRoot = document.getElementById('scrollRoot'); +const parallax = document.getElementById('parallax'); -```typescript -const bounceAnimation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'BounceIn', - direction: 'bottom', +getWebAnimation( + parallax, + { + keyframeEffect: { + name: 'parallax', + keyframes: [{ transform: 'translateY(80px)' }, { transform: 'translateY(-80px)' }], + }, + startOffset: { name: 'cover', offset: { value: 0, unit: 'percentage' } }, + endOffset: { name: 'cover', offset: { value: 100, unit: 'percentage' } }, }, - duration: 1200, -}); + { trigger: 'view-progress', element: scrollRoot }, +); ``` -### Spin Animation (Ongoing) - -```typescript -const spinAnimation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'Spin', - direction: 'clockwise', - }, - duration: 2000, - iterations: Infinity, // Loop forever -}); -``` +`startOffset`/`endOffset` live on the animation options, not the trigger. -## Scroll-Driven Animations +### Without `ViewTimeline` (polyfill) -For animations that respond to scroll position: +When `window.ViewTimeline` isn't available, use `getScrubScene()` instead. With a `view-progress` trigger it returns `ScrubScrollScene[]` โ€” plain objects you drive yourself from an `IntersectionObserver` or scroll listener: ```typescript import { getScrubScene } from '@wix/motion'; -const scrollAnimation = getScrubScene( - element, +const scenes = getScrubScene( + parallax, { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: 0.5, + keyframeEffect: { + name: 'parallax', + keyframes: [{ transform: 'translateY(80px)' }, { transform: 'translateY(-80px)' }], }, + startOffset: { name: 'cover', offset: { value: 0, unit: 'percentage' } }, + endOffset: { name: 'cover', offset: { value: 100, unit: 'percentage' } }, }, - { - trigger: 'view-progress', - element: document.body, // Scroll container - }, + { trigger: 'view-progress', element: scrollRoot }, ); -``` - -## Animation Control - -All animations return an `AnimationGroup` with control methods: - -```typescript -const animation = getWebAnimation(element, options); -// Control playback -await animation.play(); -animation.pause(); -animation.cancel(); - -// Set playback rate -animation.setPlaybackRate(2); // 2x speed - -// Set progress manually (0-1) -animation.progress(0.5); // 50% complete - -// Listen for completion -animation.onFinish(() => { - console.log('Animation finished!'); +// Drive each scene yourself from a scroll/IntersectionObserver listener, +// then clean up with `scene.destroy()` when you're done. +scenes?.forEach((scene) => { + scene.effect(scene, getScrollProgressFor(scene)); // your own 0..1 progress calculation }); - -// Check current state -console.log(animation.playState); // 'running', 'paused', 'finished' ``` -## CSS Integration - -Wix Motion can also generate CSS animations for better performance in some scenarios: - -```typescript -import { getCSSAnimation } from '@wix/motion'; - -const cssAnimations = getCSSAnimation('myElementId', { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 1000, -}); - -// Apply to stylesheet -cssAnimations.forEach(({ target, animation, keyframes }) => { - // Insert CSS rules for the animation -}); -``` +If you're using `@wix/interact`, its bundled scroll polyfill, [`fizban`](https://github.com/wix-incubator/fizban), drives this automatically. -## TypeScript Support +## Pointer-driven animations -Wix Motion is built with TypeScript and provides excellent IntelliSense: +Pass a `pointer-move` trigger to map cursor position to an effect. The axis to read (`'x'` or `'y'`) is set on the **trigger**, not on the effect options: ```typescript -import type { TimeAnimationOptions, EntranceAnimation, AnimationGroup } from '@wix/motion'; - -// Type-safe animation options -const options: TimeAnimationOptions = { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' } as EntranceAnimation, - duration: 1000, -}; - -// Typed return value -const animation: AnimationGroup = getWebAnimation(element, options); -``` - -## Common Patterns - -### Sequential Animations - -```typescript -async function sequentialAnimations() { - await fadeInAnimation.play(); - await slideAnimation.play(); - await bounceAnimation.play(); -} -``` +import { getScrubScene } from '@wix/motion'; -### Parallel Animations +const card = document.getElementById('card'); -```typescript -async function parallelAnimations() { - await Promise.all([fadeInAnimation.play(), slideAnimation.play(), bounceAnimation.play()]); -} -``` - -### Animation Chaining +const scene = getScrubScene( + card, + { + keyframeEffect: { + name: 'tilt', + keyframes: [{ transform: 'rotate(-6deg)' }, { transform: 'rotate(6deg)' }], + }, + transitionDuration: 200, + transitionEasing: 'easeOut', + }, + { trigger: 'pointer-move', axis: 'x', element: card }, +); -```typescript -fadeInAnimation - .play() - .then(() => slideAnimation.play()) - .then(() => bounceAnimation.play()); +card?.addEventListener('pointermove', (event) => { + const rect = card.getBoundingClientRect(); + const x = (event.clientX - rect.left) / rect.width; + scene?.effect(scene, { x, y: 0 }); +}); ``` -## Performance Tips +## Observing completion -1. **Prepare animations** for better performance: +`await animation.play()` only tells you playback has started. To react when an animation actually finishes, use `onFinish()` or await the `finished` promise: ```typescript -import { prepareAnimation } from '@wix/motion'; +animation?.onFinish(() => { + console.log('animation finished'); +}); -prepareAnimation(element, animationOptions); +// or: +await animation?.finished; // resolves with Animation[] +console.log('animation finished'); ``` -2. **Use appropriate animation types**: - - `TimeAnimationOptions` for time-based animations - - `ScrubAnimationOptions` for scroll/mouse-driven animations - -3. **Prefer CSS animations** for simple effects on mobile devices - -## Next Steps - -Now that you've created your first animation, explore more: - -- **[Core Concepts](core-concepts.md)** - Understand the animation system architecture -- **[Animation Categories](categories/)** - Discover all 82+ animation presets -- **[API Reference](api/)** - Deep dive into all available functions -- **[Advanced Patterns](guides/advanced-patterns.md)** - Learn performance optimization and complex scenarios - -## Troubleshooting - -### Animation Not Playing? - -- Check that the element exists in the DOM -- Ensure the element is visible (not `display: none`) -- Verify animation options are correctly formatted - -### Performance Issues? - -- Consider CSS animations for simple effects -- Avoid creating animations in tight loops - -### TypeScript Errors? - -- Ensure you're importing types correctly -- Check that animation options match the expected interface - ---- +## Next steps -**Ready for more?** Check out our [interactive playground](../playground/) to experiment with all animations in real-time! +- [Core Concepts](./core-concepts.md) โ€” the options shape, effect-definition modes, triggers, and easing system. +- [API Reference](./api/README.md) โ€” full function signatures and types. +- Ready-made effects live in [`@wix/motion-presets`](https://github.com/wix/interact/tree/master/packages/motion-presets); register them with `registerEffects()` and reference them via `namedEffect`. +- For declarative, config-driven triggerโ†’effect wiring (including React and Web Component bindings), see [`@wix/interact`](https://github.com/wix/interact/tree/master/packages/interact). diff --git a/packages/motion/docs/guides/README.md b/packages/motion/docs/guides/README.md index 1dd828dd..4babfd5d 100644 --- a/packages/motion/docs/guides/README.md +++ b/packages/motion/docs/guides/README.md @@ -1,217 +1,20 @@ -# Advanced Guides +# Guides -Comprehensive guides for mastering Wix Motion in complex applications and specialized use cases. +Task-oriented guides for the parts of `@wix/motion` that go beyond a single function call โ€” authoring +your own effects, generating CSS ahead of time, and getting the performance characteristics right. -## Overview +## Available guides -These guides are designed for developers who need to implement sophisticated animation systems, optimize performance, or integrate Wix Motion with specific frameworks and testing strategies. +- **[Custom Effects](./custom-effects.md)** โ€” the `customEffect` callback, authoring and registering your + own effect modules via `registerEffects()`, driving scrub scenes manually, and `data-motion-part` + sub-targeting. +- **[SSR & CSS Generation](./ssr-css.md)** โ€” the `getCSSAnimation()` descriptor shape, building a + stylesheet from it, and how it enables FOUC-free rendering. +- **[Performance](./performance.md)** โ€” preferring `transform`/`opacity`, `fastdom` batching, the CSS vs. + WAAPI tradeoff, and reduced-motion handling. -## Available Guides +## See also -### ๐Ÿ“ˆ [Performance Optimization](performance.md) - -Complete guide to optimizing Wix Motion animations for smooth 60fps performance across all devices. - -**Topics Covered:** - -- Rendering strategy selection (CSS vs Web Animations API) -- DOM performance optimization with `fastdom` -- Memory management and lifecycle optimization -- Device-specific optimizations (mobile, low-end devices, battery) -- Animation-specific performance patterns -- Performance monitoring and debugging tools - -**Best For:** Developers building performance-critical applications, mobile-first sites, or applications with many concurrent animations. - -### ๐Ÿ”ฌ [Advanced Usage Patterns](advanced-patterns.md) - -Sophisticated animation techniques and patterns for complex applications. - -**Topics Covered:** - -- Custom animation development and preset creation -- Complex timing patterns and orchestration -- State-driven animation systems -- Advanced scroll patterns and multi-layer parallax -- Interactive animation patterns and gesture-driven effects -- Performance architecture for large applications - -**Best For:** Senior developers building animation-heavy applications, custom animation libraries, or complex interactive experiences. - -### ๐Ÿงฉ [Framework Integration](framework-integration.md) - -Integration patterns for React, Vue, Angular, and other frontend frameworks. - -**Topics Covered:** - -- React hooks, components, and lifecycle management -- Vue composables and reactive animation patterns -- Angular services, directives, and dependency injection -- Universal patterns for cross-framework compatibility -- SSR-safe animation initialization -- TypeScript integration and type safety - -**Best For:** Teams using modern frontend frameworks who need proper animation lifecycle management and reactive updates. - -### ๐Ÿงช [Testing Animation Behaviors](testing.md) - -Comprehensive testing strategies for animation systems. - -**Topics Covered:** - -- Unit testing animation logic and state management -- Integration testing with framework components -- Visual regression testing with Playwright and Chromatic -- Performance testing and memory leak detection -- Accessibility testing and reduced motion support -- E2E testing for complex user flows - -**Best For:** Quality-focused teams implementing comprehensive testing strategies for animation-heavy applications. - -## Guide Selection Matrix - -| Use Case | Primary Guide | Secondary Guides | -| --------------------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------- | -| **Performance Issues** | [Performance](performance.md) | [Advanced Patterns](advanced-patterns.md) | -| **Complex Animations** | [Advanced Patterns](advanced-patterns.md) | [Performance](performance.md) | -| **React/Vue/Angular Integration** | [Framework Integration](framework-integration.md) | [Testing](testing.md) | -| **Custom Animation Development** | [Advanced Patterns](advanced-patterns.md) | [Performance](performance.md) | -| **Mobile Optimization** | [Performance](performance.md) | [Framework Integration](framework-integration.md) | -| **Large-Scale Applications** | [Advanced Patterns](advanced-patterns.md) | [Performance](performance.md), [Framework Integration](framework-integration.md) | - -## Quick Reference - -### Performance Optimization Checklist - -- [ ] Choose appropriate rendering method (CSS vs JS) -- [ ] Batch DOM operations using `fastdom` -- [ ] Use transform and opacity properties exclusively -- [ ] Implement intersection observers for viewport optimizations -- [ ] Monitor performance with profiling tools -- [ ] Test on low-end devices and slow networks - -### Framework Integration Essentials - -- [ ] Proper lifecycle management (create/destroy animations) -- [ ] Memoize animation options to prevent recreations -- [ ] Handle SSR gracefully with environment checks -- [ ] Implement cleanup in component unmount handlers -- [ ] Use TypeScript for better development experience - -### Testing Requirements - -- [ ] Unit tests for animation creation and control -- [ ] Integration tests with framework components -- [ ] Visual regression tests for UI consistency -- [ ] Performance tests for frame rates and memory -- [ ] Accessibility tests for reduced motion support -- [ ] E2E tests for complete user flows - -### Advanced Pattern Categories - -- [ ] **Custom Effects** - Create reusable animation presets -- [ ] **Orchestration** - Complex timing and sequencing -- [ ] **State Management** - Animation-driven application states -- [ ] **Scroll Systems** - Multi-layer parallax and scroll storytelling -- [ ] **Interactions** - Gesture-driven and responsive animations - -## Common Integration Patterns - -### React Hook Pattern - -```typescript -const { ref, play, pause, progress } = useAnimation(animationOptions, { - autoPlay: true, - dependencies: [animationOptions], -}); -``` - -### Vue Composable Pattern - -```typescript -const { animation, play, pause } = useAnimation( - elementRef, - computed(() => animationOptions), -); -``` - -### Angular Service Pattern - -```typescript -this.animationService - .createAnimation(id, element, options) - .subscribe((state) => this.handleAnimationState(state)); -``` - -### Testing Pattern - -```typescript -const { animation } = renderWithAnimation(component, options); -await waitFor(() => expect(animation).toHaveCompletedAnimation()); -``` - -## Advanced Topics by Complexity - -### Beginner Advanced (Building on Core Concepts) - -1. **Custom Animation Creation** - Extend existing presets -2. **React/Vue Integration** - Basic hooks and composables -3. **Performance Monitoring** - Simple FPS tracking - -### Intermediate Advanced - -1. **Animation Orchestration** - Complex sequences and timing -2. **State-Driven Systems** - Animation state machines -3. **Framework Patterns** - Advanced lifecycle management - -### Expert Advanced - -1. **Custom Animation Systems** - Build animation libraries -2. **Performance Architecture** - Large-scale optimization strategies -3. **Advanced Scroll Systems** - Multi-layer parallax and storytelling -4. **Cross-Framework Solutions** - Universal animation managers - -## Troubleshooting Guide - -### Performance Issues - -1. **Check [Performance Guide](performance.md)** - Device-specific optimizations -2. **Review animation complexity** - Simplify effects for mobile -3. **Monitor memory usage** - Implement proper cleanup -4. **Use CSS animations** - For simple, fire-and-forget effects - -### Framework Integration Issues - -1. **Check [Framework Integration](framework-integration.md)** - Lifecycle patterns -2. **Verify cleanup logic** - Animations should cancel on unmount -3. **Review dependency arrays** - Prevent unnecessary recreations -4. **Test SSR compatibility** - Handle server-side rendering - -### Complex Animation Issues - -1. **Check [Advanced Patterns](advanced-patterns.md)** - Sophisticated techniques -2. **Review timing and orchestration** - Sequence and state management -3. **Consider custom effects** - Build specialized animations -4. **Optimize for scale** - Performance architecture patterns - -## Additional Resources - -### Code Examples - -All guides include extensive code examples that you can copy and adapt for your specific use cases. - -### Type Definitions - -Comprehensive TypeScript support is covered in the [API Types documentation](../api/types.md). - -### Core Functions - -For basic animation creation, see the [Core Functions guide](../api/core-functions.md). - -### Animation Categories - -For understanding available animations, explore the [Category Guides](../categories/). - ---- - -**Ready to dive deeper?** Choose the guide that best matches your current needs, or start with [Performance Optimization](performance.md) if you're building production applications. +- [API Reference](../api/README.md) โ€” full function, class, and type signatures. +- [Core Concepts](../core-concepts.md) โ€” the effect-definition modes, triggers, and mental model these + guides build on. diff --git a/packages/motion/docs/guides/advanced-patterns.md b/packages/motion/docs/guides/advanced-patterns.md deleted file mode 100644 index 72133e90..00000000 --- a/packages/motion/docs/guides/advanced-patterns.md +++ /dev/null @@ -1,1367 +0,0 @@ -# Advanced Usage Patterns - -Sophisticated animation techniques and patterns for complex applications using Wix Motion. This guide covers advanced scenarios, custom development, and architectural patterns. - -## Overview - -This guide is designed for developers implementing complex animation systems, creating custom effects, or building animation-heavy applications. It covers: - -- **Custom Animation Development** - Creating your own animation presets -- **Complex Timing Patterns** - Sequencing and orchestration -- **State-Driven Animations** - Integrating with application state -- **Advanced Scroll Patterns** - Sophisticated scroll interactions -- **Performance Architecture** - Building scalable animation systems - -## Custom Animation Development - -### Creating Custom Named Effects - -Build your own animation presets that integrate seamlessly with Wix Motion's architecture. - -#### Basic Custom Effect Structure - -```typescript -// Define the effect type -interface MyCustomEffect extends BaseDataItemLike<'MyCustomEffect'> { - intensity?: number; - direction?: 'in' | 'out'; - color?: string; -} - -// Implementation following Wix Motion patterns -export function web(options: TimeAnimationOptions & { namedEffect: MyCustomEffect }) { - const { intensity = 1, direction = 'in', color = '#ff0000' } = options.namedEffect; - - return [ - { - name: 'MyCustomEffect', - keyframes: [ - { - transform: direction === 'in' ? 'scale(0) rotate(0deg)' : 'scale(1) rotate(0deg)', - backgroundColor: direction === 'in' ? 'transparent' : color, - opacity: direction === 'in' ? 0 : 1, - }, - { - transform: direction === 'in' ? 'scale(1) rotate(360deg)' : 'scale(0) rotate(360deg)', - backgroundColor: direction === 'in' ? color : 'transparent', - opacity: direction === 'in' ? 1 : 0, - }, - ], - timing: { - duration: options.duration || 1000, - easing: options.easing || 'ease-out', - fill: 'forwards', - }, - }, - ]; -} - -export function getNames(options: TimeAnimationOptions & { namedEffect: MyCustomEffect }) { - return ['MyCustomEffect']; -} - -export function style(options: TimeAnimationOptions & { namedEffect: MyCustomEffect }) { - // CSS version for better performance - return web(options); // Can also return CSS-specific implementation -} - -// Optional preparation function for measurements -export function prepare( - options: TimeAnimationOptions & { namedEffect: MyCustomEffect }, - dom?: DomApi, -) { - // Pre-calculate any measurements needed - if (dom) { - dom.measure((element) => { - // Store measurements for animation - const bounds = element.getBoundingClientRect(); - console.log('Element prepared:', bounds); - }); - } -} -``` - -#### Advanced Custom Effect with Dynamic Parameters - -```typescript -interface DynamicWaveEffect extends BaseDataItemLike<'DynamicWaveEffect'> { - amplitude?: number; - frequency?: number; - phase?: number; - axis?: 'x' | 'y' | 'both'; -} - -export function web( - options: TimeAnimationOptions & { namedEffect: DynamicWaveEffect }, - dom?: DomApi, -) { - const { amplitude = 50, frequency = 2, phase = 0, axis = 'both' } = options.namedEffect; - const duration = options.duration || 2000; - - // Generate dynamic keyframes - const keyframes = []; - const steps = 20; // Number of keyframe steps - - for (let i = 0; i <= steps; i++) { - const progress = i / steps; - const time = progress * duration; - - // Calculate wave position - const waveValue = Math.sin((time / duration) * frequency * 2 * Math.PI + phase) * amplitude; - - let transform = ''; - switch (axis) { - case 'x': - transform = `translateX(${waveValue}px)`; - break; - case 'y': - transform = `translateY(${waveValue}px)`; - break; - case 'both': - const xWave = Math.sin((time / duration) * frequency * 2 * Math.PI + phase) * amplitude; - const yWave = Math.cos((time / duration) * frequency * 2 * Math.PI + phase) * amplitude; - transform = `translate(${xWave}px, ${yWave}px)`; - break; - } - - keyframes.push({ transform, offset: progress }); - } - - return [ - { - name: 'DynamicWaveEffect', - keyframes, - timing: { - duration, - easing: 'linear', - iterations: options.iterations || 1, - }, - }, - ]; -} -``` - -### Custom Scroll Effects - -Create sophisticated scroll-driven animations with precise control. - -#### Physics-Based Scroll Animation - -```typescript -interface PhysicsScrollEffect extends BaseDataItemLike<'PhysicsScrollEffect'> { - mass?: number; - stiffness?: number; - damping?: number; - velocity?: number; -} - -export default function create( - options: ScrubAnimationOptions & { namedEffect: PhysicsScrollEffect }, -) { - const { mass = 1, stiffness = 100, damping = 10, velocity = 0 } = options.namedEffect; - - return [ - { - name: 'PhysicsScrollEffect', - keyframes: [ - { transform: 'translateY(0px)', offset: 0 }, - { transform: 'translateY(100px)', offset: 1 }, - ], - timing: { - duration: { value: 100, unit: 'percentage' }, - }, - custom: { - mass, - stiffness, - damping, - velocity, - }, - // Custom progress calculation - progressFunction: (scrollProgress: number, customData: any) => { - // Implement spring physics - const { mass, stiffness, damping } = customData; - - // Simplified spring equation - const springForce = -stiffness * scrollProgress; - const dampingForce = -damping * velocity; - const acceleration = (springForce + dampingForce) / mass; - - // Apply physics-based transformation - return Math.min(Math.max(scrollProgress + acceleration * 0.016, 0), 1); - }, - }, - ]; -} -``` - -#### Multi-Layer Parallax System - -```typescript -class AdvancedParallaxSystem { - private layers: ParallaxLayer[] = []; - private scrollManager: ScrollManager; - - constructor() { - this.scrollManager = new ScrollManager(); - } - - addLayer(element: HTMLElement, config: ParallaxLayerConfig): ParallaxLayer { - const layer: ParallaxLayer = { - element, - config, - animation: this.createLayerAnimation(element, config), - bounds: null, - isVisible: false, - }; - - this.layers.push(layer); - this.setupIntersectionObserver(layer); - - return layer; - } - - private createLayerAnimation(element: HTMLElement, config: ParallaxLayerConfig) { - return getScrubScene( - element, - { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'ParallaxScroll', - speed: config.speed, - }, - customEffect: { - ranges: [ - { name: 'translateY', min: config.range.start, max: config.range.end }, - { name: 'opacity', min: config.opacity?.start || 1, max: config.opacity?.end || 1 }, - { name: 'scale', min: config.scale?.start || 1, max: config.scale?.end || 1 }, - ], - }, - }, - { - trigger: 'view-progress', - element: config.viewport || document.body, - }, - ); - } - - private setupIntersectionObserver(layer: ParallaxLayer) { - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - layer.isVisible = entry.isIntersecting; - layer.bounds = entry.boundingClientRect; - - if (layer.isVisible) { - this.enableLayerUpdates(layer); - } else { - this.disableLayerUpdates(layer); - } - }); - }, - { - rootMargin: '50px', - threshold: [0, 0.1, 0.5, 0.9, 1], - }, - ); - - observer.observe(layer.element); - layer.observer = observer; - } - - private enableLayerUpdates(layer: ParallaxLayer) { - this.scrollManager.addUpdateCallback(layer.element.id, (scrollData) => { - this.updateLayer(layer, scrollData); - }); - } - - private disableLayerUpdates(layer: ParallaxLayer) { - this.scrollManager.removeUpdateCallback(layer.element.id); - } - - private updateLayer(layer: ParallaxLayer, scrollData: ScrollData) { - if (!layer.isVisible || !layer.bounds) return; - - const progress = this.calculateLayerProgress(layer, scrollData); - const transforms = this.calculateLayerTransforms(layer, progress); - - // Apply transforms efficiently - layer.element.style.transform = `translate3d(${transforms.x}px, ${transforms.y}px, 0) scale(${transforms.scale})`; - layer.element.style.opacity = transforms.opacity.toString(); - } - - private calculateLayerProgress(layer: ParallaxLayer, scrollData: ScrollData): number { - const { scrollY, viewportHeight } = scrollData; - const { top, height } = layer.bounds!; - - // Calculate when element enters and exits viewport - const enterY = top + scrollY - viewportHeight; - const exitY = top + scrollY + height; - const totalDistance = exitY - enterY; - - // Current progress through the animation range - const currentDistance = scrollY - enterY; - return Math.max(0, Math.min(1, currentDistance / totalDistance)); - } - - private calculateLayerTransforms(layer: ParallaxLayer, progress: number) { - const config = layer.config; - - return { - x: - this.interpolate(progress, config.range.start, config.range.end) * - (config.axis?.includes('x') ? 1 : 0), - y: - this.interpolate(progress, config.range.start, config.range.end) * - (config.axis?.includes('y') !== false ? 1 : 0), - scale: config.scale ? this.interpolate(progress, config.scale.start, config.scale.end) : 1, - opacity: config.opacity - ? this.interpolate(progress, config.opacity.start, config.opacity.end) - : 1, - }; - } - - private interpolate(progress: number, start: number, end: number): number { - return start + (end - start) * progress; - } - - destroy() { - this.layers.forEach((layer) => { - if (layer.observer) { - layer.observer.disconnect(); - } - if (layer.animation) { - layer.animation.destroy(); - } - }); - this.layers = []; - this.scrollManager.destroy(); - } -} - -interface ParallaxLayerConfig { - speed: number; - range: { start: number; end: number }; - opacity?: { start: number; end: number }; - scale?: { start: number; end: number }; - axis?: 'x' | 'y' | 'both'; - viewport?: HTMLElement; -} - -interface ParallaxLayer { - element: HTMLElement; - config: ParallaxLayerConfig; - animation: any; - bounds: DOMRect | null; - isVisible: boolean; - observer?: IntersectionObserver; -} - -interface ScrollData { - scrollY: number; - scrollX: number; - viewportHeight: number; - viewportWidth: number; - deltaY: number; - velocity: number; -} - -class ScrollManager { - private callbacks = new Map void>(); - private rafId: number | null = null; - private lastScrollY = 0; - private lastScrollTime = performance.now(); - private velocity = 0; - - addUpdateCallback(id: string, callback: (data: ScrollData) => void) { - this.callbacks.set(id, callback); - if (this.callbacks.size === 1) { - this.startListening(); - } - } - - removeUpdateCallback(id: string) { - this.callbacks.delete(id); - if (this.callbacks.size === 0) { - this.stopListening(); - } - } - - private startListening() { - const onScroll = () => { - if (this.rafId) return; - - this.rafId = requestAnimationFrame(() => { - this.updateCallbacks(); - this.rafId = null; - }); - }; - - window.addEventListener('scroll', onScroll, { passive: true }); - } - - private stopListening() { - if (this.rafId) { - cancelAnimationFrame(this.rafId); - this.rafId = null; - } - } - - private updateCallbacks() { - const now = performance.now(); - const currentScrollY = window.scrollY; - const deltaY = currentScrollY - this.lastScrollY; - const deltaTime = now - this.lastScrollTime; - - // Calculate velocity (pixels per millisecond) - this.velocity = deltaTime > 0 ? deltaY / deltaTime : 0; - - const scrollData: ScrollData = { - scrollY: currentScrollY, - scrollX: window.scrollX, - viewportHeight: window.innerHeight, - viewportWidth: window.innerWidth, - deltaY, - velocity: this.velocity, - }; - - this.callbacks.forEach((callback) => { - try { - callback(scrollData); - } catch (error) { - console.warn('Scroll callback error:', error); - } - }); - - this.lastScrollY = currentScrollY; - this.lastScrollTime = now; - } - - destroy() { - this.callbacks.clear(); - this.stopListening(); - } -} -``` - -## Complex Timing and Orchestration - -### Animation Sequences and Choreography - -```typescript -class AnimationChoreographer { - private timeline: AnimationTimeline = []; - private globalSpeed = 1; - private isPlaying = false; - - // Add animations to timeline - sequence(animation: AnimationGroup, startTime: number = 0): this { - this.timeline.push({ - animation, - startTime, - duration: this.getAnimationDuration(animation), - status: 'pending', - }); - - // Sort by start time - this.timeline.sort((a, b) => a.startTime - b.startTime); - return this; - } - - parallel(animations: AnimationGroup[], startTime: number = 0): this { - animations.forEach((animation) => { - this.sequence(animation, startTime); - }); - return this; - } - - stagger(animations: AnimationGroup[], staggerDelay: number = 100, startTime: number = 0): this { - animations.forEach((animation, index) => { - this.sequence(animation, startTime + index * staggerDelay); - }); - return this; - } - - // Advanced timing patterns - overlap( - firstAnimation: AnimationGroup, - secondAnimation: AnimationGroup, - overlapAmount: number, - ): this { - const firstDuration = this.getAnimationDuration(firstAnimation); - const secondStartTime = Math.max(0, firstDuration - overlapAmount); - - this.sequence(firstAnimation, 0); - this.sequence(secondAnimation, secondStartTime); - return this; - } - - // Play the entire timeline - async play(): Promise { - this.isPlaying = true; - const startTime = performance.now(); - - // Start all animations at their scheduled times - const timeoutIds: number[] = []; - - this.timeline.forEach((item) => { - const delay = item.startTime / this.globalSpeed; - - const timeoutId = window.setTimeout(async () => { - if (this.isPlaying) { - item.status = 'playing'; - await item.animation.play(); - - // Mark as completed when finished - item.animation.finished - .then(() => { - item.status = 'completed'; - }) - .catch(() => { - item.status = 'failed'; - }); - } - }, delay); - - timeoutIds.push(timeoutId); - }); - - // Wait for all animations to complete - const allAnimations = this.timeline.map((item) => item.animation.finished); - try { - await Promise.all(allAnimations); - } catch (error) { - console.warn('Some animations failed:', error); - } finally { - this.isPlaying = false; - timeoutIds.forEach((id) => clearTimeout(id)); - } - } - - pause(): void { - this.isPlaying = false; - this.timeline.forEach((item) => { - if (item.status === 'playing') { - item.animation.pause(); - } - }); - } - - setSpeed(speed: number): void { - this.globalSpeed = speed; - this.timeline.forEach((item) => { - item.animation.setPlaybackRate(speed); - }); - } - - // Get total duration of the timeline - getTotalDuration(): number { - if (this.timeline.length === 0) return 0; - - const lastItem = this.timeline[this.timeline.length - 1]; - return lastItem.startTime + lastItem.duration; - } - - // Reset timeline - reset(): void { - this.timeline.forEach((item) => { - item.animation.cancel(); - item.status = 'pending'; - }); - this.isPlaying = false; - } - - private getAnimationDuration(animation: AnimationGroup): number { - // Calculate duration from animation options or default - return 1000; // Simplified - would inspect actual animation - } -} - -interface AnimationTimelineItem { - animation: AnimationGroup; - startTime: number; - duration: number; - status: 'pending' | 'playing' | 'completed' | 'failed'; -} - -type AnimationTimeline = AnimationTimelineItem[]; - -// Usage example -const choreographer = new AnimationChoreographer(); - -const heroAnimation = getWebAnimation('#hero', heroOptions); -const titleAnimation = getWebAnimation('#title', titleOptions); -const subtitleAnimation = getWebAnimation('#subtitle', subtitleOptions); -const buttonAnimation = getWebAnimation('#cta-button', buttonOptions); - -// Create complex sequence -await choreographer - .sequence(heroAnimation, 0) // Start hero immediately - .overlap(titleAnimation, heroAnimation, 200) // Title overlaps hero by 200ms - .stagger([subtitleAnimation, buttonAnimation], 150, 500) // Stagger subtitle and button - .setSpeed(1.2) // Play 20% faster - .play(); -``` - -### State-Driven Animation System - -```typescript -interface AnimationState { - id: string; - animations: AnimationGroup[]; - transitions: Record; - onEnter?: () => void; - onExit?: () => void; -} - -interface TransitionConfig { - to: string; - condition?: () => boolean; - animation?: AnimationGroup; - duration?: number; -} - -class StateMachine { - private states = new Map(); - private currentState: string | null = null; - private isTransitioning = false; - - addState(state: AnimationState): this { - this.states.set(state.id, state); - return this; - } - - addTransition(fromState: string, toState: string, config: Partial = {}): this { - const state = this.states.get(fromState); - if (state) { - state.transitions[toState] = { - to: toState, - condition: config.condition || (() => true), - animation: config.animation, - duration: config.duration || 300, - }; - } - return this; - } - - async transition(toStateId: string): Promise { - if (this.isTransitioning || !this.states.has(toStateId)) { - return; - } - - const currentState = this.currentState ? this.states.get(this.currentState) : null; - const newState = this.states.get(toStateId); - - if (!newState) return; - - // Check if transition is allowed - if (currentState) { - const transition = currentState.transitions[toStateId]; - if (!transition || (transition.condition && !transition.condition())) { - return; - } - } - - this.isTransitioning = true; - - try { - // Exit current state - if (currentState) { - await this.exitState(currentState); - } - - // Perform transition animation if defined - const transition = currentState?.transitions[toStateId]; - if (transition?.animation) { - await transition.animation.play(); - } - - // Enter new state - await this.enterState(newState); - this.currentState = toStateId; - } catch (error) { - console.error('State transition failed:', error); - } finally { - this.isTransitioning = false; - } - } - - private async exitState(state: AnimationState): Promise { - // Call exit handler - if (state.onExit) { - state.onExit(); - } - - // Reverse all state animations - const exitPromises = state.animations.map((animation) => animation.reverse()); - await Promise.all(exitPromises); - } - - private async enterState(state: AnimationState): Promise { - // Call enter handler - if (state.onEnter) { - state.onEnter(); - } - - // Play all state animations - const enterPromises = state.animations.map((animation) => animation.play()); - await Promise.all(enterPromises); - } - - getCurrentState(): string | null { - return this.currentState; - } - - canTransitionTo(toStateId: string): boolean { - if (!this.currentState) return true; - - const currentState = this.states.get(this.currentState); - if (!currentState) return false; - - const transition = currentState.transitions[toStateId]; - return !!(transition && (!transition.condition || transition.condition())); - } -} - -// Usage example -const pageMachine = new StateMachine(); - -// Define states -const loadingState: AnimationState = { - id: 'loading', - animations: [ - getWebAnimation('#loading-spinner', spinnerOptions), - getWebAnimation('#loading-text', fadeOptions), - ], - transitions: {}, - onEnter: () => console.log('Entering loading state'), - onExit: () => console.log('Exiting loading state'), -}; - -const contentState: AnimationState = { - id: 'content', - animations: [ - getWebAnimation('#main-content', slideInOptions), - getWebAnimation('#navigation', fadeInOptions), - ], - transitions: {}, - onEnter: () => console.log('Content loaded'), - onExit: () => console.log('Leaving content'), -}; - -const errorState: AnimationState = { - id: 'error', - animations: [ - getWebAnimation('#error-message', shakeOptions), - getWebAnimation('#retry-button', pulseOptions), - ], - transitions: {}, -}; - -// Setup state machine -pageMachine - .addState(loadingState) - .addState(contentState) - .addState(errorState) - .addTransition('loading', 'content', { - condition: () => window.dataLoaded === true, - animation: getWebAnimation('#transition-overlay', fadeOutOptions), - }) - .addTransition('loading', 'error', { - condition: () => window.loadError === true, - }) - .addTransition('error', 'loading', { - animation: getWebAnimation('#retry-transition', slideUpOptions), - }); - -// Use state machine -await pageMachine.transition('loading'); - -// Later, when data loads -window.dataLoaded = true; -await pageMachine.transition('content'); -``` - -## Interactive Animation Patterns - -### Gesture-Driven Animations - -```typescript -class GestureAnimationController { - private element: HTMLElement; - private hammerjs: any; // HammerJS for gesture recognition - private currentAnimation: AnimationGroup | null = null; - private gestureStates = new Map(); - - constructor(element: HTMLElement) { - this.element = element; - this.setupGestureRecognition(); - } - - private setupGestureRecognition() { - // Initialize HammerJS or similar gesture library - this.hammerjs = new Hammer(this.element); - - // Configure gestures - this.hammerjs.get('swipe').set({ direction: Hammer.DIRECTION_ALL }); - this.hammerjs.get('pan').set({ direction: Hammer.DIRECTION_ALL }); - this.hammerjs.get('pinch').set({ enable: true }); - this.hammerjs.get('rotate').set({ enable: true }); - - // Bind gesture events - this.hammerjs.on('swipe', this.handleSwipe.bind(this)); - this.hammerjs.on('pan', this.handlePan.bind(this)); - this.hammerjs.on('pinch', this.handlePinch.bind(this)); - this.hammerjs.on('rotate', this.handleRotate.bind(this)); - } - - private handleSwipe(event: any) { - const direction = this.getSwipeDirection(event.direction); - const velocity = event.velocity; - - // Create velocity-based animation - const distance = Math.min(velocity * 200, 400); // Cap max distance - - const animation = getWebAnimation(this.element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'GlideIn', - direction: this.directionToAngle(direction), - distance: { value: distance, unit: 'px' }, - }, - duration: Math.max(300, 1000 / velocity), // Faster swipes = shorter duration - easing: 'easeOut', - }); - - this.playAnimationWithFeedback(animation); - } - - private handlePan(event: any) { - if (!this.currentAnimation) { - // Create scrub animation for panning - this.currentAnimation = getWebAnimation(this.element, { - type: 'ScrubAnimationOptions', - namedEffect: { - type: 'TrackMouse', - distance: { value: 100, unit: 'px' }, - axis: 'both', - }, - transitionDuration: 0, // Immediate response - }) as AnimationGroup; - } - - // Update animation progress based on pan distance - const maxDistance = 200; - const panDistance = Math.sqrt(event.deltaX ** 2 + event.deltaY ** 2); - const progress = Math.min(panDistance / maxDistance, 1); - - this.currentAnimation.progress(progress); - - // Clean up on pan end - if (event.isFinal) { - this.currentAnimation.cancel(); - this.currentAnimation = null; - } - } - - private handlePinch(event: any) { - const scale = event.scale; - const scaleAnimation = getWebAnimation(this.element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'DropIn', - initialScale: scale, - }, - duration: 200, - }); - - this.playAnimationWithFeedback(scaleAnimation); - } - - private handleRotate(event: any) { - const rotation = event.rotation; - const rotateAnimation = getWebAnimation(this.element, { - type: 'TimeAnimationOptions', - namedEffect: { - type: 'SpinIn', - direction: rotation > 0 ? 'clockwise' : 'counter-clockwise', - spins: Math.abs(rotation) / 360, - }, - duration: 500, - }); - - this.playAnimationWithFeedback(rotateAnimation); - } - - private playAnimationWithFeedback(animation: AnimationGroup) { - // Add haptic feedback if available - if ('vibrate' in navigator) { - navigator.vibrate(50); - } - - // Add visual feedback - this.element.style.filter = 'brightness(1.1)'; - - animation.play().then(() => { - // Reset visual feedback - this.element.style.filter = ''; - }); - } - - private getSwipeDirection(hammerDirection: number): 'up' | 'down' | 'left' | 'right' { - switch (hammerDirection) { - case Hammer.DIRECTION_UP: - return 'up'; - case Hammer.DIRECTION_DOWN: - return 'down'; - case Hammer.DIRECTION_LEFT: - return 'left'; - case Hammer.DIRECTION_RIGHT: - return 'right'; - default: - return 'up'; - } - } - - private directionToAngle(direction: string): number { - const angles = { up: 0, right: 90, down: 180, left: 270 }; - return angles[direction] || 0; - } -} - -interface GestureState { - type: 'swipe' | 'pan' | 'pinch' | 'rotate'; - startTime: number; - currentValue: number; - velocity: number; -} -``` - -### Scroll-Based Storytelling - -```typescript -class ScrollStorytellingEngine { - private chapters: StoryChapter[] = []; - private currentChapter = 0; - private scrollManager: ScrollManager; - private debug = false; - - constructor(debug = false) { - this.debug = debug; - this.scrollManager = new ScrollManager(); - this.setupScrollListener(); - } - - addChapter(chapter: StoryChapter): this { - this.chapters.push(chapter); - return this; - } - - private setupScrollListener() { - this.scrollManager.addUpdateCallback('storytelling', (scrollData) => { - this.updateStory(scrollData); - }); - } - - private updateStory(scrollData: ScrollData) { - const { scrollY, viewportHeight } = scrollData; - - // Find current chapter based on scroll position - const newChapter = this.findCurrentChapter(scrollY); - - if (newChapter !== this.currentChapter) { - this.transitionToChapter(newChapter); - this.currentChapter = newChapter; - } - - // Update current chapter animations - if (this.chapters[this.currentChapter]) { - this.updateChapterAnimations(this.currentChapter, scrollData); - } - } - - private findCurrentChapter(scrollY: number): number { - for (let i = 0; i < this.chapters.length; i++) { - const chapter = this.chapters[i]; - const chapterStart = chapter.element.offsetTop; - const chapterEnd = chapterStart + chapter.element.offsetHeight; - - if (scrollY >= chapterStart && scrollY < chapterEnd) { - return i; - } - } - - // If past all chapters, stay on last chapter - return Math.max(0, this.chapters.length - 1); - } - - private async transitionToChapter(chapterIndex: number) { - const chapter = this.chapters[chapterIndex]; - if (!chapter) return; - - // Call chapter enter callback - if (chapter.onEnter) { - chapter.onEnter(chapterIndex); - } - - // Play chapter entrance animations - if (chapter.entranceAnimations) { - const entrancePromises = chapter.entranceAnimations.map((anim) => anim.play()); - await Promise.all(entrancePromises); - } - - if (this.debug) { - console.log(`Entered chapter ${chapterIndex}: ${chapter.title}`); - } - } - - private updateChapterAnimations(chapterIndex: number, scrollData: ScrollData) { - const chapter = this.chapters[chapterIndex]; - if (!chapter.scrollAnimations) return; - - const chapterProgress = this.calculateChapterProgress(chapter, scrollData.scrollY); - - chapter.scrollAnimations.forEach((animation) => { - // Update scroll-driven animations with current progress - if (animation.progress) { - animation.progress(chapterProgress); - } - }); - - // Update any custom scroll behaviors - if (chapter.onScroll) { - chapter.onScroll(chapterProgress, scrollData); - } - } - - private calculateChapterProgress(chapter: StoryChapter, scrollY: number): number { - const chapterStart = chapter.element.offsetTop; - const chapterHeight = chapter.element.offsetHeight; - const chapterEnd = chapterStart + chapterHeight; - - if (scrollY <= chapterStart) return 0; - if (scrollY >= chapterEnd) return 1; - - return (scrollY - chapterStart) / chapterHeight; - } - - // Advanced chapter transitions - createChapterTransition(fromChapter: number, toChapter: number): AnimationGroup[] { - const from = this.chapters[fromChapter]; - const to = this.chapters[toChapter]; - - if (!from || !to) return []; - - const transitions: AnimationGroup[] = []; - - // Fade out previous chapter - if (from.element) { - transitions.push( - getWebAnimation(from.element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, // Will be reversed - duration: 500, - }), - ); - } - - // Fade in new chapter - if (to.element) { - transitions.push( - getWebAnimation(to.element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 500, - delay: 250, // Slight overlap - }), - ); - } - - return transitions; - } - - // Get story progress - getStoryProgress(): StoryProgress { - const totalChapters = this.chapters.length; - const currentChapterProgress = this.chapters[this.currentChapter] - ? this.calculateChapterProgress(this.chapters[this.currentChapter], window.scrollY) - : 0; - - const overallProgress = - totalChapters > 0 ? (this.currentChapter + currentChapterProgress) / totalChapters : 0; - - return { - currentChapter: this.currentChapter, - currentChapterProgress, - overallProgress, - chapterTitle: this.chapters[this.currentChapter]?.title || '', - }; - } -} - -interface StoryChapter { - title: string; - element: HTMLElement; - entranceAnimations?: AnimationGroup[]; - scrollAnimations?: AnimationGroup[]; - onEnter?: (chapterIndex: number) => void; - onScroll?: (progress: number, scrollData: ScrollData) => void; - onExit?: (chapterIndex: number) => void; -} - -interface StoryProgress { - currentChapter: number; - currentChapterProgress: number; - overallProgress: number; - chapterTitle: string; -} - -// Usage example -const story = new ScrollStorytellingEngine(true); - -story - .addChapter({ - title: 'The Beginning', - element: document.getElementById('chapter-1')!, - entranceAnimations: [ - getWebAnimation('#chapter-1 h1', titleFadeOptions), - getWebAnimation('#chapter-1 .content', contentSlideOptions), - ], - scrollAnimations: [getScrubScene('#chapter-1 .parallax-bg', parallaxOptions, scrollTrigger)[0]], - onEnter: () => console.log('Story begins...'), - onScroll: (progress) => { - // Custom scroll behavior for this chapter - document.getElementById('progress-bar')!.style.width = `${progress * 100}%`; - }, - }) - .addChapter({ - title: 'The Journey', - element: document.getElementById('chapter-2')!, - entranceAnimations: [getWebAnimation('#chapter-2', journeyAnimationOptions)], - onEnter: () => console.log('The journey begins...'), - }); -``` - -## Performance Architecture for Large Applications - -### Animation Pool and Resource Management - -```typescript -class AnimationPool { - private pool = new Map(); - private activeAnimations = new Map(); - private maxPoolSize = 20; - private preloadedEffects = new Set(); - - // Get animation from pool or create new one - getAnimation(key: string, factory: () => AnimationGroup): AnimationGroup { - let pool = this.pool.get(key) || []; - - let animation: AnimationGroup; - if (pool.length > 0) { - animation = pool.pop()!; - this.resetAnimation(animation); - } else { - animation = factory(); - } - - this.activeAnimations.set(animation.id || key, animation); - return animation; - } - - // Return animation to pool - releaseAnimation(animation: AnimationGroup, key: string) { - const animationId = animation.id || key; - this.activeAnimations.delete(animationId); - - // Reset animation state - animation.cancel(); - - // Add to pool if not full - let pool = this.pool.get(key) || []; - if (pool.length < this.maxPoolSize) { - pool.push(animation); - this.pool.set(key, pool); - } - } - - // Preload common animations - preloadAnimations(preloadConfig: PreloadConfig[]) { - preloadConfig.forEach((config) => { - const pool: AnimationGroup[] = []; - - for (let i = 0; i < config.count; i++) { - const animation = config.factory(); - pool.push(animation); - } - - this.pool.set(config.key, pool); - this.preloadedEffects.add(config.key); - }); - } - - // Clean up unused animations - cleanup() { - const now = performance.now(); - - this.pool.forEach((animations, key) => { - // Remove old unused animations - const activeAnimations = animations.filter((anim) => { - const lastUsed = (anim as any).lastUsed || now; - return now - lastUsed < 30000; // Keep for 30 seconds - }); - - if (activeAnimations.length === 0) { - this.pool.delete(key); - } else { - this.pool.set(key, activeAnimations); - } - }); - } - - private resetAnimation(animation: AnimationGroup) { - animation.cancel(); - animation.progress(0); - (animation as any).lastUsed = performance.now(); - } - - // Get pool statistics - getStats(): PoolStats { - const totalPooled = Array.from(this.pool.values()).reduce((sum, pool) => sum + pool.length, 0); - - return { - totalPooled, - activeAnimations: this.activeAnimations.size, - preloadedEffects: this.preloadedEffects.size, - poolKeys: Array.from(this.pool.keys()), - }; - } -} - -interface PreloadConfig { - key: string; - count: number; - factory: () => AnimationGroup; -} - -interface PoolStats { - totalPooled: number; - activeAnimations: number; - preloadedEffects: number; - poolKeys: string[]; -} - -// Global animation manager -class GlobalAnimationManager { - private pool = new AnimationPool(); - private performanceMonitor = new PerformanceMonitor(); - private isLowPerformanceMode = false; - - constructor() { - this.setupPerformanceMonitoring(); - this.preloadCommonAnimations(); - } - - private setupPerformanceMonitoring() { - setInterval(() => { - const fps = this.performanceMonitor.getAverageFPS(); - const wasLowPerf = this.isLowPerformanceMode; - - this.isLowPerformanceMode = fps < 45; - - if (this.isLowPerformanceMode !== wasLowPerf) { - this.handlePerformanceModeChange(); - } - }, 2000); - - // Cleanup every 30 seconds - setInterval(() => { - this.pool.cleanup(); - }, 30000); - } - - private handlePerformanceModeChange() { - if (this.isLowPerformanceMode) { - console.warn('Entering low performance mode'); - // Could disable complex animations, reduce quality, etc. - } else { - console.log('Performance improved, resuming normal mode'); - } - } - - private preloadCommonAnimations() { - this.pool.preloadAnimations([ - { - key: 'fadeIn', - count: 5, - factory: () => - getWebAnimation(document.createElement('div'), { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - }), - }, - { - key: 'slideIn', - count: 3, - factory: () => - getWebAnimation(document.createElement('div'), { - type: 'TimeAnimationOptions', - namedEffect: { type: 'SlideIn', direction: 'bottom' }, - }), - }, - ]); - } - - // Public API - createOptimizedAnimation(element: HTMLElement, options: AnimationOptions): AnimationGroup { - const key = this.generateAnimationKey(options); - - if (this.isLowPerformanceMode) { - options = this.simplifyOptionsForPerformance(options); - } - - return this.pool.getAnimation(key, () => getWebAnimation(element, options)); - } - - releaseAnimation(animation: AnimationGroup, options: AnimationOptions) { - const key = this.generateAnimationKey(options); - this.pool.releaseAnimation(animation, key); - } - - private generateAnimationKey(options: AnimationOptions): string { - // Create a key based on animation type and parameters - if (options.namedEffect) { - return `${options.namedEffect.type}_${JSON.stringify(options.namedEffect)}`; - } - return `custom_${options.type}`; - } - - private simplifyOptionsForPerformance(options: AnimationOptions): AnimationOptions { - // Simplify animations for low performance devices - if (options.type === 'TimeAnimationOptions') { - return { - ...options, - duration: Math.min(options.duration || 1000, 300), - namedEffect: { type: 'FadeIn' }, // Use simplest animation - }; - } - return options; - } - - getStats() { - return { - pool: this.pool.getStats(), - performance: { - isLowPerformanceMode: this.isLowPerformanceMode, - currentFPS: this.performanceMonitor.getAverageFPS(), - }, - }; - } -} - -// Export singleton instance -export const animationManager = new GlobalAnimationManager(); -``` - ---- - -**Next**: Continue with [Framework Integration](framework-integration.md) for React, Vue, and Angular patterns, or explore [Testing](testing.md) for animation testing strategies. diff --git a/packages/motion/docs/guides/custom-effects.md b/packages/motion/docs/guides/custom-effects.md new file mode 100644 index 00000000..99eaf971 --- /dev/null +++ b/packages/motion/docs/guides/custom-effects.md @@ -0,0 +1,323 @@ +# Custom Effects + +`@wix/motion` gives you three mutually-exclusive ways to describe what an animation does +(`keyframeEffect` / `customEffect` / `namedEffect`), plus a registry for sharing named effects, and a +scrub-scene contract for driving scroll/pointer effects yourself. This guide covers everything beyond +inline keyframes: the `customEffect` callback, authoring and registering your own effect modules, and +manually driving scrub scenes. + +> **Gotchas** +> +> - There is no top-level `type` field on the options object โ€” see [Core Concepts](../core-concepts.md). +> - `customEffect` only does something when it's a **function**. The `{ ranges }` object form is accepted +> by the type but is inert in `@wix/motion` alone. +> - An unregistered `namedEffect.type` makes `getRegisteredEffect` warn and return `null` โ€” which makes +> `getWebAnimation`/`getCSSAnimation`/`getScrubScene` return `null` (or `[]`) too. + +## 1. Inline keyframes (recap) + +The zero-registration path is `keyframeEffect: { name, keyframes }` โ€” a `name` for the animation/ +`@keyframes`, and a standard WAAPI `Keyframe[]`. See [Getting Started](../getting-started.md) for a full +walkthrough; this guide focuses on the other two modes. + +```typescript +getWebAnimation(element, { + keyframeEffect: { + name: 'fade-up', + keyframes: [ + { opacity: 0, transform: 'translateY(20px)' }, + { opacity: 1, transform: 'translateY(0)' }, + ], + }, + duration: 600, +}); +``` + +## 2. The `customEffect` callback + +`customEffect` is the only **programmatic** mode โ€” a per-frame JavaScript callback instead of CSS/WAAPI +keyframes: + +```typescript +type CustomEffect = + | { ranges: { name: string; min: number; max: number; step?: number }[] } // inert in @wix/motion + | ((element: Element | null, progress: number | null) => void); // the working form +``` + +When `customEffect` is a function, `getWebAnimation` builds a `CustomAnimation` internally instead of a +plain `KeyframeEffect`. `CustomAnimation` wraps a real `Animation` (so `play`/`pause`/`cancel`/`finished`/ +`playState` all behave normally) and runs a `requestAnimationFrame` loop that calls +`customEffect(target, progress)` whenever the animation's computed progress changes. On `cancel()`, it +calls `customEffect(target, null)` once โ€” **`progress === null` signals cancellation**, and your callback +should treat it as a reset/cleanup instruction, not just another frame. + +The returned value is still a plain `AnimationGroup` โ€” `CustomAnimation` is an internal implementation +detail and isn't exported from the package. + +### Example: driving a CSS custom property + +```typescript +import { getWebAnimation } from '@wix/motion'; + +function paintGauge(element: Element | null, progress: number | null) { + const target = element as HTMLElement | null; + if (!target) return; + + if (progress === null) { + // Cancelled โ€” reset to a known state. + target.style.removeProperty('--gauge-progress'); + return; + } + + target.style.setProperty('--gauge-progress', String(progress)); +} + +const gauge = document.getElementById('gauge'); + +const animation = getWebAnimation(gauge, { + customEffect: paintGauge, + duration: 2000, + iterations: 0, // 0 โ‡’ Infinity + easing: 'linear', +}); + +animation?.play(); + +// later, e.g. on teardown: +// animation?.cancel(); // paintGauge(target, null) fires, clearing the custom property +``` + +```css +#gauge { + background: conic-gradient(steelblue calc(var(--gauge-progress, 0) * 360deg), #eee 0); +} +``` + +The same pattern works for driving a `` render loop, a WebGL uniform, or any other per-frame +side effect that CSS/WAAPI keyframes can't express directly โ€” `customEffect` gets called on the engine's +own rAF cadence rather than one you manage yourself. + +## 3. `registerEffects()` and the `EffectModule` contract + +For effects you want to reuse by name โ€” `namedEffect: { type: 'MyEffect', ...params }` โ€” author a module +matching `AnimationEffectAPI` and register it with `registerEffects()`. + +```typescript +type AnimationEffectAPI = { + web: (options, dom?: DomApi, config?: Record) => AnimationData[]; + getNames: (options) => string[]; + style?: (options) => AnimationData[]; // enables the CSS path (getCSSAnimation) + prepare?: (options, dom?: DomApi) => void; // measure/mutate before animating +}; + +type DomApi = { measure: MeasureCallback; mutate: MeasureCallback }; +type MeasureCallback = (fn: (target: HTMLElement | null) => void) => void; +``` + +- `web` is required โ€” it returns the `AnimationData[]` used to build the WAAPI `KeyframeEffect`(s). +- `getNames` is required โ€” it returns the animation/`@keyframes` name(s) the module produces, used to + look up existing CSS animations on an element (`getElementCSSAnimation`). +- `style` is optional โ€” implement it (same return shape as `web`) to opt into the `getCSSAnimation()` / + SSR path. See [SSR & CSS Generation](./ssr-css.md). +- `prepare` is optional โ€” a measure/mutate hook run by `prepareAnimation()` before the animation plays, + useful when the effect needs a real layout measurement it can't express purely in CSS. + +### Example: a module that measures before animating + +This effect reveals an element's width by measuring its natural rendered width in `prepare` (via +`dom.measure`), writing it as a CSS custom property (via `dom.mutate`), and animating toward that +variable in both the WAAPI and CSS paths: + +```typescript +import type { AnimationEffectAPI, AnimationData } from '@wix/motion'; + +export const RevealWidth: AnimationEffectAPI<'time'> = { + getNames: () => ['RevealWidth'], + + prepare(_options, dom) { + dom?.measure((target) => { + const width = target?.getBoundingClientRect().width ?? 0; + + dom.mutate((mutateTarget) => { + (mutateTarget as HTMLElement | null)?.style.setProperty('--reveal-width', `${width}px`); + }); + }); + }, + + web(options): AnimationData[] { + return [ + { + ...options, + name: 'RevealWidth', + keyframes: [{ width: '0px' }, { width: 'var(--reveal-width)' }], + }, + ]; + }, + + style(options): AnimationData[] { + return [ + { + ...options, + name: 'RevealWidth', + keyframes: [{ width: '0px' }, { width: 'var(--reveal-width)' }], + }, + ]; + }, +}; +``` + +Register it, then reference it anywhere via `namedEffect`: + +```typescript +import { registerEffects, prepareAnimation, getWebAnimation } from '@wix/motion'; +import { RevealWidth } from './effects/RevealWidth'; + +registerEffects({ RevealWidth }); + +const bar = document.getElementById('bar')!; + +prepareAnimation(bar, { namedEffect: { type: 'RevealWidth' } }, () => { + const animation = getWebAnimation(bar, { + namedEffect: { type: 'RevealWidth' }, + duration: 600, + easing: 'ease-out', + }); + + animation?.play(); +}); +``` + +If `'RevealWidth'` were never registered, `getRegisteredEffect` would warn to the console and +`getWebAnimation` would return `null`. + +`@wix/motion-presets` ships a large catalog of effects built to this exact contract โ€” reach for it before +authoring your own module when a suitable preset already exists. + +## 4. Driving scrub scenes manually + +`getScrubScene()` is for the cases where nothing drives progress for you automatically: the +`view-progress` polyfill (no native `window.ViewTimeline`) and `pointer-move` effects. It returns scene +objects exposing `effect(_, progress)` and `destroy()`, which **you** call from your own listener. + +```typescript +function getScrubScene( + target: HTMLElement | string | null, + animationOptions: AnimationOptions, + trigger: Partial & { element?: HTMLElement }, + sceneOptions?: Record, +): ScrubScrollScene[] | ScrubPointerScene | ScrubPointerScene[] | null; +``` + +`@wix/interact` automates both of the cases below via its bundled scroll polyfill, +[`fizban`](https://github.com/wix-incubator/fizban) โ€” reach for `getScrubScene` directly only when you +need to drive progress yourself outside of `@wix/interact`. + +### Scroll (`view-progress` polyfill) + +When `window.ViewTimeline` is unavailable, `getScrubScene` returns a `ScrubScrollScene[]` โ€” one entry per +partial animation, each exposing `viewSource`, `getProgress()`, `effect(_, progress)`, and `destroy()`. + +```typescript +import { getScrubScene } from '@wix/motion'; + +const scrollRoot = document.getElementById('scrollRoot')!; +const target = document.getElementById('parallax')!; + +const scenes = getScrubScene( + target, + { + keyframeEffect: { + name: 'parallax', + keyframes: [{ transform: 'translateY(80px)' }, { transform: 'translateY(-80px)' }], + }, + startOffset: { name: 'cover', offset: { value: 0, unit: 'percentage' } }, + endOffset: { name: 'cover', offset: { value: 100, unit: 'percentage' } }, + }, + { trigger: 'view-progress', element: scrollRoot }, +); + +// getScrubScene can return null if the underlying animation couldn't be created. +if (Array.isArray(scenes)) { + scenes.forEach((scene) => { + function onScroll() { + const progress = computeProgress(scene.viewSource); // your own 0..1 calculation + scene.effect(null, progress); + } + + window.addEventListener('scroll', onScroll, { passive: true }); + + // on teardown: + // window.removeEventListener('scroll', onScroll); + // scene.destroy(); + }); +} +``` + +### Pointer (`pointer-move`) + +For pointer-driven effects, `getScrubScene` returns a single `ScrubPointerScene`. Drive it from your own +`pointermove` listener with normalized `{ x, y }` coordinates: + +```typescript +import { getScrubScene } from '@wix/motion'; + +const card = document.getElementById('card')!; + +const scene = getScrubScene( + card, + { + keyframeEffect: { + name: 'tilt', + keyframes: [{ transform: 'rotate(-6deg)' }, { transform: 'rotate(6deg)' }], + }, + }, + { trigger: 'pointer-move', element: card, axis: 'x' }, // axis lives on the trigger, not the effect +); + +if (scene && !Array.isArray(scene)) { + function onPointerMove(event: PointerEvent) { + const rect = card.getBoundingClientRect(); + const x = (event.clientX - rect.left) / rect.width; + const y = (event.clientY - rect.top) / rect.height; + + scene!.effect(null, { x, y }); + } + + card.addEventListener('pointermove', onPointerMove); + + // on teardown: + // card.removeEventListener('pointermove', onPointerMove); + // scene.destroy(); +} +``` + +## 5. `data-motion-part` sub-targeting + +An effect's `AnimationData` can target a descendant of the animated element instead of the element +itself, via the `part` field: + +```typescript +web(options): AnimationData[] { + return [{ ...options, part: 'BG_LAYER', name: 'bg-zoom', keyframes: [/* ... */] }]; +} +``` + +At runtime this resolves against `[data-motion-part~=""]` (matching the target element itself or a +descendant). On the CSS path (`getCSSAnimation`), the generated selector becomes +`#[data-motion-part~=""]`. Mark the intended sub-element in your markup: + +```html +
+
...
+
+``` + +## See also + +- [Core Functions](../api/core-functions.md) โ€” full signatures for `getWebAnimation`, `getScrubScene`, + `registerEffects`, and friends. +- [Type Definitions](../api/types.md) โ€” `AnimationEffectAPI`, `EffectModule`, `DomApi`, `ScrubScrollScene`, + `ScrubPointerScene`, and the rest of the authoring types. +- [Core Concepts](../core-concepts.md) โ€” the effect-definition modes and trigger model this guide builds + on. +- [SSR & CSS Generation](./ssr-css.md) โ€” the `style()` hook and the `getCSSAnimation()` path. diff --git a/packages/motion/docs/guides/framework-integration.md b/packages/motion/docs/guides/framework-integration.md deleted file mode 100644 index 961f1f6b..00000000 --- a/packages/motion/docs/guides/framework-integration.md +++ /dev/null @@ -1,1122 +0,0 @@ -# Framework Integration - -Comprehensive guide for integrating Wix Motion with popular frontend frameworks including React, Vue, Angular, and others. - -## Overview - -Wix Motion is framework-agnostic but requires careful integration with reactive frameworks to avoid memory leaks and ensure optimal performance. This guide provides patterns, hooks, components, and best practices for each major framework. - -### Integration Principles - -1. **Lifecycle Management** - Properly create and destroy animations with component lifecycles -2. **Reactive Updates** - Handle dynamic prop changes and state updates -3. **Performance Optimization** - Minimize re-renders and unnecessary animation recreations -4. **TypeScript Support** - Maintain strong typing across framework boundaries -5. **SSR Compatibility** - Handle server-side rendering gracefully - -## React Integration - -### Core React Hook - -```typescript -import { useRef, useEffect, useCallback, useMemo } from 'react'; -import { getWebAnimation, AnimationGroup, AnimationOptions } from '@wix/motion'; - -interface UseAnimationOptions { - autoPlay?: boolean; - dependencies?: React.DependencyList; - disabled?: boolean; -} - -export function useAnimation( - animationOptions: AnimationOptions, - options: UseAnimationOptions = {}, -): { - ref: React.RefObject; - animation: AnimationGroup | null; - play: () => Promise; - pause: () => void; - cancel: () => void; - progress: (p: number) => void; -} { - const elementRef = useRef(null); - const animationRef = useRef(null); - const { autoPlay = false, dependencies = [], disabled = false } = options; - - // Memoize animation options to prevent unnecessary recreations - const memoizedOptions = useMemo(() => animationOptions, dependencies); - - // Create animation when element and options are ready - useEffect(() => { - if (!elementRef.current || disabled) return; - - // Clean up previous animation - if (animationRef.current) { - animationRef.current.cancel(); - } - - // Create new animation - animationRef.current = getWebAnimation(elementRef.current, memoizedOptions); - - // Auto-play if requested - if (autoPlay) { - animationRef.current.play(); - } - - // Cleanup on unmount - return () => { - if (animationRef.current) { - animationRef.current.cancel(); - animationRef.current = null; - } - }; - }, [memoizedOptions, autoPlay, disabled]); - - // Animation control methods - const play = useCallback(async () => { - if (animationRef.current) { - await animationRef.current.play(); - } - }, []); - - const pause = useCallback(() => { - if (animationRef.current) { - animationRef.current.pause(); - } - }, []); - - const cancel = useCallback(() => { - if (animationRef.current) { - animationRef.current.cancel(); - } - }, []); - - const progress = useCallback((p: number) => { - if (animationRef.current) { - animationRef.current.progress(p); - } - }, []); - - return { - ref: elementRef, - animation: animationRef.current, - play, - pause, - cancel, - progress, - }; -} -``` - -### React Components - -#### Basic Animation Component - -```typescript -import React, { forwardRef } from 'react'; -import { useAnimation } from './useAnimation'; -import { AnimationOptions } from '@wix/motion'; - -interface AnimatedElementProps extends React.HTMLAttributes { - animationOptions: AnimationOptions; - autoPlay?: boolean; - disabled?: boolean; - onAnimationComplete?: () => void; - children?: React.ReactNode; -} - -export const AnimatedElement = forwardRef( - ({ animationOptions, autoPlay, disabled, onAnimationComplete, children, ...props }, forwardedRef) => { - const { ref, animation, play } = useAnimation(animationOptions, { - autoPlay, - disabled, - dependencies: [animationOptions] - }); - - // Handle animation completion - React.useEffect(() => { - if (animation && onAnimationComplete) { - animation.onFinish(onAnimationComplete); - } - }, [animation, onAnimationComplete]); - - return ( -
{ - // Handle both forwarded ref and internal ref - if (typeof forwardedRef === 'function') { - forwardedRef(node); - } else if (forwardedRef) { - forwardedRef.current = node; - } - (ref as React.MutableRefObject).current = node; - }} - {...props} - > - {children} -
- ); - } -); -``` - -#### Advanced Animation Component with State - -```typescript -interface AnimatedContainerProps { - children: React.ReactNode; - animationType: 'fadeIn' | 'slideIn' | 'bounceIn'; - direction?: 'top' | 'right' | 'bottom' | 'left'; - duration?: number; - trigger?: 'immediate' | 'intersection' | 'manual'; - className?: string; -} - -export const AnimatedContainer: React.FC = ({ - children, - animationType, - direction = 'bottom', - duration = 800, - trigger = 'intersection', - className -}) => { - const [shouldAnimate, setShouldAnimate] = React.useState(trigger === 'immediate'); - - // Generate animation options based on props - const animationOptions = React.useMemo((): AnimationOptions => { - const baseOptions = { - type: 'TimeAnimationOptions' as const, - duration, - easing: 'easeOut' - }; - - switch (animationType) { - case 'fadeIn': - return { - ...baseOptions, - namedEffect: { type: 'FadeIn' } - }; - case 'slideIn': - return { - ...baseOptions, - namedEffect: { type: 'SlideIn', direction } - }; - case 'bounceIn': - return { - ...baseOptions, - namedEffect: { type: 'BounceIn', direction } - }; - default: - return { - ...baseOptions, - namedEffect: { type: 'FadeIn' } - }; - } - }, [animationType, direction, duration]); - - const { ref } = useAnimation(animationOptions, { - autoPlay: shouldAnimate, - disabled: !shouldAnimate, - dependencies: [animationOptions, shouldAnimate] - }); - - // Intersection observer for trigger - React.useEffect(() => { - if (trigger !== 'intersection' || !ref.current) return; - - const observer = new IntersectionObserver( - (entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - setShouldAnimate(true); - observer.unobserve(entry.target); - } - }); - }, - { threshold: 0.1 } - ); - - observer.observe(ref.current); - - return () => observer.disconnect(); - }, [trigger]); - - return ( -
- {children} -
- ); -}; -``` - -#### Scroll Animation Hook - -[TBD] - -### React Usage Examples - -```tsx -// Basic usage -function App() { - return ( - -

Welcome to our site!

-

This content slides in when visible.

-
- ); -} - -// Advanced usage with custom hook -function CustomAnimatedComponent() { - const { ref, play, pause, progress } = useAnimation({ - type: 'TimeAnimationOptions', - namedEffect: { type: 'BounceIn' }, - duration: 1000, - }); - - const [isPlaying, setIsPlaying] = useState(false); - - const handleToggle = () => { - if (isPlaying) { - pause(); - } else { - play(); - } - setIsPlaying(!isPlaying); - }; - - return ( -
-
- Animated Content -
- - progress(parseFloat(e.target.value))} - /> -
- ); -} - -// Scroll animation usage -[TBD]; -``` - -## Vue Integration - -### Vue Composables - -```typescript -// useAnimation.ts -import { ref, onMounted, onUnmounted, watch, Ref } from 'vue'; -import { getWebAnimation, AnimationGroup, AnimationOptions } from '@wix/motion'; - -export function useAnimation( - elementRef: Ref, - animationOptions: Ref | AnimationOptions, - options: { - autoPlay?: boolean; - disabled?: Ref | boolean; - } = {}, -) { - const animation = ref(null); - const isPlaying = ref(false); - const progress = ref(0); - - const { autoPlay = false, disabled = false } = options; - - function createAnimation() { - if (!elementRef.value) return; - - // Clean up existing animation - if (animation.value) { - animation.value.cancel(); - } - - const options = - typeof animationOptions === 'object' && 'value' in animationOptions - ? animationOptions.value - : animationOptions; - - animation.value = getWebAnimation(elementRef.value, options); - - if (autoPlay) { - play(); - } - } - - async function play() { - if (animation.value) { - isPlaying.value = true; - await animation.value.play(); - } - } - - function pause() { - if (animation.value) { - animation.value.pause(); - isPlaying.value = false; - } - } - - function cancel() { - if (animation.value) { - animation.value.cancel(); - isPlaying.value = false; - progress.value = 0; - } - } - - function setProgress(p: number) { - if (animation.value) { - animation.value.progress(p); - progress.value = p; - } - } - - // Watch for changes in animation options - watch( - () => - typeof animationOptions === 'object' && 'value' in animationOptions - ? animationOptions.value - : animationOptions, - createAnimation, - { deep: true }, - ); - - // Watch for element changes - watch(elementRef, createAnimation); - - // Watch for disabled state - watch( - () => (typeof disabled === 'object' && 'value' in disabled ? disabled.value : disabled), - (isDisabled) => { - if (isDisabled && animation.value) { - animation.value.cancel(); - } else if (!isDisabled) { - createAnimation(); - } - }, - ); - - onMounted(createAnimation); - - onUnmounted(() => { - if (animation.value) { - animation.value.cancel(); - } - }); - - return { - animation: readonly(animation), - isPlaying: readonly(isPlaying), - progress: readonly(progress), - play, - pause, - cancel, - setProgress, - }; -} -``` - -### Vue Components - -```vue - - - - -``` - -```vue - -[TBD] -``` - -### Vue Usage Examples - -```vue - - - - -``` - -## Angular Integration - -### Angular Service - -```typescript -// animation.service.ts -import { Injectable, OnDestroy } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { - getWebAnimation, - getScrubScene, - AnimationGroup, - AnimationOptions, - ScrubAnimationOptions, -} from '@wix/motion'; - -@Injectable({ - providedIn: 'root', -}) -export class AnimationService implements OnDestroy { - private animations = new Map(); - private animationStates = new Map>(); - - createAnimation( - id: string, - element: HTMLElement, - options: AnimationOptions, - ): Observable { - // Clean up existing animation - this.destroyAnimation(id); - - const animation = getWebAnimation(element, options); - this.animations.set(id, animation); - - // Create state subject - const stateSubject = new BehaviorSubject({ - id, - state: 'idle', - progress: 0, - }); - this.animationStates.set(id, stateSubject); - - // Monitor animation state - this.monitorAnimation(id, animation, stateSubject); - - return stateSubject.asObservable(); - } - - createScrollAnimation( - id: string, - element: HTMLElement, - options: ScrubAnimationOptions, - trigger?: any, - ): Observable { - // Clean up existing - this.destroyAnimation(id); - - const scene = getScrubScene( - element, - options, - trigger || { - trigger: 'view-progress', - element: document.body, - }, - ); - - // Store scene reference - (this.animations as any).set(id, scene); - - const stateSubject = new BehaviorSubject({ - id, - state: 'idle', - progress: 0, - }); - this.animationStates.set(id, stateSubject); - - // Monitor scroll progress - this.monitorScrollAnimation(id, scene, stateSubject); - - return stateSubject.asObservable(); - } - - private monitorAnimation( - id: string, - animation: AnimationGroup, - stateSubject: BehaviorSubject, - ) { - const updateState = () => { - const state: AnimationState = { - id, - state: animation.playState, - progress: animation.getProgress(), - }; - stateSubject.next(state); - - if (animation.playState === 'running') { - requestAnimationFrame(updateState); - } - }; - - animation.ready.then(() => { - updateState(); - }); - - animation.onFinish(() => { - stateSubject.next({ - id, - state: 'finished', - progress: 1, - }); - }); - } - - private monitorScrollAnimation( - id: string, - scene: any, - stateSubject: BehaviorSubject, - ) { - const updateProgress = () => { - if (Array.isArray(scene) && scene[0]) { - const progress = scene[0].getProgress(); - stateSubject.next({ - id, - state: 'running', - progress, - }); - } - - requestAnimationFrame(updateProgress); - }; - - updateProgress(); - } - - async playAnimation(id: string): Promise { - const animation = this.animations.get(id); - if (animation && 'play' in animation) { - await animation.play(); - } - } - - pauseAnimation(id: string): void { - const animation = this.animations.get(id); - if (animation && 'pause' in animation) { - animation.pause(); - } - } - - setAnimationProgress(id: string, progress: number): void { - const animation = this.animations.get(id); - if (animation && 'progress' in animation) { - animation.progress(progress); - } - } - - destroyAnimation(id: string): void { - const animation = this.animations.get(id); - if (animation) { - if ('cancel' in animation) { - animation.cancel(); - } else if ('destroy' in animation) { - (animation as any).destroy(); - } - this.animations.delete(id); - } - - const stateSubject = this.animationStates.get(id); - if (stateSubject) { - stateSubject.complete(); - this.animationStates.delete(id); - } - } - - ngOnDestroy(): void { - // Clean up all animations - this.animations.forEach((_, id) => { - this.destroyAnimation(id); - }); - } -} - -interface AnimationState { - id: string; - state: 'idle' | 'running' | 'paused' | 'finished'; - progress: number; -} -``` - -### Angular Directive - -```typescript -// animated.directive.ts -import { - Directive, - ElementRef, - Input, - OnInit, - OnDestroy, - Output, - EventEmitter, - OnChanges, - SimpleChanges, -} from '@angular/core'; -import { AnimationService } from './animation.service'; -import { AnimationOptions } from '@wix/motion'; -import { Subscription } from 'rxjs'; - -@Directive({ - selector: '[appAnimated]', -}) -export class AnimatedDirective implements OnInit, OnDestroy, OnChanges { - @Input('appAnimated') animationOptions!: AnimationOptions; - @Input() autoPlay = false; - @Input() disabled = false; - @Input() animationId?: string; - - @Output() animationStateChange = new EventEmitter(); - @Output() animationComplete = new EventEmitter(); - - private id: string; - private subscription?: Subscription; - - constructor( - private elementRef: ElementRef, - private animationService: AnimationService, - ) { - this.id = this.animationId || `animation_${Math.random().toString(36).substr(2, 9)}`; - } - - ngOnInit(): void { - this.createAnimation(); - } - - ngOnChanges(changes: SimpleChanges): void { - if (changes['animationOptions'] && !changes['animationOptions'].firstChange) { - this.createAnimation(); - } - - if (changes['disabled']) { - if (this.disabled) { - this.animationService.destroyAnimation(this.id); - } else { - this.createAnimation(); - } - } - } - - ngOnDestroy(): void { - this.animationService.destroyAnimation(this.id); - if (this.subscription) { - this.subscription.unsubscribe(); - } - } - - private createAnimation(): void { - if (this.disabled || !this.animationOptions) return; - - this.subscription?.unsubscribe(); - - this.subscription = this.animationService - .createAnimation(this.id, this.elementRef.nativeElement, this.animationOptions) - .subscribe((state) => { - this.animationStateChange.emit(state); - - if (state.state === 'finished') { - this.animationComplete.emit(); - } - }); - - if (this.autoPlay) { - this.play(); - } - } - - async play(): Promise { - await this.animationService.playAnimation(this.id); - } - - pause(): void { - this.animationService.pauseAnimation(this.id); - } - - setProgress(progress: number): void { - this.animationService.setAnimationProgress(this.id, progress); - } -} -``` - -### Angular Component - -```typescript -// animated-container.component.ts -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; -import { AnimationOptions } from '@wix/motion'; - -@Component({ - selector: 'app-animated-container', - template: ` -
- -
- `, - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class AnimatedContainerComponent { - @Input() animationOptions!: AnimationOptions; - @Input() autoPlay = false; - @Input() disabled = false; - @Input() className?: string; - - @Output() animationStateChange = new EventEmitter(); - @Output() animationComplete = new EventEmitter(); - - onStateChange(state: any): void { - this.animationStateChange.emit(state); - } -} -``` - -### Angular Usage Examples - -```typescript -// app.component.ts -import { Component } from '@angular/core'; -import { AnimationOptions } from '@wix/motion'; - -@Component({ - selector: 'app-root', - template: ` -
- - -

Welcome to our Angular app!

-
- - -
-

Controlled animation content

-
- - - - - -
Dynamic content
-
- - -
- `, -}) -export class AppComponent { - fadeInOptions: AnimationOptions = { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 800, - }; - - slideInOptions: AnimationOptions = { - type: 'TimeAnimationOptions', - namedEffect: { type: 'SlideIn', direction: 'bottom' }, - duration: 1000, - }; - - dynamicOptions: AnimationOptions = this.fadeInOptions; - shouldAutoPlay = false; - - onFadeComplete(): void { - console.log('Fade animation completed'); - } - - onStateChange(state: any): void { - console.log('Animation state:', state); - } - - changeAnimation(): void { - this.dynamicOptions = - this.dynamicOptions === this.fadeInOptions ? this.slideInOptions : this.fadeInOptions; - this.shouldAutoPlay = true; - } -} -``` - -## Universal Patterns - -### SSR-Safe Animation Initialization - -```typescript -// ssr-safe-animation.ts -export function createSSRSafeAnimation( - element: HTMLElement | null, - options: AnimationOptions, - fallback?: () => void, -): AnimationGroup | null { - // Check if we're in browser environment - if (typeof window === 'undefined' || typeof document === 'undefined') { - console.warn('Animations not available in SSR environment'); - if (fallback) fallback(); - return null; - } - - // Check for animation support - if (!element || !('animate' in HTMLElement.prototype)) { - console.warn('Web Animations API not supported'); - if (fallback) fallback(); - return null; - } - - try { - return getWebAnimation(element, options); - } catch (error) { - console.error('Animation creation failed:', error); - if (fallback) fallback(); - return null; - } -} -``` - -### Cross-Framework Animation Manager - -```typescript -// universal-animation-manager.ts -interface FrameworkAdapter { - onMount?: (callback: () => void) => void; - onUnmount?: (callback: () => void) => void; - watch?: (value: () => T, callback: (newValue: T) => void) => void; - ref?: (initialValue: T) => { value: T }; -} - -export class UniversalAnimationManager { - private animations = new Map(); - private adapter: FrameworkAdapter; - - constructor(adapter: FrameworkAdapter) { - this.adapter = adapter; - } - - createAnimation( - id: string, - getElement: () => HTMLElement | null, - getOptions: () => AnimationOptions, - config: { - autoPlay?: boolean; - dependencies?: any[]; - } = {}, - ) { - const createFn = () => { - const element = getElement(); - const options = getOptions(); - - if (!element) return; - - // Clean up existing - this.destroyAnimation(id); - - // Create new animation - const animation = createSSRSafeAnimation(element, options, () => { - // Fallback: just ensure element is visible - element.style.opacity = '1'; - }); - - if (animation) { - this.animations.set(id, animation); - - if (config.autoPlay) { - animation.play(); - } - } - }; - - // Initial creation - if (this.adapter.onMount) { - this.adapter.onMount(createFn); - } else { - createFn(); - } - - // Watch for changes - if (this.adapter.watch && config.dependencies) { - this.adapter.watch(getOptions, createFn); - } - - // Cleanup - if (this.adapter.onUnmount) { - this.adapter.onUnmount(() => { - this.destroyAnimation(id); - }); - } - - return { - play: () => this.animations.get(id)?.play(), - pause: () => this.animations.get(id)?.pause(), - cancel: () => this.destroyAnimation(id), - progress: (p: number) => this.animations.get(id)?.progress(p), - }; - } - - destroyAnimation(id: string) { - const animation = this.animations.get(id); - if (animation) { - animation.cancel(); - this.animations.delete(id); - } - } - - destroyAll() { - this.animations.forEach((animation) => animation.cancel()); - this.animations.clear(); - } -} -``` - -## Best Practices Summary - -### โœ… Do - -- **Cleanup animations** on component unmount -- **Use refs/reactive patterns** for element access -- **Memoize animation options** to prevent unnecessary recreations -- **Handle SSR gracefully** with proper environment checks -- **Implement error boundaries** for animation failures - -### โŒ Don't - -- **Forget to clean up** animations and event listeners -- **Recreate animations** on every prop change -- **Block rendering** with synchronous custom animation operations - -### Performance Tips - -1. **Lazy load animations below the fold** for better initial page load -2. **Use CSS animations** with SSR as much as possible -3. **Batch DOM operations** when creating multiple animations -4. **Implement intersection observers** for viewport-based animations -5. **Monitor performance** and adapt animation complexity - ---- - -**Complete**: You now have comprehensive framework integration patterns for Wix Motion. Return to the [Guide Overview](README.md) or continue with [Testing](testing.md) for animation testing strategies. diff --git a/packages/motion/docs/guides/performance.md b/packages/motion/docs/guides/performance.md index 5db58f96..87c7d0aa 100644 --- a/packages/motion/docs/guides/performance.md +++ b/packages/motion/docs/guides/performance.md @@ -1,468 +1,76 @@ -# Performance Optimization +# Performance -Complete guide to optimizing Wix Motion animations for smooth 60fps performance across all devices and use cases. +`@wix/motion` is built on the Web Animations API and CSS Animations, which already run off the main +thread where possible. The guidance below is what actually affects performance in this package โ€” nothing +more. -## Overview +## Prefer `transform` and `opacity` -Wix Motion is designed for high performance, but complex animations and large-scale implementations require careful optimization. This guide covers performance principles, optimization techniques, and best practices for maintaining smooth animations. +Both are compositor-only properties in modern browsers โ€” animating them avoids layout and paint. Animate +`transform: translate()/scale()/rotate()` instead of `top`/`left`/`width`/`height`, and `opacity` instead +of `visibility`-style fades. -### Performance Principles +## Let `fastdom` batch your DOM reads and writes -1. **Minimize Layout Thrashing** - Use only transform, fitler, and opacity properties when possible -2. **Batch DOM Operations** - Group measurements and mutations using `fastdom` -3. **Optimize Animation Lifecycle** - Prepare, create, and clean up efficiently -4. **Choose Appropriate Rendering** - Balance CSS vs JavaScript animations -5. **Respect Device Capabilities** - Adapt to mobile and low-power devices - -## Rendering Strategy Selection - -### CSS Animations vs Web Animations API - -Choose the right rendering approach based on your use case: - -#### Use CSS Animations (`getCSSAnimation`) where possible: +`@wix/motion`'s only runtime dependency is [`fastdom`](https://github.com/wilsonpage/fastdom), which +batches DOM reads (`measure`) and writes (`mutate`) into separate animation-frame phases to avoid layout +thrashing. `prepareAnimation()` uses it to run an effect's optional `prepare(options, domApi)` hook โ€” +measure, then mutate โ€” before the animation is created: ```typescript -const cssRules = getCSSAnimation('hero-element', { - type: 'TimeAnimationOptions', - namedEffect: { type: 'FadeIn' }, - duration: 800 -}); - -// Insert into a stylesheet -const sheet = new CSSStyleSheet(); -style.insertRule(cssRules); -document.adoptedStyleSheets.push(sheet); +import { prepareAnimation, getWebAnimation } from '@wix/motion'; -// OR render to a style tag on server-side using plain strings -`${cssRules}` - -// OR render to a style tag on server-side using a framework (e.g. React) - -``` - -#### Use Web Animations API (`getWebAnimation`) For: - -```typescript -// โœ… Interactive animations requiring control -const interactiveAnimation = getWebAnimation(element, { - type: 'TimeAnimationOptions', - namedEffect: { type: 'BounceIn' }, +prepareAnimation(element, animationOptions, () => { + // Runs inside a fastdom.mutate โ€” safe to read layout-affecting styles + // without triggering an extra reflow. + getWebAnimation(element, animationOptions)?.play(); }); - -// Control based on user interaction -button.addEventListener('click', async () => { - await interactiveAnimation.play(); -}); - -// โœ… Dynamic parameter changes -const dynamicAnimation = getWebAnimation(element, baseOptions); - -// Change speed based on performance -if (performance.now() - lastFrame > 16) { - dynamicAnimation.setPlaybackRate(0.5); // Slow down if dropping frames -} - -// โœ… Complex timing sequences -const sequence = [ - getWebAnimation(intro, introOptions), - getWebAnimation(main, mainOptions), - getWebAnimation(outro, outroOptions), -]; - -for (const animation of sequence) { - await animation.play(); - await animation.finished; -} ``` -## DOM Performance Optimization +If you're animating many elements at once, prefer registering one effect module (with a single +`prepare` hook) over triggering many individual measurements โ€” `fastdom` can only batch what's scheduled +through it. -### Efficient Measurement and Mutation +## Use the CSS path for fire-and-forget effects -Use `fastdom` to batch DOM operations and prevent layout thrashing: +`getCSSAnimation()` generates `@keyframes`/`animation` descriptors instead of a live `Animation` +instance. Once the resulting CSS is in a stylesheet, the browser drives the animation on the compositor +thread with no per-frame JavaScript involvement โ€” see [SSR & CSS Generation](./ssr-css.md). Reach for +`getWebAnimation()` (WAAPI) when you need playback control (`pause`, `reverse`, `setPlaybackRate`, +`onFinish`) or dynamic, runtime-computed keyframes. -```typescript -import fastdom from 'fastdom'; +## No animation loop unless you ask for one -class OptimizedAnimationManager { - private measurementQueue: (() => void)[] = []; - private mutationQueue: (() => void)[] = []; +Neither `getWebAnimation()` nor `getCSSAnimation()` runs a `requestAnimationFrame` loop. The only case +that does is a `customEffect` callback (see [Custom Effects](./custom-effects.md)) โ€” `CustomAnimation` +runs an rAF loop for the lifetime of that animation to call your function on progress changes. If you use +`customEffect` for an infinite or scroll/pointer-driven effect, `cancel()` it (or otherwise stop the +underlying `Animation`) when the element leaves the viewport or unmounts, rather than leaving the loop +running. - // Batch measurements - queueMeasurement(fn: () => void) { - this.measurementQueue.push(fn); +## Respect reduced motion - if (this.measurementQueue.length === 1) { - fastdom.measure(() => { - this.measurementQueue.forEach((measurement) => measurement()); - this.measurementQueue = []; - }); - } - } +Pass `{ reducedMotion: true }` as the 4th argument to `getWebAnimation()` (or via `context.reducedMotion` +to `getAnimation()` / `getSequence()`) to apply the user's `prefers-reduced-motion` preference. It only +affects time-based (non-scrub) animations: - // Batch mutations - queueMutation(fn: () => void) { - this.mutationQueue.push(fn); - - if (this.mutationQueue.length === 1) { - fastdom.mutate(() => { - this.mutationQueue.forEach((mutation) => mutation()); - this.mutationQueue = []; - }); - } - } - - // Optimized element preparation - prepareElements(elements: HTMLElement[], options: AnimationOptions[]) { - // Batch all measurements first - const measurements: any[] = []; - - fastdom.measure(() => { - elements.forEach((element, index) => { - measurements[index] = { - width: element.offsetWidth, - height: element.offsetHeight, - bounds: element.getBoundingClientRect(), - }; - }); - }); - - // Then batch all preparations - fastdom.mutate(() => { - elements.forEach((element, index) => { - prepareAnimation(element, options[index]); - }); - }); - } -} -``` +- Single-iteration animations (`iterations: 1` or unset) collapse to a `1ms` duration. +- Multi-iteration animations (`iterations: 0` or `> 1`) are dropped entirely โ€” `getWebAnimation()` returns + `null`. -### Minimize Reflows and Repaints - -#### โœ… Preferred Properties (GPU Accelerated) +Guard the return value accordingly: ```typescript -// Use transform instead of changing position -const optimizedMove = getWebAnimation(element, { - type: 'TimeAnimationOptions', - keyframeEffect: { - type: 'KeyframeEffect', - name: 'optimizedMove', - keyframes: [ - { transform: 'translateX(0px)' }, // โœ… GPU accelerated - { transform: 'translateX(100px)' }, - ], - }, -}); - -// Use scale instead of width/height -const optimizedResize = getWebAnimation(element, { - type: 'TimeAnimationOptions', - keyframeEffect: { - type: 'KeyframeEffect', - name: 'optimizedResize', - keyframes: [ - { transform: 'scale(0.5)' }, // โœ… GPU accelerated - { transform: 'scale(1)' }, - ], - }, +const animation = getWebAnimation(element, animationOptions, undefined, { + reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches, }); -``` - -#### โŒ Avoid Layout Properties - -```typescript -// โŒ These properties cause expensive layout recalculations -const expensiveAnimation = { - keyframes: [ - { width: '100px', height: '100px', left: '0px', top: '0px' }, - { width: '200px', height: '200px', left: '50px', top: '50px' }, - ], -}; - -// โœ… Use transforms instead -const efficientAnimation = { - keyframes: [ - { transform: 'translate(0px, 0px) scale(1)' }, - { transform: 'translate(50px, 50px) scale(2)' }, - ], -}; -``` - -## Performance Monitoring and Debugging - -### Animation Performance Profiler - -```typescript -class AnimationProfiler { - private profiles = new Map(); - - startProfiling(animationId: string) { - const profile: PerformanceProfile = { - startTime: performance.now(), - frameCount: 0, - droppedFrames: 0, - totalDuration: 0, - lastFrameTime: performance.now(), - }; - - this.profiles.set(animationId, profile); - this.monitorAnimation(animationId); - } - - private monitorAnimation(animationId: string) { - const profile = this.profiles.get(animationId); - if (!profile) return; - - const monitor = () => { - const now = performance.now(); - const frameTime = now - profile.lastFrameTime; - - profile.frameCount++; - profile.totalDuration = now - profile.startTime; - - // Detect dropped frames (> 16.67ms for 60fps) - if (frameTime > 20) { - profile.droppedFrames++; - } - - profile.lastFrameTime = now; - - // Continue monitoring - if (this.profiles.has(animationId)) { - requestAnimationFrame(monitor); - } - }; - - requestAnimationFrame(monitor); - } - stopProfiling(animationId: string): PerformanceReport | null { - const profile = this.profiles.get(animationId); - if (!profile) return null; - - this.profiles.delete(animationId); - - const avgFPS = (profile.frameCount / profile.totalDuration) * 1000; - const dropRate = (profile.droppedFrames / profile.frameCount) * 100; - - return { - animationId, - duration: profile.totalDuration, - averageFPS: avgFPS, - frameDropRate: dropRate, - totalFrames: profile.frameCount, - droppedFrames: profile.droppedFrames, - performance: this.getPerformanceRating(avgFPS, dropRate), - }; - } - - private getPerformanceRating(fps: number, dropRate: number): 'excellent' | 'good' | 'poor' { - if (fps >= 55 && dropRate < 5) return 'excellent'; - if (fps >= 45 && dropRate < 10) return 'good'; - return 'poor'; - } -} - -interface PerformanceProfile { - startTime: number; - frameCount: number; - droppedFrames: number; - totalDuration: number; - lastFrameTime: number; -} - -interface PerformanceReport { - animationId: string; - duration: number; - averageFPS: number; - frameDropRate: number; - totalFrames: number; - droppedFrames: number; - performance: 'excellent' | 'good' | 'poor'; -} +animation?.play(); ``` -### Debug Tools - -```typescript -class AnimationDebugger { - private isDebugMode = false; - private debugOverlay: HTMLElement | null = null; - - enableDebugMode() { - this.isDebugMode = true; - this.createDebugOverlay(); - } - - disableDebugMode() { - this.isDebugMode = false; - this.removeDebugOverlay(); - } - - logAnimationStats(animation: AnimationGroup, label: string) { - if (!this.isDebugMode) return; - - console.group(`๐ŸŽฌ Animation: ${label}`); - console.log('State:', animation.playState); - console.log('Progress:', animation.getProgress()); - console.log('Animation count:', animation.animations.length); - - // Log individual animation details - animation.animations.forEach((anim, index) => { - console.log(`Animation ${index}:`, { - effect: anim.effect, - playState: anim.playState, - currentTime: anim.currentTime, - startTime: anim.startTime, - }); - }); - - console.groupEnd(); - } - - measureAnimationImpact( - element: HTMLElement, - beforeCallback: () => void, - afterCallback: () => void, - ) { - if (!this.isDebugMode) return; - - // Measure before animation - const beforeMetrics = this.captureMetrics(); - beforeCallback(); - - // Measure after animation - requestAnimationFrame(() => { - const afterMetrics = this.captureMetrics(); - this.reportMetricsDiff(beforeMetrics, afterMetrics); - afterCallback(); - }); - } - - private captureMetrics() { - return { - timestamp: performance.now(), - memory: (performance as any).memory - ? { - used: (performance as any).memory.usedJSHeapSize, - total: (performance as any).memory.totalJSHeapSize, - } - : null, - timing: performance.getEntriesByType('measure').length, - }; - } - - private reportMetricsDiff(before: any, after: any) { - console.group('๐Ÿ“Š Performance Impact'); - console.log('Duration:', after.timestamp - before.timestamp, 'ms'); - - if (before.memory && after.memory) { - const memoryDiff = after.memory.used - before.memory.used; - console.log('Memory change:', memoryDiff, 'bytes'); - } - - console.groupEnd(); - } - - private createDebugOverlay() { - if (this.debugOverlay) return; - - this.debugOverlay = document.createElement('div'); - this.debugOverlay.style.cssText = ` - position: fixed; - top: 10px; - right: 10px; - background: rgb(0 0 0 / 0.8); - color: white; - padding: 10px; - border-radius: 5px; - font-family: monospace; - font-size: 12px; - z-index: 10000; - pointer-events: none; - `; - - document.body.appendChild(this.debugOverlay); - this.updateDebugOverlay(); - } - - private updateDebugOverlay() { - if (!this.debugOverlay) return; - - const updateStats = () => { - if (!this.debugOverlay) return; - - const memory = (performance as any).memory; - const fps = this.calculateFPS(); - - this.debugOverlay.innerHTML = ` -
FPS: ${fps.toFixed(1)}
- ${memory ? `
Memory: ${(memory.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB
` : ''} -
Animations: ${document.getAnimations().length}
- `; - - if (this.isDebugMode) { - requestAnimationFrame(updateStats); - } - }; - - updateStats(); - } - - private calculateFPS(): number { - // Simple FPS calculation - let lastTime = performance.now(); - let frameCount = 0; - let fps = 60; - - const measure = () => { - frameCount++; - const currentTime = performance.now(); - - if (currentTime - lastTime >= 1000) { - fps = frameCount; - frameCount = 0; - lastTime = currentTime; - } - - requestAnimationFrame(measure); - }; - - measure(); - return fps; - } - - private removeDebugOverlay() { - if (this.debugOverlay) { - this.debugOverlay.remove(); - this.debugOverlay = null; - } - } -} -``` - -## Performance Checklist - -### โœ… Pre-Animation Optimization - -- [ ] Choose appropriate rendering method (CSS vs JavaScript) -- [ ] Batch DOM measurements using `fastdom` or similar -- [ ] Pre-calculate complex values during idle time - -### โœ… Animation Creation Optimization - -- [ ] Prefer transform, opacity properties -- [ ] Set up proper cleanup for memory management -- [ ] Consider device capabilities and user preferences - -### โœ… Runtime Optimization - -- [ ] Pause infinite animations when out of viewport -- [ ] Adjust animation complexity based on performance -- [ ] Clean up animations when components unmount - ---- +## See also -**Next**: Explore [Advanced Patterns](advanced-patterns.md) for complex animation scenarios, or return to the [API Reference](../api/) for function documentation. +- [Custom Effects](./custom-effects.md) โ€” authoring `customEffect` callbacks and registered effect + modules. +- [SSR & CSS Generation](./ssr-css.md) โ€” the `getCSSAnimation()` path in depth. +- [Core Functions](../api/core-functions.md) โ€” full signatures for every function mentioned above. diff --git a/packages/motion/docs/guides/ssr-css.md b/packages/motion/docs/guides/ssr-css.md new file mode 100644 index 00000000..903f8a59 --- /dev/null +++ b/packages/motion/docs/guides/ssr-css.md @@ -0,0 +1,147 @@ +# SSR & CSS Generation + +`getCSSAnimation()` generates plain data โ€” `@keyframes` and `animation` descriptors โ€” instead of a live +`AnimationGroup`. Nothing runs; nothing touches an `Animation` instance. That makes it usable anywhere +you don't have a DOM to animate against, most importantly on the server or at build time, so an +animation's CSS is already in the page before any JavaScript executes. This is the path to avoiding a +flash of unstyled/unanimated content (FOUC). + +> **Gotcha**: `getCSSAnimation()` returns an **array of descriptor objects**, never a string. Iterate it +> to build your own stylesheet. + +## The descriptor shape + +```typescript +function getCSSAnimation( + target: string | null, + animationOptions: AnimationOptions, + trigger?: TriggerVariant, +): Array<{ + target: string; + animation: string; + composition?: CompositeOperation; + custom?: Record; + name: string; + keyframes: Record[]; + id: string | undefined; + animationTimeline: string; + animationRange: string; +}>; +``` + +`target` is a string (element id or selector) here, not an `HTMLElement` โ€” unlike `getWebAnimation`, CSS +rules target selectors, so there's no element reference to accept. One entry is returned per generated +`@keyframes`/`animation` pair (e.g. one per `data-motion-part` sub-target). + +| Field | Meaning | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `target` | Selector for the animated element or sub-part, e.g. `"#hero"` or `"#hero[data-motion-part~='icon']"`; `""` if nothing resolved. | +| `animation` | The CSS `animation` shorthand value. **Paused by default** โ€” see [below](#paused-by-default). | +| `composition` | `CompositeOperation`, if the effect set one. | +| `custom` | Custom property values referenced by the keyframes, if any. | +| `name` | The `@keyframes` name โ€” pair it with `keyframes` to build the `@keyframes` block. | +| `keyframes` | Ordered keyframe declarations (property-bag objects, not a WAAPI `Keyframe[]`) โ€” the steps of the `@keyframes` block. | +| `id` | Effect id, if `animationOptions.effectId` was set. | +| `animationTimeline` | `` `--${trigger.id}` `` for `view-progress` triggers, else `""`. Maps to the CSS `animation-timeline` property. | +| `animationRange` | e.g. `"cover 0% cover 100%"` for `view-progress` triggers, else `""`. Maps to `animation-range`. | + +## Building a stylesheet from descriptors + +The `keyframes` array holds plain property maps with no explicit offsets, so distribute them evenly +across `0%`โ€“`100%` the same way the browser does when keyframe offsets are omitted: + +```typescript +import { getCSSAnimation } from '@wix/motion'; + +function keyframesToCSS(name: string, keyframes: Record[]) { + const steps = keyframes + .map((keyframe, i) => { + const offset = keyframes.length > 1 ? (i / (keyframes.length - 1)) * 100 : 0; + const decls = Object.entries(keyframe) + .filter(([, value]) => value !== undefined) + .map(([prop, value]) => `${prop}: ${value};`) + .join(' '); + + return `${offset}% { ${decls} }`; + }) + .join('\n '); + + return `@keyframes ${name} {\n ${steps}\n}`; +} + +function ruleToCSS({ + target, + animation, + animationTimeline, + animationRange, +}: ReturnType[number]) { + if (!target) return ''; + + const extra = [ + animationTimeline ? `animation-timeline: ${animationTimeline};` : '', + animationRange ? `animation-range: ${animationRange};` : '', + ] + .filter(Boolean) + .join(' '); + + return `${target} { animation: ${animation}; ${extra} }`; +} + +const descriptors = getCSSAnimation('hero', { + namedEffect: { type: 'FadeIn' }, + duration: 800, +}); + +const css = descriptors + .map((d) => `${keyframesToCSS(d.name, d.keyframes)}\n${ruleToCSS(d)}`) + .join('\n\n'); +``` + +`css` is a plain string at this point โ€” inject it however fits your rendering target: + +```typescript +// Server-side / build-time: embed directly in the HTML response. +const html = `${markup}`; + +// Client-side, e.g. hydrating the same string: replace a stylesheet's contents. +const sheet = new CSSStyleSheet(); +sheet.replaceSync(css); +document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; +``` + +## `forCSS` and native `ViewTimeline` support + +For a `view-progress` trigger, `getWebAnimation()` picks between a native, `ViewTimeline`-linked +animation (`duration: 'auto'`) and a scrubbable polyfill duration (`99.99ms` / `0.01ms` delay) based on +whether `window.ViewTimeline` exists **at runtime**. `getCSSAnimation()` can't make that runtime check โ€” +there is no `window` on the server, and even in the browser you want the same CSS regardless of which +client renders it. So it always passes an internal `forCSS` flag that forces `duration: 'auto'` for +`view-progress` animations, independent of `window.ViewTimeline` support. This is what makes the +generated CSS safe to render ahead of time: it's native-`ViewTimeline` CSS every time, which any browser +that doesn't support `ViewTimeline` will simply treat as a paused/static animation rather than break. + +## Paused by default + +The `animation` shorthand generated for **time-based** animations is paused by default โ€” the CSS is +ready before your JavaScript runs, but it won't start playing on its own. Flip +`animation-play-state: running` (e.g. by toggling a class once your JS is ready, or via whatever +condition should start the animation) to let it play. `view-progress` animations aren't paused this way โ€” +they're driven by the scroll timeline instead. + +## Relationship to `@wix/interact`'s `generate()` + +`@wix/interact` builds on `getCSSAnimation()` to emit a **complete** CSS string for an entire +`InteractConfig` in one call โ€” `@keyframes`, animation/transition custom properties, view-timeline +declarations, state-selector rules, and FOUC-prevention rules that hide entrance-animated elements until +their animation starts. If you're working with `@wix/interact` configs, use its `generate()` function +instead of assembling descriptors yourself โ€” see +[`generate()` in the `@wix/interact` function reference](../../../interact/docs/api/functions.md#generate) +for the full contract. Reach for `getCSSAnimation()` directly only when you're generating CSS for a +`keyframeEffect` or `namedEffect` outside of `@wix/interact`. + +## See also + +- [Core Functions](../api/core-functions.md) โ€” the full `getCSSAnimation()` signature alongside the rest + of the core API. +- [Custom Effects](./custom-effects.md) โ€” implement an effect module's optional `style()` hook to opt + into this path. diff --git a/packages/motion/rules/css-generation.md b/packages/motion/rules/css-generation.md new file mode 100644 index 00000000..eb80db77 --- /dev/null +++ b/packages/motion/rules/css-generation.md @@ -0,0 +1,218 @@ +--- +name: css-generation +description: Read when generating CSS animations for SSR / FOUC-free rendering via getCSSAnimation. +--- + +# CSS Generation (`getCSSAnimation`) + +`getCSSAnimation` is `@wix/motion`'s SSR/FOUC-free path: it turns the same `AnimationOptions` used by +`getWebAnimation` into plain CSS building blocks (`@keyframes` + an `animation` shorthand) that can be +rendered into a ``); +``` + +On the server, write the same `css` string into the rendered HTML's `` instead of calling +`document.head.insertAdjacentHTML`. + +## The `iterations` Idiom in CSS + +```typescript +// ../src/api/cssAnimations.ts:30-31 +!iterations || iterations === Infinity ? 'infinite' : iterations; +``` + +By the time this runs, `iterations` has already been normalized by `getEffectsData` +(`../src/api/common.ts:100`): `effect.iterations === 0 ? Infinity : effect.iterations || 1`. Net +effect: **`iterations: 0` on the options โ‡’ `infinite` in the generated CSS; `undefined` โ‡’ `1`.** Use +`0`, not `Infinity`, as the idiom โ€” it's what the rest of the codebase standardizes on. + +## Relationship to `@wix/interact`'s `generate()` + +`@wix/interact` builds its own `generate(config, useFirstChild)` on top of this primitive to emit +**complete** page CSS: `@keyframes`, animation/transition custom properties, `view-timeline` +declarations, state-selector rules, coordinated-list aggregation, and โ€” critically โ€” +**FOUC-prevention initial rules** that hide `viewEnter` + `once` entrance targets until the animation +starts. If you're integrating declaratively via `@wix/interact`, call its `generate()` instead of +hand-rolling the loop above โ€” see `@wix/interact`'s `integration.md` +(`../../interact/rules/integration.md`). This file documents only the lower-level `@wix/motion` +contract `generate()` is built on; it intentionally does not duplicate `@wix/interact`'s FOUC/initial- +rule logic. + +## Gotchas / Rules + +- **MUST** treat the return value of `getCSSAnimation` as an **array of descriptors** โ€” never as a + CSS string. +- **MUST** pass `target` as a `string | null` (an id) โ€” not an `HTMLElement` โ€” this function has no + live-DOM requirement. +- **Rule:** the `animation` shorthand is paused by default for time-based/pointer animations; the + caller (or `@wix/interact`) is responsible for playing it once the trigger fires. `view-progress` + animations are emitted unpaused because `animation-timeline` governs their progress instead. +- **Rule:** `iterations: 0` โ‡’ `infinite` in the emitted CSS โ€” same idiom as `getWebAnimation` + (`./waapi.md`). +- **Rule:** for full FOUC-prevention and declarative CSS generation, use `@wix/interact`'s + `generate()` rather than reimplementing it against these descriptors. +- A registered effect only participates in `getCSSAnimation` output if it implements the optional + `style` member of `AnimationEffectAPI` โ€” see [`./custom-effects.md`](./custom-effects.md). + +## See Also + +- [`./motion-main.md`](./motion-main.md) โ€” entry point, function map, package boundary, easing + reference. +- [`./custom-effects.md`](./custom-effects.md) โ€” the `AnimationEffectAPI`/`style()` contract that + feeds `getCSSAnimation`, and `data-motion-part` sub-targeting. +- `./scrub-scenes.md` โ€” the native-`ViewTimeline`-vs-polyfill duration branch this file's `forCSS` + override bypasses. +- `../../interact/rules/integration.md` โ€” `@wix/interact`'s `generate()`, which builds full + FOUC-prevention page CSS on top of `getCSSAnimation`. diff --git a/packages/motion/rules/custom-effects.md b/packages/motion/rules/custom-effects.md new file mode 100644 index 00000000..e8b88a84 --- /dev/null +++ b/packages/motion/rules/custom-effects.md @@ -0,0 +1,281 @@ +--- +name: custom-effects +description: Read when authoring custom effects for @wix/motion โ€” customEffect callbacks, registered effect modules via registerEffects, and data-motion-part sub-targeting. +--- + +# Custom Effects & `registerEffects` + +Covers the two **programmatic** effect-definition modes in `@wix/motion`: a per-frame JS callback +(`customEffect`) and a reusable, named effect module registered via `registerEffects()` โ€” plus +sub-element targeting via `data-motion-part`. For the third, non-programmatic mode +(`keyframeEffect`) and the full structural-discrimination mental model, see +[`./motion-main.md`](./motion-main.md). For driving a registered/`customEffect` animation from +scroll or pointer input, see `./scrub-scenes.md`. For the ready-made preset catalog โ€” which is +itself built on the `registerEffects` contract documented here โ€” see `@wix/motion-presets`. + +## Table of Contents + +- [Package Boundary](#package-boundary) +- [Effect-Definition Modes](#effect-definition-modes) +- [Authoring a `customEffect` Callback](#authoring-a-customeffect-callback) +- [Authoring a Registered Effect Module](#authoring-a-registered-effect-module) +- [`registerEffects`](#registereffects) +- [Using a Registered Effect](#using-a-registered-effect) +- [`data-motion-part` Sub-Targeting](#data-motion-part-sub-targeting) +- [Gotchas / Rules](#gotchas--rules) +- [See Also](#see-also) + +## Package Boundary + +| Need | Use | +| -------------------------------------------------------------------------------------------------- | ------------------------- | +| Declarative triggerโ†’effect wiring, config-driven orchestration | `@wix/interact` | +| Ready-made effect catalog (entrance/scroll/ongoing/mouse presets) โ€” already built on this contract | `@wix/motion-presets` | +| Authoring a new custom callback or a new registered effect module | `@wix/motion` (this file) | + +This file documents the authoring contract only โ€” it does not list presets, preset parameters, or +angle/direction conventions (a `@wix/motion-presets` concern). + +## Effect-Definition Modes + +Exactly one of three fields on `AnimationOptions` drives what animates (`../src/api/common.ts:64-86`). +There is **no top-level `type` field** โ€” see [`./motion-main.md`](./motion-main.md#core-mental-model). + +| # | Field | Shape | Registration required? | +| --- | ---------------- | -------------------------------------------------------------- | ---------------------------------------- | +| 1 | `keyframeEffect` | `{ name: string; keyframes: Keyframe[] }` | No โ€” see `./motion-main.md` | +| 2 | `customEffect` | `(element: Element \| null, progress: number \| null) => void` | No โ€” this file | +| 3 | `namedEffect` | `{ type: string } & Record` | Yes, via `registerEffects()` โ€” this file | + +This file covers modes 2 and 3. + +## Authoring a `customEffect` Callback + +```typescript +// ../src/types.ts:101-105 +type CustomEffect = + | { ranges: { name: string; min: number; max: number; step?: number }[] } // INERT โ€” see gotcha below + | ((element: Element | null, progress: number | null) => void); // the only mode that runs code +``` + +**MUST:** author `customEffect` as a **function** `(element, progress) => void`. The `{ ranges }` +object form type-checks but does nothing on its own โ€” see [the gotcha below](#gotcha-the--ranges--object-form-is-inert). + +### Runtime behavior + +- A function `customEffect` is wrapped in `CustomAnimation` (`../src/CustomAnimation.ts`), built only + when `typeof effect.customEffect === 'function'` (`../src/api/webAnimations.ts:145-153`). +- `CustomAnimation` wraps an inner `Animation` (its `KeyframeEffect` is forced to `composite: 'add'` + so it isn't auto-removed) and runs a `requestAnimationFrame` loop that calls + `customEffect(target, progress)` whenever the animation's computed progress changes + (`../src/CustomAnimation.ts:45-63`). +- **On `cancel()`, the loop calls `customEffect(target, null)`** โ€” your callback receives + `progress === null` to signal cancellation (`../src/CustomAnimation.ts:155-163`). Handle `null` to + reset/clean up whatever the callback previously mutated. +- `CustomAnimation` mirrors the `Animation` interface (`play`, `pause`, `cancel`, `reverse`, + `finished`, `playState`, `currentTime`, โ€ฆ), so a `customEffect`-based `AnimationGroup` is driven the + same way as a keyframe-based one โ€” see `./waapi.md`. + +### Minimal correct example + +```typescript +import { getWebAnimation } from '@wix/motion'; + +const group = getWebAnimation(el, { + customEffect: (element, progress) => { + if (progress === null) { + // cancelled โ€” undo whatever this callback applied + (element as HTMLElement | null)?.style.removeProperty('opacity'); + return; + } + (element as HTMLElement | null)?.style.setProperty('opacity', String(progress)); + }, + duration: 1000, +}); + +group?.play(); +``` + +### Gotcha: the `{ ranges }` object form is inert + +Passing `customEffect: { ranges: [...] }` satisfies the `CustomEffect` type but produces **no +visible effect on its own**: + +- `getNamedEffect` (`../src/api/common.ts:82-84`) turns _any_ `customEffect` value โ€” function or + object โ€” into `[{ ...options, keyframes: [] }]`, i.e. an `AnimationData` with **no keyframes**. +- Only when `typeof effect.customEffect === 'function'` does `getWebAnimation` build a + `CustomAnimation` that actually calls your code (`../src/api/webAnimations.ts:145-153`). For the + object form that check fails, so the engine instead builds a plain `Animation` from an **empty** + `KeyframeEffect` โ€” nothing animates. + +**Rule:** always author `customEffect` as a function; do not rely on `{ ranges }` to do anything in +`@wix/motion` alone. + +## Authoring a Registered Effect Module + +The contract you implement for `registerEffects()` is `AnimationEffectAPI` (`../src/types.ts:57-66`): + +```typescript +type AnimationEffectAPI = { + web: ( + animationOptions: AnimationOptionsTypes[Enum], + dom?: DomApi, + options?: Record, + ) => AnimationData[]; + getNames: (animationOptions: AnimationOptionsTypes[Enum]) => string[]; + style?: (options: AnimationOptionsTypes[Enum]) => AnimationData[]; + prepare?: (options: AnimationOptionsTypes[Enum], dom?: DomApi) => void; +}; +``` + +| Member | Required | Called from | Returns / does | +| ---------- | -------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `web` | Yes | `getWebAnimation` (`../src/api/webAnimations.ts:52-54`) | Builds the `AnimationData[]` used to construct the runtime WAAPI `Animation`(s). Receives a `DomApi` (only when `target` resolves to an `HTMLElement`) for measuring before returning keyframes. | +| `getNames` | Yes | `getElementCSSAnimation` (`../src/motion.ts:41`) | Returns the `@keyframes`/animation names this effect can produce, so `getAnimation`/`getElementCSSAnimation` can find a matching CSS animation **already running** on the element (by `animationName`) instead of creating a new WAAPI one. | +| `style` | No | `getCSSAnimation` โ†’ `getCSSAnimationEffect` (`../src/api/cssAnimations.ts:39-46`) | Same return shape as `web`, but for the CSS-generation/SSR path โ€” see [`./css-generation.md`](./css-generation.md). If omitted, `getCSSAnimation` produces no descriptors for this effect. | +| `prepare` | No | `prepareAnimation` (`../src/api/prepare.ts:13-17`) | Runs measure/mutate work (via the `DomApi`) before the animation plays โ€” e.g. reading layout to compute a keyframe value. No return value. | + +`DomApi` (`../src/types.ts:122-123`): + +```typescript +type DomApi = { measure: MeasureCallback; mutate: MeasureCallback }; +type MeasureCallback = (fn: (target: HTMLElement | null) => void) => void; +``` + +Both `measure`/`mutate` batch through `fastdom` (`../src/api/common.ts:56-62`) โ€” use `dom.measure(...)` +for reads and `dom.mutate(...)` for writes to avoid layout thrashing. + +### `AnimationData` โ€” what `web`/`style` must return + +```typescript +// ../src/types.ts:184-199 +type AnimationData = (TimeAnimationOptions | AnimationDataForScrub) & { + name?: string; // @keyframes / animation name + keyframes: Record[]; // WAAPI-style keyframe list + custom?: Record; // CSS custom properties the effect needs + composite?: CompositeOperation; + part?: string; // sub-target โ€” see below + timing?: Partial; +}; +``` + +`web`/`style` return an **array** โ€” one entry per animation the effect needs (e.g. a multi-part +effect drives one `AnimationData` per `part`). + +### Other `EffectModule` shapes + +`registerEffects` accepts the union `EffectModule` (`../src/types.ts:261-266`): + +```typescript +type EffectModule = + | AnimationEffectAPI<'time'> + | AnimationEffectAPI<'scrub'> + | ScrollEffectModule // { web(options, dom?): AnimationData[] } โ€” ../src/types.ts:249-251 + | MouseEffectModule // { web(options): (element: HTMLElement) => object } โ€” ../src/types.ts:253-255 + | WebAnimationEffectFactory<'scrub'>; // bare function, same signature as AnimationEffectAPI.web โ€” ../src/types.ts:68-72 +``` + +`ScrollEffectModule`/`MouseEffectModule`/`WebAnimationEffectFactory` are lower-level factory shapes +used internally by scroll/pointer preset authors. `AnimationEffectAPI` above is the shape to target +for a standard authored effect. + +**`@wix/motion-presets` modules are exactly this shape.** For the full ready-made catalog +(entrance/scroll/ongoing/mouse), see `@wix/motion-presets` โ€” do not re-derive preset params here. + +## `registerEffects` + +```typescript +// ../src/api/registry.ts:5-7 +function registerEffects(effects: Record): void; +``` + +- Merges the given map into a single **global** registry (`Object.assign(registry, effects)`) โ€” + later calls add to, and can overwrite, earlier registrations by name. +- Call it **before** any `getWebAnimation`/`getCSSAnimation`/`getScrubScene` call whose options + reference the name via `namedEffect.type`. +- An unregistered name logs a `console.warn` and resolves to `null` + (`../src/api/registry.ts:9-18`), which propagates: `getWebAnimation` returns `null` for that + animation, and `getCSSAnimation` produces no descriptor for it. + +```typescript +import { registerEffects } from '@wix/motion'; +import type { AnimationEffectAPI, AnimationData } from '@wix/motion'; + +const FadeInLite: AnimationEffectAPI<'time'> = { + getNames: () => ['fade-in-lite'], + web: () => + [{ name: 'fade-in-lite', keyframes: [{ opacity: 0 }, { opacity: 1 }] }] as AnimationData[], +}; + +registerEffects({ FadeInLite }); +``` + +## Using a Registered Effect + +```typescript +import { getWebAnimation } from '@wix/motion'; + +getWebAnimation(el, { namedEffect: { type: 'FadeInLite' }, duration: 800, easing: 'ease-out' }); +``` + +`namedEffect.type` is the **only** valid `type` in `@wix/motion` โ€” it is the registered effect's +name, not a discriminator for the options object (no such field exists; see +[`./motion-main.md`](./motion-main.md#core-mental-model)). + +## `data-motion-part` Sub-Targeting + +An effect can target a sub-element instead of the animation root by setting `part` on the +`AnimationData` it returns. Resolution (`../src/api/common.ts:19-24`): + +```typescript +function getElementMotionPart(element: Element | null, part: string) { + if (element?.matches(`[data-motion-part~="${part}"]`)) { + return element; + } + return element?.querySelector(`[data-motion-part~="${part}"]`); +} +``` + +- Matches the element itself, or the first matching descendant, carrying + `data-motion-part~=""` (space-separated attribute match, so one element can carry multiple + part names). +- Used at runtime in `getWebAnimation` to pick the actual `KeyframeEffect` target + (`../src/api/webAnimations.ts:130`): `part ? getElementMotionPart(element, part) : element`. +- In the CSS path (`./css-generation.md`), the target selector becomes + `#[data-motion-part~=""]` (`../src/api/cssAnimations.ts:10-12`). + +```html +
+ Hi +
+``` + +```typescript +// one AnimationData returned by an effect's web()/style(): +{ name: 'pulse-glow', part: 'glow', keyframes: [{ offset: 0, opacity: 0.4 }, { offset: 1, opacity: 1 }] } +``` + +## Gotchas / Rules + +- **MUST** author `customEffect` as a **function** `(element, progress) => void` โ€” the `{ ranges }` + object form is inert in `@wix/motion` alone. +- **MUST** handle `progress === null` inside a `customEffect` callback โ€” it is called with `null` on + cancel, not skipped. +- **MUST** call `registerEffects()` before any options reference that name via `namedEffect.type` โ€” + otherwise `getWebAnimation`/`getCSSAnimation` silently drop the animation (`console.warn` plus an + effective `null`/no descriptor). +- **MUST** implement both `web` and `getNames` on a registered effect module โ€” `style` and `prepare` + are optional. +- **Rule:** `namedEffect.type` is the registered effect's **name** โ€” it is unrelated to the + (nonexistent) top-level `type` field on `AnimationOptions`. +- **Rule:** for the ready-made effect catalog, install `@wix/motion-presets` and register it with + `registerEffects(presets)` โ€” do not hand-author what already exists there. + +## See Also + +- [`./motion-main.md`](./motion-main.md) โ€” entry point, package boundary, structural discrimination, + function map, easing reference. +- `./scrub-scenes.md` โ€” driving a `customEffect`/registered scrub effect from your own scroll or + pointer loop via `getScrubScene`. +- [`./css-generation.md`](./css-generation.md) โ€” how a registered effect's `style()` output becomes + SSR-safe CSS via `getCSSAnimation`. diff --git a/packages/motion/rules/motion-main.md b/packages/motion/rules/motion-main.md new file mode 100644 index 00000000..845144b5 --- /dev/null +++ b/packages/motion/rules/motion-main.md @@ -0,0 +1,181 @@ +--- +name: motion-main +description: Read when working directly with @wix/motion โ€” creating WAAPI/CSS/scroll/pointer animations imperatively, driving scrub scenes, building sequences, or generating CSS. For preset selection see @wix/motion-presets; for declarative trigger wiring see @wix/interact. +--- + +# `@wix/motion` Reference + +`@wix/motion` is a low-level, web-first animation toolkit built on the native Web Animations API +(WAAPI) and CSS `@keyframes`, with scroll-driven (`ViewTimeline`) and pointer-tracking support. +It is the **imperative engine only** โ€” it has no trigger system, no declarative config schema, and +ships no preset catalog of its own. + +Package: `@wix/motion` ยท ESM entry (`module`): `dist/es/motion.js` ยท CJS (`main`): `dist/cjs/motion.js` +ยท types: `dist/types/index.d.ts` ยท `engines.node`: `>=18` ยท sole runtime dependency: `fastdom` ยท +`sideEffects: false`. Install with `npm install @wix/motion`. + +## Table of Contents + +- [Package Boundary](#package-boundary) +- [Core Mental Model](#core-mental-model) +- [Effect-Definition Modes](#effect-definition-modes) +- [Function Map](#function-map) +- [Easing Reference](#easing-reference) +- [Gotchas](#gotchas) +- [Spoke Files](#spoke-files) + +--- + +## Package Boundary + +| Need | Use | +| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | +| Declarative triggerโ†’effect wiring, config-driven orchestration, React/Web components | `@wix/interact` | +| Ready-made effect catalog (entrance/scroll/ongoing/mouse presets) | `@wix/motion-presets` (register via `registerEffects`) | +| Custom render callbacks, manual scrub-scene driving, programmatic sequences, SSR/CSS generation, inline keyframes | `@wix/motion` (this package) | + +`@wix/motion`'s only contract with presets is the **registry** (`registerEffects`) and the structural +`EffectModule` shape it accepts. These motion rules document that contract only โ€” they do **not** +list presets, preset parameters, category counts, or angle/direction conventions (those are a +`@wix/motion-presets` concern; see `@wix/motion-presets`'s `presets-main.md`). Trigger wiring, +`InteractConfig`, and CSS generation for a declarative page are a `@wix/interact` concern; see +`@wix/interact`'s `integration.md`. + +## Core Mental Model + +**`AnimationOptions` is discriminated STRUCTURALLY, not by a `type` field.** + +```typescript +// src/types.ts:113-117 โ€” no top-level `type` field anywhere on this union +type AnimationOptions = (TimeAnimationOptions | ScrubAnimationOptions) & AnimationExtraOptions; +``` + +The engine branches on which of `keyframeEffect` / `namedEffect` / `customEffect` is present on the +options object (`../src/api/common.ts:64-86`), and separately on the `trigger` argument passed to +`getWebAnimation`/`getScrubScene`: + +- `trigger` **omitted** (or its `trigger` field is neither of the two values below) โ‡’ **time-based** + animation โ€” options are `TimeAnimationOptions`. +- `trigger: { trigger: 'view-progress' }` โ‡’ scroll-driven โ€” options are `ScrubAnimationOptions`. +- `trigger: { trigger: 'pointer-move' }` โ‡’ pointer-driven โ€” options are `ScrubAnimationOptions`. + +> โš ๏ธ **There is no top-level `type` field on the options object.** Do not write +> `{ type: 'TimeAnimationOptions', namedEffect: {...} }` โ€” that field does not exist and is never read. +> This was the single most common historical error in hand-written/generated configs. +> +> โœ… `getWebAnimation(el, { namedEffect: { type: 'FadeIn' }, duration: 1000 })` +> โŒ `getWebAnimation(el, { type: 'TimeAnimationOptions', namedEffect: { type: 'FadeIn' }, duration: 1000 })` +> +> The **only** `type` field that legitimately exists is on `namedEffect` itself โ€” and there it means +> "the registered preset name", not a discriminant for the outer options object: +> `NamedEffect = { type: string } & Record` (`../src/types.ts:99`). + +## Effect-Definition Modes + +Exactly one of these three fields drives what actually animates (`../src/api/common.ts:64-86`): + +| Mode | Field | Shape | Behavior | +| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Inline keyframes | `keyframeEffect` | `{ name: string; keyframes: Keyframe[] }` (`MotionKeyframeEffect`, `../src/types.ts:138-141`) โ€” **no `type` field** | WAAPI/CSS keyframes with zero registration required. `name` becomes the `@keyframes`/animation name. | +| Custom JS callback | `customEffect` | `(element: Element \| null, progress: number \| null) => void` | Per-frame callback driven by `CustomAnimation`'s `requestAnimationFrame` loop. The **only** programmatic mode โ€” see the union caveat below. | +| Registered preset | `namedEffect` | `{ type: string } & Record` (`NamedEffect`) | References an effect registered via `registerEffects()`. `type` is the registered name; other keys are preset-specific params owned by the preset package. Unregistered โ‡’ `getWebAnimation` returns `null`. | + +`CustomEffect` is itself a union (`../src/types.ts:101-105`): + +```typescript +type CustomEffect = + | { ranges: { name: string; min: number; max: number; step?: number }[] } // INERT alone โ€” no visible effect + | ((element: Element | null, progress: number | null) => void); // WORKS โ€” the function form +``` + +Only the **function** form does anything at runtime in `@wix/motion`; the `{ ranges }` object form is +passed through as a keyframe-less animation and produces no visible effect on its own +(`../src/api/common.ts:82-84`). Always author `customEffect` as a function. + +## Function Map + +All functions below are exported from `@wix/motion`'s root (`../src/index.ts`, re-exporting +`../src/motion.ts:270-281`). Every return type that includes `| null` genuinely returns `null` at +runtime โ€” type your consts accordingly. + +| Function | Signature | Returns | Source | +| ----------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | +| `getWebAnimation` | `(target, animationOptions, trigger?, options?, ownerDocument?)` | `AnimationGroup \| MouseAnimationInstance \| null` | `../src/api/webAnimations.ts:60` | +| `getCSSAnimation` | `(target, animationOptions, trigger?)` | `Array<{ target, animation, composition?, custom?, name, keyframes, id, animationTimeline, animationRange }>` โ€” **an array of descriptors, never a string** | `../src/api/cssAnimations.ts:51` | +| `getScrubScene` | `(target, animationOptions, trigger, sceneOptions?)` | `ScrubScrollScene[] \| ScrubPointerScene \| ScrubPointerScene[] \| null` | `../src/motion.ts:74` | +| `getAnimation` | `(target, animationOptions, trigger?, reducedMotion?)` | `AnimationGroup \| MouseAnimationInstance \| null` | `../src/motion.ts:198` | +| `prepareAnimation` | `(target, animation, callback?)` | `void` | `../src/api/prepare.ts:5` | +| `getSequence` | `(options, animationGroups, context?)` | `Sequence` | `../src/motion.ts:261` | +| `createAnimationGroups` | `(animationGroupArgs, context?)` | `AnimationGroup[]` | `../src/motion.ts:232` | +| `registerEffects` | `(effects: Record)` | `void` | `../src/api/registry.ts:5` | +| `getEasing` | `(easing?: string)` | `string` โ€” CSS easing string, default `'linear'` | `../src/utils.ts:7` | +| `getJsEasing` | `(easing?: string)` | `((t: number) => number) \| undefined` | `../src/utils.ts:177` | + +Also exported (not detailed here): `getElementCSSAnimation`, `getElementAnimation` โ€” look for an +existing CSS animation already running on an element (used internally by `getAnimation`). + +For full argument tables, return-case breakdowns, and the `AnimationGroup`/`AnimationOptions` +surfaces behind `getWebAnimation`, see [`./waapi.md`](./waapi.md). + +## Easing Reference + +`easing` fields accept a named key or a raw CSS easing string. There are two separate key sets โ€” +do not mix them up: + +**JS easings** (`jsEasings`, `../src/easings.ts:187-213`) โ€” Penner functions, resolved by +`getJsEasing` (used internally for `offsetEasing` and `transitionEasing`): + +`linear`, `sineIn`, `sineOut`, `sineInOut`, `quadIn`, `quadOut`, `quadInOut`, `cubicIn`, `cubicOut`, +`cubicInOut`, `quartIn`, `quartOut`, `quartInOut`, `quintIn`, `quintOut`, `quintInOut`, `expoIn`, +`expoOut`, `expoInOut`, `circIn`, `circOut`, `circInOut`, `backIn`, `backOut`, `backInOut`. + +**CSS easings** (`cssEasings`, `../src/easings.ts:218-248`) โ€” named โ†’ `cubic-bezier(...)` (or a +plain CSS keyword), resolved by `getEasing` for the `easing` option: + +`linear`, `ease`, `easeIn`, `easeOut`, `easeInOut`, plus every JS key above except +`linear`/`ease*`, each mapped to a `cubic-bezier(...)` string. + +Both helpers also accept a raw `cubic-bezier(x1, y1, x2, y2)` string (hyphenated โ€” **not** +`cubicBezier(...)`), and `getJsEasing` additionally parses a CSS `linear(...)` string. `getEasing` +falls back to the raw string if it isn't a known key, else `'linear'`. `getJsEasing` returns +`undefined` only when `easing` is falsy, and otherwise falls back to `jsEasings.linear` if nothing +else parses. + +**Easing names that DO NOT EXIST โ€” never use:** `easeOutCubic`, `elasticOut`, `bounceOut`, `bounceIn`. + +`elastic` and `bounce` **do** exist, but only as `ScrubTransitionEasing` values +(`'linear' | 'hardBackOut' | 'easeOut' | 'elastic' | 'bounce'`) for pointer-smoothing +(`transitionEasing` on `ScrubAnimationOptions`) โ€” a completely different field from `easing`. Do not +pass `elastic`/`bounce` to `easing`. + +## Gotchas + +1. **No top-level `type` field on options** โ€” discrimination is structural (see Core Mental Model). +2. `getCSSAnimation` returns an **array of descriptor objects**, not a string. +3. `customEffect` must be a **function** to do anything; the `{ ranges }` object form is inert. +4. `await group.play()` resolves once playback has **started**, not when it finishes โ€” use + `onFinish(callback)` or the `finished` promise for completion (see `./waapi.md`). +5. `AnimationGroup.finished` is `Promise` (plural), not `Promise`. +6. Pointer `axis` (`'x' | 'y'`) lives on the **trigger**, not on the effect options. +7. `iterations: 0` โ‡’ `Infinity`; `iterations: undefined` โ‡’ `1` (`../src/api/common.ts:100`). +8. `getWebAnimation` / `getScrubScene` / `getAnimation` can all return `null` โ€” type consts accordingly. +9. `getWebAnimation` takes **one** `AnimationOptions` object, never an array. Use `getSequence` to + coordinate multiple elements/effects. +10. `startOffset` / `endOffset` are fields on the **animation options** (`ScrubAnimationOptions`), + not on the trigger. +11. The ESM import resolves to `dist/es/motion.js` (not `dist/esm/index.js`); requires Node `>=18`. +12. Use `Array.from(el.querySelectorAll(...))` before `.map` โ€” a `NodeList` has no `.map`. +13. `cubic-bezier(...)` is hyphenated, not `cubicBezier(...)`. + +## Spoke Files + +- [`./waapi.md`](./waapi.md) โ€” `getWebAnimation` full signature/return cases, `TimeAnimationOptions` / + `ScrubAnimationOptions` field tables, and the complete `AnimationGroup` surface (play/pause/reverse/ + progress/onFinish/onAbort/finished/playState). +- `./scrub-scenes.md` โ€” `getScrubScene`, `ScrubScrollScene`/`ScrubPointerScene`, and the + native-`ViewTimeline`-vs-polyfill scroll/pointer driving loop. +- `./sequences.md` โ€” `getSequence`, `createAnimationGroups`, `Sequence`'s stagger-offset formula, and + `AnimationGroupArgs`. +- `./custom-effects.md` โ€” authoring `customEffect` functions and `CustomAnimation`'s rAF loop/cancel + semantics. +- `./css-generation.md` โ€” `getCSSAnimation`'s descriptor shape and the SSR/FOUC-free CSS generation path. diff --git a/packages/motion/rules/scrub-scenes.md b/packages/motion/rules/scrub-scenes.md new file mode 100644 index 00000000..870d3bf3 --- /dev/null +++ b/packages/motion/rules/scrub-scenes.md @@ -0,0 +1,294 @@ +--- +name: scrub-scenes +description: Reference for getScrubScene โ€” the manual/polyfill path for driving scroll- or pointer-driven scrub animations from your own listener loop, when no native ViewTimeline is available or you need to drive progress yourself. Read when calling getScrubScene directly, writing a custom scroll/pointer driver, or debugging why a scrub animation isn't progressing. +--- + +# Scrub Scenes (`getScrubScene`) + +`getScrubScene` is `@wix/motion`'s differentiator: it hands you a small, stateless "scene" object whose +`effect(_, progress)` method you call from **your own** scroll/pointer listener. It does not attach any +listeners itself โ€” this is the manual/polyfill path, used when there is no native `ViewTimeline` to link +to, or for pointer-follow effects (which have no native browser equivalent). `@wix/interact` builds its +scroll and pointer triggers on top of exactly this function, using the bundled `fizban` scroll polyfill to +drive `ScrubScrollScene`s โ€” reach for `@wix/interact` before hand-rolling a driver in application code. + +## Table of Contents + +- [Package Boundary](#package-boundary) +- [`getScrubScene` Signature](#getscrubscene-signature) +- [Return Cases](#return-cases) +- [`ScrubScrollScene` Contract](#scrubscrollscene-contract) +- [`ScrubPointerScene` Contract](#scrubpointerscene-contract) +- [Driving a Scroll Scene](#driving-a-scroll-scene) +- [Driving a Pointer Scene](#driving-a-pointer-scene) +- [Pointer Specifics](#pointer-specifics) +- [Native vs. Polyfill Duration](#native-vs-polyfill-duration) +- [Gotchas / Rules](#gotchas--rules) +- [See Also](#see-also) + +## Package Boundary + +| Need | Use | +| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | +| Declarative scroll/pointer triggers, automatic driving via `fizban` | `@wix/interact` | +| Ready-made scroll/mouse preset catalog | `@wix/motion-presets` (register via `registerEffects`) | +| Manual scrub-scene driving, custom scroll/pointer loops, SSR fallback for browsers without `ViewTimeline` | `@wix/motion` (this file) | + +This file documents the imperative engine only. It does not document preset params or angle/direction +conventions โ€” those belong to `@wix/motion-presets`. + +## `getScrubScene` Signature + +```typescript +function getScrubScene( + target: HTMLElement | string | null, + animationOptions: AnimationOptions, + trigger: Partial & { element?: HTMLElement }, // may also carry `axis` + sceneOptions: Record = {}, // { disabled, allowActiveEvent, ...restโ†’getWebAnimation } +): ScrubScrollScene[] | ScrubPointerScene | ScrubPointerScene[] | null; +``` + +(`../src/motion.ts:74-196`) + +| Arg | Type | Notes | +| ------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `target` | `HTMLElement \| string \| null` | An element, an element `id` (resolved via `getElementById`), or `null`. | +| `animationOptions` | `AnimationOptions` | Same structurally-discriminated options as `getWebAnimation` โ€” `keyframeEffect` \| `namedEffect` \| `customEffect`, **no top-level `type` field**. For a real scrub effect this is typically the `ScrubAnimationOptions` shape (`startOffset`, `endOffset`, `transitionDuration`, `transitionEasing`, `centeredToTarget`, โ€ฆ) โ€” see `./waapi.md` for the full field table. | +| `trigger` | `Partial & { element?: HTMLElement }` | 3rd arg. Set `trigger: 'view-progress'` or `trigger: 'pointer-move'` to get scrub behavior. Also carries `id`, `componentId`, `element`, and โ€” pointer only โ€” `axis?: 'x' \| 'y'`. | +| `sceneOptions` | `Record` (default `{}`) | Destructured as `{ disabled, allowActiveEvent, ...rest }`. `rest` is forwarded as the 4th arg (`options`) to `getWebAnimation` โ€” e.g. pass `{ reducedMotion: true }` here to respect reduced motion. | + +> The declared return type includes `ScrubPointerScene[]`, but the current implementation never actually +> returns an array of pointer scenes โ€” only a single `ScrubPointerScene`, an array of `ScrubScrollScene`, +> or `null`. Don't rely on the array-of-pointer-scenes case existing at runtime. + +## Return Cases + +| Condition | Returns | Notes | +| --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Underlying animation couldn't be built (e.g. unregistered `namedEffect`) | `null` | Always guard for `null` before using the result. | +| `trigger: 'view-progress'` and `window.ViewTimeline` is **absent** | `ScrubScrollScene[]` โ€” one entry per partial animation in the group | The **only** branch that emits scroll scenes. This is the polyfill path. | +| `trigger: 'view-progress'` and `window.ViewTimeline` **exists** | a `ScrubPointerScene`-shaped wrapper around the native, timeline-linked `AnimationGroup` | The native `ViewTimeline` already drives this animation on scroll; there is nothing to scrub manually. Use `getWebAnimation` directly for the native path instead of calling `getScrubScene`. | +| `trigger: 'pointer-move'` and `animationOptions.keyframeEffect` is set | single `ScrubPointerScene` wrapping an `AnimationGroup` | `effect(_, p)` computes `linearProgress = axis === 'x' ? p.x : p.y` and calls `animationGroup.progress(linearProgress)`. | +| `trigger: 'pointer-move'` and `namedEffect`/`customEffect` is set (no `keyframeEffect`) | single `ScrubPointerScene` wrapping a `MouseAnimationInstance` (or `CustomMouseAnimationInstance` for `customEffect`) | `effect(_, p)` calls the instance's `progress(p)`. | + +(`../src/motion.ts:90-195`) + +## `ScrubScrollScene` Contract + +```typescript +interface ScrubScrollScene { + start: RangeOffset; + end: RangeOffset; + viewSource: HTMLElement; + ready: Promise; + getProgress(): number; + effect(__: any, p: number): void; // p is 0..1 scroll progress + disabled: boolean; + destroy(): void; + groupId?: string; +} +``` + +(`../src/types.ts:222-232`) + +- **`start` / `end`** โ€” getters returning the resolved `RangeOffset` (`{ name?, offset? }`) for this + partial animation, read lazily at access time rather than eagerly at scene creation + (`../src/motion.ts:99-104`). Use these to align your own scroll-progress calculation to the same + named range the animation was authored against. +- **`viewSource`** โ€” the element to observe: `trigger.element`, or the element resolved from + `trigger.componentId` via `getElementById`. +- **`ready`** โ€” resolves once the animation's target has been measured/mutated (same `AnimationGroup.ready` + promise underneath). +- **`getProgress()`** โ€” reads back `AnimationGroup.getProgress()` (0 if no computed timing yet). +- **`effect(_, p)`** โ€” call with `p` as a `0..1` scroll-progress number. Internally sets + `partialAnimation.currentTime = (delay + activeDuration) * p`. +- **`disabled`** โ€” pass-through of `sceneOptions.disabled` (not read internally by the scene itself โ€” it's + informational for your driver loop to skip disabled scenes). +- **`destroy()`** โ€” cancels the underlying partial `Animation`. **Always call for every scene on + teardown/unmount.** +- **`groupId?`** โ€” declared on the type but not populated by `getScrubScene`. + +`RangeOffset` (referenced by `start`/`end` and by `ScrubAnimationOptions.startOffset`/`endOffset`): + +```typescript +type RangeOffset = { + name?: 'entry' | 'exit' | 'contain' | 'cover' | 'entry-crossing' | 'exit-crossing'; + offset?: LengthPercentage; // { value: number; unit: 'px'|'em'|'rem'|'vh'|'vw'|'vmin'|'vmax' } | { value: number; unit: 'percentage' } +}; +``` + +(`../src/types.ts:133-136`, `../src/types.ts:3-13`) + +## `ScrubPointerScene` Contract + +```typescript +interface ScrubPointerScene { + target?: HTMLElement; + centeredToTarget?: boolean; + transitionDuration?: number; + transitionEasing?: ScrubTransitionEasing; + getProgress(): Progress | number; + effect(__: any, p: Progress): void; + disabled: boolean; + destroy(): void; + allowActiveEvent?: boolean; + ready?: Promise; +} + +type Progress = { x: number; y: number; v?: { x: number; y: number }; active?: boolean }; +``` + +(`../src/types.ts:234-245`, `../src/types.ts:74-79`) + +- **`effect(_, p)`** โ€” call with `p` as a `Progress` payload: `{ x, y }` at minimum, plus optional + `v` (velocity `{x, y}`) and `active` (pointer-down/active state) for smoothing. +- **`destroy()`** โ€” cancels the underlying `AnimationGroup`/`MouseAnimationInstance`. **Always call on + teardown.** +- Which optional fields are actually populated depends on which branch produced the scene: + +| Field | `keyframeEffect` pointer path | `namedEffect`/`customEffect` pointer path | +| -------------------- | ----------------------------------------------- | ------------------------------------------------------------------------ | +| `target` | `undefined` | the wrapped `MouseAnimationInstance.target` | +| `centeredToTarget` | from `animationOptions.centeredToTarget` | from `animationOptions.centeredToTarget` | +| `transitionDuration` | not set | set only if `customEffect` **and** `transitionDuration` are both present | +| `transitionEasing` | not set | set only in that same case โ€” see gotcha below | +| `allowActiveEvent` | not set | from `sceneOptions.allowActiveEvent` | +| `ready` | `animationGroup.ready` | not set (`undefined`) | +| `getProgress()` | returns an internally-tracked last-driven value | delegates to the wrapped instance's own `getProgress()` | + +- **Gotcha โ€” `transitionEasing` is resolved, not raw**: when set, `scene.transitionEasing` is assigned + `getJsEasing(transitionEasing)` โ€” a **resolved JS easing function** `(t: number) => number` โ€” despite the + type declaring `transitionEasing?: ScrubTransitionEasing` (a string union). Don't treat it as a string at + runtime (`../src/motion.ts:161-165`). +- **Gotcha โ€” `getProgress()` may not exist**: the bottom fallback calls + `(animation as AnimationGroup | CustomMouseAnimationInstance).getProgress()`. A plain + `MouseAnimationInstance` (the base interface, `../src/types.ts:81-86`) does **not** declare + `getProgress`; only `AnimationGroup` and `CustomMouseAnimationInstance` do. If a `namedEffect` mouse + preset's factory returns a plain `MouseAnimationInstance`, calling `scene.getProgress()` can throw at + runtime โ€” verify with the specific preset (`@wix/motion-presets`) before relying on it. + +## Driving a Scroll Scene + +`getScrubScene` performs no observation โ€” you own the scroll loop. This is a minimal illustration; a +spec-accurate implementation must resolve each scene's `start`/`end` named ranges the way the CSS +scroll-timeline spec (and `fizban`, which `@wix/interact` uses) does โ€” this example uses a naive linear +approximation instead: + +```javascript +const scenes = getScrubScene( + target, + animationOptions, // ScrubAnimationOptions with startOffset/endOffset + { trigger: 'view-progress', id, componentId }, +); + +if (!scenes) { + // animation could not be created, or the native ViewTimeline path was used instead +} else { + const computeProgress = (viewSource) => { + const rect = viewSource.getBoundingClientRect(); + const vh = window.innerHeight; + // naive linear approximation โ€” a real driver resolves start/end named ranges (cover/entry/exit/โ€ฆ) + return Math.min(1, Math.max(0, (vh - rect.top) / (vh + rect.height))); + }; + + const onScroll = () => { + scenes.forEach((scene) => { + if (scene.disabled) return; + scene.effect(null, computeProgress(scene.viewSource)); + }); + }; + + window.addEventListener('scroll', onScroll, { passive: true }); + onScroll(); + + // teardown + function destroy() { + window.removeEventListener('scroll', onScroll); + scenes.forEach((scene) => scene.destroy()); + } +} +``` + +## Driving a Pointer Scene + +```javascript +const scene = getScrubScene( + target, + animationOptions, // e.g. { keyframeEffect: {...} } + { trigger: 'pointer-move', axis: 'y' }, +); + +if (!scene) { + // animation could not be created +} else { + const onPointerMove = (e) => { + const rect = target.getBoundingClientRect(); + const x = (e.clientX - rect.left) / rect.width; + const y = (e.clientY - rect.top) / rect.height; + scene.effect(null, { x, y, active: true }); + }; + + window.addEventListener('pointermove', onPointerMove); + + // teardown + function destroy() { + window.removeEventListener('pointermove', onPointerMove); + scene.destroy(); + } +} +``` + +## Pointer Specifics + +- **`axis: 'x' | 'y'`** lives on the **trigger** (3rd arg), not on `animationOptions`. In the + `keyframeEffect` pointer path, `axis === 'x'` reads `p.x`, otherwise `p.y` (`../src/motion.ts:143`). +- **`centeredToTarget`** โ€” on `ScrubAnimationOptions`; surfaced onto the returned scene for the driver to + read. +- **Transition smoothing** โ€” `transitionDuration` (ms) and `transitionEasing` on `ScrubAnimationOptions`: + + ```typescript + type ScrubTransitionEasing = 'linear' | 'hardBackOut' | 'easeOut' | 'elastic' | 'bounce'; + ``` + + (`../src/types.ts:131`) โ€” these are **not** the same set as `jsEasings`/`cssEasings` (no `elasticOut`, + `bounceOut`, etc. โ€” see `./motion-main.md`'s easing reference for the general easing keys). + +- **`allowActiveEvent`** โ€” passed via `sceneOptions` (4th arg to `getScrubScene`), not via + `animationOptions`. +- **`startOffset` / `endOffset`** โ€” properties of the **`animationOptions`** (`ScrubAnimationOptions`), + NOT of the trigger. +- **`ScrubAnimationOptions.duration` is a `LengthPercentage`, not milliseconds** โ€” the core engine's own + `KeyframeEffect` timing for scrub triggers is always either `'auto'` or the fixed `99.99ms`/`0.01ms` pair + (see below); this field exists for a preset's own `web()` logic to interpret, not for engine timing. + +## Native vs. Polyfill Duration + +With `trigger: 'view-progress'` (`../src/api/common.ts:106-120`): + +- If `window.ViewTimeline` exists (or `forCSS` is set, i.e. the `getCSSAnimation`/SSR path) โ†’ + `duration: 'auto'`, timeline-linked, and it auto-plays. +- Otherwise โ†’ `duration: 99.99`, `delay: 0.01` (ms) so the animation's progress is externally scrubbable + via `currentTime`, and `start`/`end` range info is written onto each animation for `getScrubScene` to + read. + +## Gotchas / Rules + +- **MUST** check for `null` before using the result of `getScrubScene`. +- **MUST** drive `effect(...)` yourself, from your own scroll/pointer listener โ€” `getScrubScene` attaches + no listeners. +- **MUST** call `destroy()` on every scene (loop over the array for `ScrubScrollScene[]`) on teardown. +- **Rule**: pointer `axis` goes on the trigger (3rd arg), not inside `animationOptions`. +- **Rule**: `startOffset`/`endOffset` belong on `animationOptions`, not on the trigger. +- **Rule**: if `window.ViewTimeline` exists, don't call `getScrubScene` for `view-progress` โ€” call + `getWebAnimation` directly and let the native timeline drive it. +- **Rule**: `@wix/interact` (with its bundled `fizban` scroll polyfill) already implements a + spec-accurate scroll driver and pointer driver on top of `getScrubScene` โ€” prefer it over a hand-rolled + driver in application code. +- There is **no top-level `type` field** on `animationOptions` โ€” discrimination is structural + (`keyframeEffect` / `namedEffect` / `customEffect`). See `./motion-main.md`. + +## See Also + +- [`./motion-main.md`](./motion-main.md) โ€” entry point, function map, package boundary, easing reference. +- [`./waapi.md`](./waapi.md) โ€” full `AnimationOptions` field tables and the `AnimationGroup` surface that + scroll/keyframe pointer scenes wrap. diff --git a/packages/motion/rules/sequences.md b/packages/motion/rules/sequences.md new file mode 100644 index 00000000..1f92c1f3 --- /dev/null +++ b/packages/motion/rules/sequences.md @@ -0,0 +1,269 @@ +--- +name: sequences +description: Reference for getSequence, createAnimationGroups, and the Sequence class โ€” building staggered/coordinated multi-group time-based animations in @wix/motion. Read when animating a list of elements (or several different elements) with timing offsets between them, or when working with Sequence.addGroups/removeGroups. +--- + +# Sequences (`getSequence` / `Sequence`) + +`Sequence` coordinates multiple `AnimationGroup` instances as a single timeline with staggered `delay` +offsets. It extends `AnimationGroup`, so it inherits the full playback API (`play`, `pause`, `reverse`, +`cancel`, `progress`, `setPlaybackRate`, `finished`, `playState`, โ€ฆ) while distributing an +easing-shaped delay across its child groups. `getSequence` is the factory most callers use; `@wix/interact` +uses it internally for staggered list animations. + +## Table of Contents + +- [Package Boundary](#package-boundary) +- [`getSequence` / `createAnimationGroups` Signatures](#getsequence--createanimationgroups-signatures) +- [`SequenceOptions` / `AnimationGroupArgs`](#sequenceoptions--animationgroupargs) +- [Target Resolution](#target-resolution) +- [Stagger Offset Formula](#stagger-offset-formula) +- [`Sequence` Class Surface](#sequence-class-surface) +- [Reduced Motion](#reduced-motion) +- [Gotchas / Rules](#gotchas--rules) +- [See Also](#see-also) + +## Package Boundary + +| Need | Use | +| ---------------------------------------------------------------------- | ------------------------- | +| Declarative `sequences` config, list/stagger wiring via triggers | `@wix/interact` | +| Ready-made preset catalog for the individual effects inside a sequence | `@wix/motion-presets` | +| Programmatic multi-group stagger orchestration | `@wix/motion` (this file) | + +This file documents the imperative engine only โ€” it does not document preset params. + +## `getSequence` / `createAnimationGroups` Signatures + +```typescript +function getSequence( + options: SequenceOptions, + animationGroups: AnimationGroupArgs[], + context?: Record, // supports { reducedMotion: boolean } +): Sequence; +``` + +(`../src/motion.ts:261-268`) โ€” resolves every `AnimationGroupArgs` entry into one or more `AnimationGroup`s +via `createAnimationGroups`, then wraps them in `new Sequence(groups, options)`. + +```typescript +function createAnimationGroups( + animationGroupArgs: AnimationGroupArgs[], + context?: Record, +): AnimationGroup[]; +``` + +(`../src/motion.ts:232-256`) โ€” builds groups without wrapping them in a `Sequence`. Used internally by +`getSequence`; call it directly if you want the raw groups (e.g. to build your own `Sequence` yourself, or +to feed some other coordination). Entries whose resolved animation is not an `AnimationGroup` (e.g. a +`MouseAnimationInstance`) are silently skipped. + +## `SequenceOptions` / `AnimationGroupArgs` + +```typescript +type SequenceOptions = { + delay?: number; // ms base delay, default 0 + offset?: number; // ms stagger interval, default 0 + offsetEasing?: string | ((p: number) => number); // default 'linear' +}; +``` + +(`../src/types.ts:268-272`) + +```typescript +type AnimationGroupArgs = { + target: HTMLElement | HTMLElement[] | string | null; + options: AnimationOptions; + context?: Record; +}; +``` + +(`../src/types.ts:274-278`) + +> **Gotcha**: the per-entry `AnimationGroupArgs.context` field is declared but **not read** by +> `createAnimationGroups` โ€” only the top-level `context` argument (the function's own 3rd/2nd parameter) +> is used, and only its `reducedMotion` field (`../src/motion.ts:238-247`). Don't rely on per-entry +> context. + +## Target Resolution + +`AnimationGroupArgs.target` is resolved per entry via `resolveTargets` (`../src/motion.ts:217-227`): + +| `target` type | Resolves to | Groups created | +| --------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------- | +| `HTMLElement` | `[target]` | one `AnimationGroup` | +| `HTMLElement[]` | `target` as-is | one `AnimationGroup` per element | +| `string` | `Array.from(document.querySelectorAll(target))` | one `AnimationGroup` per match (zero if none match โ€” not an error) | +| `null` | `[null]` | one `AnimationGroup` for an element-less/`null` target, passed through to `getAnimation` | + +## Stagger Offset Formula + +``` +offset[i] = (offsetEasing(i / last) * last * offset) | 0 +``` + +where `i` is the (0-based) group index and `last` is the index of the final group (`count - 1`). Single- +group sequences (`count <= 1`) always produce `[0]`, regardless of `offset`/`offsetEasing` +(`../src/Sequence.ts:52-62`). + +Each group's calculated offset is added to its animations' `delay` timing. An `endDelay` is also computed +per group so that **all groups share the same total active duration** โ€” this is what lets `finished` / +`onFinish` resolve at the correct overall time regardless of per-group stagger +(`../src/Sequence.ts:64-102`). + +### Offsets by Easing + +Given 5 groups with `offset: 200`: + +| Easing | Offsets | Distribution | +| --------- | ------------------------- | ------------------------ | +| `linear` | `[0, 200, 400, 600, 800]` | Even spacing | +| `quadIn` | `[0, 50, 200, 450, 800]` | Slow start, accelerating | +| `sineOut` | `[0, 306, 565, 739, 800]` | Fast start, decelerating | + +### Example + +```typescript +import { getSequence } from '@wix/motion'; + +const items = document.querySelectorAll('.card'); + +const sequence = getSequence( + { offset: 150, offsetEasing: 'quadIn' }, + Array.from(items).map((el) => ({ + target: el, + options: { + duration: 600, + easing: 'ease-out', + keyframeEffect: { + name: 'fade-up', + keyframes: [ + { opacity: 0, transform: 'translateY(20px)' }, + { opacity: 1, transform: 'translateY(0)' }, + ], + }, + }, + })), +); + +sequence.play(); +await sequence.onFinish(() => console.log('all staggered animations complete')); +``` + +## `Sequence` Class Surface + +```typescript +class Sequence extends AnimationGroup { + animationGroups: AnimationGroup[]; + delay: number; + offset: number; + offsetEasing: (p: number) => number; + + constructor(animationGroups: AnimationGroup[], options?: SequenceOptions); + + addGroups(entries: IndexedGroup[]): void; + removeGroups(predicate: (group: AnimationGroup) => boolean): AnimationGroup[]; + async onFinish(callback: () => void): Promise; // overridden + + // inherited from AnimationGroup โ€” see ./waapi.md + async play(callback?: () => void): Promise; + pause(): void; + async reverse(callback?: () => void): Promise; + cancel(): void; + progress(p: number): void; + setPlaybackRate(rate: number): void; + getProgress(): number; + async onAbort(callback: () => void): Promise; + get finished(): Promise; + get playState(): AnimationPlayState; + hasAnimationName(name: string): boolean; + hasAnimationId(id: string): boolean; + getTimingOptions(): { delay: number; duration: number; iterations: number }[]; +} +``` + +(`../src/Sequence.ts:13-45`) + +```typescript +type IndexedGroup = { index: number; group: AnimationGroup }; +``` + +(`../src/types.ts:280-283`) + +- **`addGroups(entries)`** (`../src/Sequence.ts:109-128`) โ€” inserts groups at the given indices (processed + highest-index-first so earlier insertion indices stay valid), splices the new groups' animations into the + flattened `animations` array at the matching position, recalculates offsets for **all** groups, and + resets `ready` to `Promise.all(animationGroups.map(g => g.ready))`. +- **`removeGroups(predicate)`** (`../src/Sequence.ts:135-163`) โ€” cancels and removes every group for which + `predicate(group)` returns `true`, rebuilds the flattened `animations` array, recalculates offsets for + the remaining groups, resets `ready`, and returns the removed groups (`[]` if none matched). +- **`onFinish(callback)`** (overridden, `../src/Sequence.ts:165-172`) โ€” awaits each child group's own + `finished` promise individually (`Promise.all(animationGroups.map(g => g.finished))`), not the flattened + `AnimationGroup.finished`. On any rejection it logs a warning via `console.warn` and does **not** invoke + `callback`. +- **`delay` / `offset` / `offsetEasing` / `animationGroups`** are public fields set at construction. They + can be read back, but mutating them after construction does **not** retrigger offset recalculation โ€” + `applyOffsets()` is private and only runs from the constructor, `addGroups`, and `removeGroups`. To + change stagger timing, construct a new `Sequence`. +- **`offsetEasing` resolution** (`../src/Sequence.ts:27-30`): if `options.offsetEasing` is a function, it's + used as-is; if it's a string, it's resolved via `getJsEasing(string)`; otherwise (or if resolution fails) + it falls back to the local `linear` easing. Valid string keys are the `jsEasings` set โ€” + `linear, sineIn, sineOut, sineInOut, quadIn, quadOut, quadInOut, cubicIn, cubicOut, cubicInOut, quartIn, +quartOut, quartInOut, quintIn, quintOut, quintInOut, expoIn, expoOut, expoInOut, circIn, circOut, +circInOut, backIn, backOut, backInOut` + (`../src/easings.ts:187-213`) โ€” or a raw `cubic-bezier(x1, y1, x2, y2)` string, or a custom + `(p: number) => number` function (`../src/utils.ts:177-187`). + +### `addGroups` / `removeGroups` Examples + +```typescript +import { AnimationGroup, Sequence } from '@wix/motion'; + +const sequence = new Sequence(existingGroups, { offset: 150 }); + +// insert a new group at position 2; offsets recompute for every group +sequence.addGroups([{ index: 2, group: new AnimationGroup(newAnimations) }]); + +// remove groups targeting a specific element +const removed = sequence.removeGroups((group) => + group.animations.some((a) => (a.effect as KeyframeEffect)?.target === removedElement), +); +``` + +## Reduced Motion + +`context.reducedMotion` (the 3rd arg to `getSequence`, 2nd arg to `createAnimationGroups`) is forwarded to +every `getAnimation(...)` call as its `reducedMotion` parameter, which forwards it to +`getWebAnimation`'s `options.reducedMotion` (see `./waapi.md`): single-iteration time-based animations +collapse to `duration: 1`; multi-iteration ones are dropped entirely (`getAnimation` returns `null` and +that entry is excluded from `createAnimationGroups`'s output). Stagger offsets are then computed over +whatever count of groups actually survived. + +## Gotchas / Rules + +- **MUST NOT** rely on scroll/pointer triggers inside a sequence: `createAnimationGroups` always calls + `getAnimation(element, options, undefined, context?.reducedMotion)` โ€” the `trigger` argument is + hard-coded to `undefined`. Every group a sequence produces is a plain **time-based** animation, + regardless of what scrub-specific fields (`startOffset`, `transitionDuration`, โ€ฆ) are present in the + `AnimationOptions` you pass. For scroll/pointer-driven coordinated groups, drive individual + `getScrubScene` results yourself instead (`./scrub-scenes.md`). +- **MUST** call `sequence.play()` (or `.reverse()`) to start playback โ€” the constructor only computes + offsets and applies timing; it does not auto-play. +- **MUST** use `sequence.onFinish(cb)` or `await sequence.finished` for completion โ€” like the inherited + `AnimationGroup.play()`, `play()` resolves once playback **starts**, not when it finishes. +- **Rule**: to change stagger timing after construction, build a new `Sequence` โ€” mutating + `delay`/`offset`/`offsetEasing` in place has no effect until `addGroups`/`removeGroups` runs. + `applyOffsets()` is private. +- **Rule**: a `string` target with zero DOM matches, or an unresolved `namedEffect`, silently yields zero + groups for that entry โ€” not an error. Check `sequence.animationGroups.length` if you need to detect + this. +- There is **no top-level `type` field** on `AnimationOptions` โ€” discrimination is structural + (`keyframeEffect` / `namedEffect` / `customEffect`). See `./motion-main.md`. + +## See Also + +- [`./motion-main.md`](./motion-main.md) โ€” entry point, function map, package boundary, easing reference. +- [`./scrub-scenes.md`](./scrub-scenes.md) โ€” for scroll/pointer-driven coordinated groups (not covered by + `Sequence`). +- [`./waapi.md`](./waapi.md) โ€” full `AnimationGroup` surface (`play`, `onFinish`, `finished`, `playState`, + โ€ฆ) that `Sequence` extends, and the full `AnimationOptions` field tables. diff --git a/packages/motion/rules/waapi.md b/packages/motion/rules/waapi.md new file mode 100644 index 00000000..dc31bfc3 --- /dev/null +++ b/packages/motion/rules/waapi.md @@ -0,0 +1,199 @@ +--- +name: waapi +description: Read when creating time-based or WAAPI animations with getWebAnimation, or controlling playback via the returned AnimationGroup (play/pause/reverse/progress/onFinish/onAbort). +--- + +# `getWebAnimation` & `AnimationGroup` + +Entry point for creating a single WAAPI animation (or getting back a `MouseAnimationInstance` on the +pointer path) and the object you get back to control it. For the router and the shared mental model +(structural discrimination, effect-definition modes, easings), see [`./motion-main.md`](./motion-main.md). +For the scroll/pointer scrubbing layer built on top of this function, see `./scrub-scenes.md`. + +## Table of Contents + +- [`getWebAnimation` Signature](#getwebanimation-signature) +- [Return Cases](#return-cases) +- [`view-progress`: Native ViewTimeline vs Polyfill](#view-progress-native-viewtimeline-vs-polyfill) +- [`TimeAnimationOptions`](#timeanimationoptions) +- [`ScrubAnimationOptions`](#scrubanimationoptions) +- [`AnimationGroup`](#animationgroup) +- [Gotchas](#gotchas) + +--- + +## `getWebAnimation` Signature + +```typescript +// ../src/api/webAnimations.ts:60-66 +function getWebAnimation( + target: HTMLElement | string | null, + animationOptions: AnimationOptions, + trigger?: Partial & { element?: HTMLElement }, + options?: Record, // engine reads { reducedMotion } + ownerDocument?: Document, +): AnimationGroup | MouseAnimationInstance | null; +``` + +| Arg | Type | Notes | +| ------------------ | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `target` | `HTMLElement \| string \| null` | A string is treated as an element `id` and resolved via `getElementById` (`ownerDocument` or `document`). | +| `animationOptions` | `AnimationOptions` | **Single** object โ€” see [Core Mental Model](./motion-main.md#core-mental-model). Never an array; use `getSequence` to coordinate multiple elements/effects. | +| `trigger?` | `Partial & { element?: HTMLElement }` | `TriggerVariant = { id: string; trigger: 'view-progress' \| 'pointer-move'; componentId: string }` (`../src/types.ts:207-211`). Also read for pointer: `axis?: 'x' \| 'y'`. Omitted/neither value โ‡’ time-based. | +| `options?` | `Record` | Engine only reads `{ reducedMotion }` here; passed through to the registered effect's `web()` as its 3rd arg. | +| `ownerDocument?` | `Document` | Used to resolve a string `target` in a different document (e.g. an iframe). | + +## Return Cases + +- **`AnimationGroup`** โ€” the default result for `keyframeEffect`/`namedEffect` (time or scrub trigger) + and for `customEffect` on a non-pointer trigger. +- **`MouseAnimationInstance`** โ€” returned **only** on the `pointer-move` trigger when + `animationOptions.keyframeEffect` is **not** set (i.e. `namedEffect` or `customEffect` on + `pointer-move`). Shape: `{ target, play(), progress(p: Progress), cancel() }` + (`../src/types.ts:81-86`). +- **`null`** โ€” returned when no effect data could be generated: + - `namedEffect.type` isn't registered (`getRegisteredEffect` warns and returns `null`). + - Reduced motion dropped a multi-iteration time-based animation (see [Gotchas](#gotchas)). + - The pointer factory couldn't be built (`typeof mouseAnimationFactory !== 'function'`). + - The effect's `web()` returned an empty array. + +## `view-progress`: Native ViewTimeline vs Polyfill + +`getWebAnimation` branches on runtime `ViewTimeline` support for `trigger: { trigger: 'view-progress' }` +(`../src/api/webAnimations.ts:116-190`): + +- **`window.ViewTimeline` present** โ€” a native `ViewTimeline` is constructed (`subject` = `trigger.element` + or the element resolved from `trigger.componentId`), the animation's `duration` is set to `'auto'`, + it is linked to that timeline, and it **auto-plays** immediately (`animation.play()` is called inside + a `fastdom.mutate`). +- **`window.ViewTimeline` absent** โ€” no timeline is attached. Instead `duration: 99.99ms` / + `delay: 0.01ms` are used so the animation's progress can be driven externally by setting + `currentTime`, and `start`/`end` range info (`{ name, offset, add }`, from `startOffset`/`endOffset`) + is written directly onto each `Animation` object. This is the data `getScrubScene` reads to build + `ScrubScrollScene[]` โ€” see `./scrub-scenes.md`. + +## `TimeAnimationOptions` + +Used when `trigger` is omitted (time-based). `duration`/`delay`/`endDelay` are **milliseconds**. + +```typescript +// ../src/types.ts:143-156 +type TimeAnimationOptions = { + id?: string; + keyframeEffect?: MotionKeyframeEffect; // see Effect-Definition Modes in motion-main.md + namedEffect?: NamedEffect; + customEffect?: CustomEffect; + duration?: number; // ms + delay?: number; // ms + endDelay?: number; // ms + easing?: string; // named key or CSS easing string โ€” see motion-main.md Easing Reference + iterations?: number; // 0 โ‡’ Infinity; undefined โ‡’ 1 + alternate?: boolean; + fill?: AnimationFillMode; // 'none' | 'backwards' | 'forwards' | 'both' + reversed?: boolean; +}; +``` + +## `ScrubAnimationOptions` + +Used when `trigger.trigger` is `'view-progress'` or `'pointer-move'`. + +```typescript +// ../src/types.ts:160-182 +type ScrubAnimationOptions = { + id?: string; + keyframeEffect?: MotionKeyframeEffect; + namedEffect?: NamedEffect; + customEffect?: CustomEffect; + startOffset?: RangeOffset; // { name?: 'entry'|'exit'|'contain'|'cover'|'entry-crossing'|'exit-crossing'; offset?: LengthPercentage } + endOffset?: RangeOffset; + playbackRate?: number; + easing?: string; + iterations?: number; + fill?: AnimationFillMode; + alternate?: boolean; + reversed?: boolean; + transitionDuration?: number; // pointer smoothing (ms) + transitionDelay?: number; + transitionEasing?: ScrubTransitionEasing; // 'linear' | 'hardBackOut' | 'easeOut' | 'elastic' | 'bounce' + centeredToTarget?: boolean; + duration?: LengthPercentage; // NOTE: length/percentage, NOT ms โ€” unlike TimeAnimationOptions.duration +}; +``` + +> โš ๏ธ `duration` means two different things depending on which options type is in play: +> `TimeAnimationOptions.duration` is a `number` of milliseconds; `ScrubAnimationOptions.duration` is a +> `LengthPercentage` (`{ value: number; unit: 'px'|'em'|'rem'|'vh'|'vw'|'vmin'|'vmax' } | { value: number; unit: 'percentage' }`). +> `startOffset`/`endOffset` (not the trigger) are where scroll-range boundaries live. + +## `AnimationGroup` + +A wrapper that simulates a WAAPI `GroupEffect` over one or more native `Animation` objects +(`../src/AnimationGroup.ts`). `getWebAnimation` returns one whenever it doesn't return a +`MouseAnimationInstance` or `null`. + +**Constructor:** `new AnimationGroup(animations: Animation[], options?: AnimationGroupOptions)`. + +**Properties:** + +| Property | Type | Notes | +| ------------------ | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | +| `animations` | `(Animation & { start?: RangeOffset; end?: RangeOffset })[]` | The wrapped native animations; `start`/`end` are written for the no-`ViewTimeline` scrub path. | +| `options?` | `AnimationGroupOptions` | The `AnimationOptions` this group was built from, plus `trigger`/offset-add fields. | +| `ready` | `Promise` | Resolves once targets are measured/mutated (`fastdom`). `play()`/`reverse()` await this first. | +| `isCSS` | `boolean` | `true` if `animations[0] instanceof CSSAnimation`. | +| `longestAnimation` | `Animation` | The animation with the greatest computed `effect.getComputedTiming().endTime`. | + +**Methods:** + +| Method | Signature | Behavior | +| ------------------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `play` | `async play(callback?: () => void): Promise` | Awaits `ready`, calls `.play()` on every animation, awaits each `animation.ready`, then calls `callback`. **Resolves once playback has STARTED, not when it finishes** (`../src/AnimationGroup.ts:39-53`). | +| `pause` | `pause(): void` | Calls `.pause()` on every animation. | +| `reverse` | `async reverse(callback?: () => void): Promise` | Same shape as `play`, but calls `.reverse()`. | +| `progress` | `progress(p: number): void` | Sets `currentTime` on every animation to `(delay + duration*iterations) * p`, scrubbing all of them to progress `p`. | +| `cancel` | `cancel(): void` | Calls `.cancel()` on every animation. | +| `setPlaybackRate` | `setPlaybackRate(rate: number): void` | Sets `playbackRate` on every animation. | +| `getProgress` | `getProgress(): number` | `longestAnimation.effect.getComputedTiming().progress`, or `0` if unavailable. | +| `onFinish` | `async onFinish(callback: () => void): Promise` | Awaits `Promise.all(animations.map(a => a.finished))`. On success, if the group isn't CSS, dispatches `new CustomEvent('animationend', { detail: { effectId } })` on the first animation's target, then calls `callback`. On interruption/rejection, logs a warning and does **not** call `callback` (`../src/AnimationGroup.ts:99-119`). | +| `onAbort` | `async onAbort(callback: () => void): Promise` | Awaits the same `finished` promise; if it rejects with `AbortError`, dispatches `new Event('animationcancel')` on the first non-CSS target and calls `callback` (`../src/AnimationGroup.ts:121-140`). | +| `hasAnimationName` | `hasAnimationName(name: string): boolean` | `true` if any animation is a `CSSAnimation` with that `animationName`. | +| `hasAnimationId` | `hasAnimationId(id: string): boolean` | `true` if any animation has that `id`. | +| `getTimingOptions` | `getTimingOptions(): { delay: number; duration: number; iterations: number }[]` | One entry per animation, read from `effect.getTiming()`. | + +**Getters:** + +| Getter | Type | Behavior | +| ----------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `finished` | `Promise` | `Promise.all(animations.map(a => a.finished))`. **Plural โ€” an array of `Animation`, not a single one.** This (or `onFinish`) is the correct way to observe completion โ€” `await play()` does not. | +| `playState` | `AnimationPlayState` | `'running'` if **any** wrapped animation is `'running'`; otherwise the first animation's `playState`. | + +## Gotchas + +- **MUST** treat `await group.play()` as "playback started", not "playback finished" โ€” use + `group.onFinish(callback)` or `await group.finished` to react to completion. +- **MUST** account for `AnimationGroup.finished` being `Promise` โ€” don't destructure it + as a single `Animation`. +- **MUST** pass `getWebAnimation` a single `AnimationOptions` object, never an array โ€” use + `getSequence`/`createAnimationGroups` (see `./sequences.md`) to drive multiple elements together. +- **MUST** type the result of `getWebAnimation` as nullable (`AnimationGroup | MouseAnimationInstance | null`) + โ€” an unregistered `namedEffect`, a dropped reduced-motion animation, or a failed pointer factory all + produce `null`. +- **Rule:** `iterations: 0` means `Infinity`, not zero iterations; `undefined` means `1` + (`../src/api/common.ts:100`). This applies to both `TimeAnimationOptions.iterations` and + `ScrubAnimationOptions.iterations`. +- **Rule โ€” reduced motion** (`options.reducedMotion`, threaded through `getAnimation`/`getSequence`'s + `context.reducedMotion`; only affects **time-based**, non-scrub animations, + `../src/api/webAnimations.ts:38-44`): + - single-iteration (`iterations === 1` or `undefined`) โ‡’ collapsed to `duration: 1`. + - multi-iteration โ‡’ the effect returns `[]`, so `getWebAnimation` returns `null` (animation dropped + entirely, not just shortened). +- **Rule:** on `pointer-move`, the `axis` (`'x' | 'y'`) that selects which pointer coordinate drives + progress lives on the **trigger** object, not on `animationOptions`. + +## See Also + +- [`./motion-main.md`](./motion-main.md) โ€” package boundary, structural discrimination, effect-definition + modes, function map, easing reference. +- `./scrub-scenes.md` โ€” `getScrubScene`, `ScrubScrollScene`/`ScrubPointerScene`, and how the no-`ViewTimeline` + polyfill path drives `AnimationGroup.progress()` from scroll/pointer events.