From 821e47a0192cc68252fb5de8aaa67f0edf31c3b5 Mon Sep 17 00:00:00 2001 From: Ben Aultowski Date: Tue, 6 Jan 2026 17:38:53 -0500 Subject: [PATCH] Rough pass at an effects library --- docs/GSAP_ANIMATIONS.md | 66 ++- docs/GSAP_USAGE.md | 309 +++++++++++ src/_data/navigation.js | 4 + ...gsap-emotional-animation-demo-preview.jpeg | Bin 0 -> 42488 bytes src/assets/scripts/bundle/gsap-effects.js | 494 ++++++++++++++++++ .../scripts/bundle/gsap-shortcode-init.js | 141 +++-- .../scripts/bundle/mix-nav-animations.js | 152 ++++++ src/pages/gsap-animations.md | 352 +++++++++++++ .../2025/testing/emotional-animations.md | 123 +++++ 9 files changed, 1588 insertions(+), 53 deletions(-) create mode 100644 docs/GSAP_USAGE.md create mode 100644 src/assets/og-images/gsap-emotional-animation-demo-preview.jpeg create mode 100644 src/assets/scripts/bundle/gsap-effects.js create mode 100644 src/assets/scripts/bundle/mix-nav-animations.js create mode 100644 src/pages/gsap-animations.md create mode 100644 src/posts/2025/testing/emotional-animations.md diff --git a/docs/GSAP_ANIMATIONS.md b/docs/GSAP_ANIMATIONS.md index 992740c..c8f5507 100644 --- a/docs/GSAP_ANIMATIONS.md +++ b/docs/GSAP_ANIMATIONS.md @@ -1,22 +1,66 @@ -# GSAP Scroll Animations Guide +# GSAP Animation System -GSAP scroll-driven animations for photographs and content. Animations are controlled by scroll position—scrolling down plays forward, scrolling up plays backward. +This project uses GSAP (GreenSock Animation Platform) for scroll-driven and interactive animations, designed for animated storytelling with low friction. + +## Architecture Overview + +The animation system is split into three main parts: + +1. **Shared Effects Library** ([`gsap-effects.js`](../src/assets/scripts/bundle/gsap-effects.js)) + - Reusable animation effects (fadeIn, shake, zoom, etc.) + - Emotional presets (jumpscare, anticipation, dread, etc.) + - Effect composition utilities + +2. **Content Animations** ([`gsap-shortcode-init.js`](../src/assets/scripts/bundle/gsap-shortcode-init.js)) + - Scroll-triggered animations in markdown/blog posts + - Low-friction shortcode syntax for content authors + +3. **UI Component Animations** ([`mix-nav-animations.js`](../src/assets/scripts/bundle/mix-nav-animations.js)) + - Interactive UI animations (hover, click, etc.) + - Component-specific animation logic + +--- + +# For Content Authors + +See [GSAP_USAGE.md](./GSAP_USAGE.md) for the complete usage guide. ## Quick Start +### Emotional Presets (Recommended) + +Animate emotions, not numbers: + ```markdown -{% gsapScrollAnim { - "animationType": "fadeIn", - "scrollStart": "top 80%" -} %} -[{ - "src": "/path/to/image.jpg", - "alt": "Image description", - "caption": "Optional caption" -}] +{% gsapScrollAnim { "emotion": "jumpscare" } %} +[{ "src": "/scary-image.jpg", "alt": "Boo!" }] {% endgsapScrollAnim %} ``` +Available emotions: `jumpscare`, `anticipation`, `dread`, `relief`, `tension`, `excitement` + +### Simple Animations + +```markdown +{% gsapScrollAnim { "animationType": "fadeIn" } %} +[{ "src": "/image.jpg", "alt": "Description" }] +{% endgsapScrollAnim %} +``` + +### Effect Composition + +Combine multiple effects: + +```markdown +{% gsapScrollAnim { "effects": ["fadeIn", "shake", "tremble"] } %} +[{ "src": "/image.jpg", "alt": "Custom combo" }] +{% endgsapScrollAnim %} +``` + +--- + +# For Developers + ## Configuration Options All parameters are optional with sensible defaults: diff --git a/docs/GSAP_USAGE.md b/docs/GSAP_USAGE.md new file mode 100644 index 0000000..9228e1c --- /dev/null +++ b/docs/GSAP_USAGE.md @@ -0,0 +1,309 @@ +# GSAP Animation System + +This project uses GSAP for scroll-driven and UI animations, designed for low-friction animated storytelling. + +## Architecture + +### 1. Shared Effects Library (`gsap-effects.js`) +Contains reusable animation effects and emotional presets. + +### 2. Content Animations (`gsap-shortcode-init.js`) +Handles scroll-triggered animations in markdown/blog posts via shortcodes. + +### 3. UI Component Animations (`mix-nav-animations.js`) +Handles interactive animations for navigation, buttons, and UI elements. + +--- + +## For Content Authors (Markdown/Posts) + +### Basic Usage + +Simple fade-in animation: +```markdown +{% gsapScrollAnim { + "animationType": "fadeIn" +} %} +[{ + "src": "/path/to/image.jpg", + "alt": "Description" +}] +{% endgsapScrollAnim %} +``` + +### Available Animation Types + +- `fadeIn` - Fade in from below +- `fadeInUp` - Fade in from further below +- `fadeInDown` - Fade in from above +- `scaleIn` - Scale up from small +- `slideInLeft` - Slide in from left +- `slideInRight` - Slide in from right +- `parallax` - Parallax scroll effect +- `stagger` - Multiple items animate in sequence +- `zoomIn` - Zoom into image focal point +- `zoomOut` - Zoom out from focal point +- `shake` - Shake back and forth +- `tremble` - Subtle continuous trembling +- `wobble` - Wobble rotation +- `pulse` - Continuous pulsing scale + +### Zoom Animations with Focal Points + +```markdown +{% gsapScrollAnim { + "animationType": "zoomIn", + "focalX": 30, + "focalY": 40, + "startZoom": 1, + "endZoom": 2.5, + "scrub": true +} %} +[{ + "src": "/path/to/high-res-image.jpg", + "alt": "Image to zoom" +}] +{% endgsapScrollAnim %} +``` + +- `focalX` / `focalY`: 0-100 (percentage of image dimensions) +- `startZoom` / `endZoom`: Scale values (1 = normal size) + +--- + +## Emotional Presets (New!) + +Create emotional storytelling without technical details: + +### Jumpscare +```markdown +{% gsapScrollAnim { + "emotion": "jumpscare" +} %} +[{ "src": "/scary-image.jpg", "alt": "Boo!" }] +{% endgsapScrollAnim %} +``` +Sudden appearance + shake + tremble (like an arrow hitting its mark) + +### Anticipation +```markdown +{% gsapScrollAnim { + "emotion": "anticipation", + "scrub": false +} %} +[{ "src": "/windup.jpg", "alt": "Getting ready" }] +{% endgsapScrollAnim %} +``` +Pull back, then spring forward (like winding up before a punch) + +### Dread +```markdown +{% gsapScrollAnim { + "emotion": "dread" +} %} +[{ "src": "/ominous.jpg", "alt": "Something's coming" }] +{% endgsapScrollAnim %} +``` +Slow reveal with unsettling movement + +### Relief +```markdown +{% gsapScrollAnim { + "emotion": "relief" +} %} +[{ "src": "/safe-now.jpg", "alt": "Phew" }] +{% endgsapScrollAnim %} +``` +Gentle fade in with settling motion + +### Tension +```markdown +{% gsapScrollAnim { + "emotion": "tension" +} %} +[{ "src": "/suspense.jpg", "alt": "Building suspense" }] +{% endgsapScrollAnim %} +``` +Slow zoom with subtle shake + +### Excitement +```markdown +{% gsapScrollAnim { + "emotion": "excitement", + "scrub": false +} %} +[{ "src": "/celebration.jpg", "alt": "Yay!" }] +{% endgsapScrollAnim %} +``` +Bouncy entrance with energy + +--- + +## Effect Composition (Advanced) + +Combine multiple effects to create custom emotions: + +```markdown +{% gsapScrollAnim { + "effects": ["fadeIn", "shake", "tremble"] +} %} +[{ "src": "/custom-combo.jpg", "alt": "Custom animation" }] +{% endgsapScrollAnim %} +``` + +Effects apply in sequence and can overlap. + +--- + +## Scroll Control Options + +### `scrub` (default: `true`) +- `true` - Animation tied to scroll position (smooth) +- `false` - Animation plays once when triggered + +### `scrollStart` (default: `"top 80%"`) +When animation begins: +- `"top 80%"` - When element's top hits 80% down viewport +- `"center center"` - When element center hits viewport center +- `"bottom 20%"` - When element bottom hits 20% down viewport + +### `scrollEnd` (default: `"bottom 20%"`) +When animation completes (for scrubbed animations) + +### `pin` (default: `false`) +Pin the element in place during animation: +```markdown +{% gsapScrollAnim { + "animationType": "zoomIn", + "pin": true, + "scrollEnd": "+=500" +} %} +``` + +### `markers` (default: `false`) +Show debug markers (for development): +```markdown +{% gsapScrollAnim { + "animationType": "fadeIn", + "markers": true +} %} +``` + +--- + +## Multiple Images + +```markdown +{% gsapScrollAnim { + "animationType": "stagger" +} %} +[{ + "src": "/image1.jpg", + "alt": "First", + "caption": "Image 1" +}, { + "src": "/image2.jpg", + "alt": "Second", + "caption": "Image 2" +}, { + "src": "/image3.jpg", + "alt": "Third", + "caption": "Image 3" +}] +{% endgsapScrollAnim %} +``` + +--- + +## Tips for Storytelling + +1. **Use emotions first** - `"emotion": "jumpscare"` is easier than combining effects manually +2. **Scrub for slow reveals** - Set `"scrub": true` for scroll-controlled drama +3. **No scrub for punchy moments** - Set `"scrub": false` for quick actions +4. **Pin for focus** - Use `"pin": true` to hold attention on an element +5. **Zoom needs high-res** - Zoom animations automatically request larger image sizes +6. **Compose for unique feels** - Combine effects when presets don't fit: `"effects": ["fadeIn", "wobble"]` + +--- + +## For Developers + +### Adding New Effects + +Edit [`gsap-effects.js`](../src/assets/scripts/bundle/gsap-effects.js): + +```javascript +export const effects = { + myNewEffect: (element, config = {}) => ({ + from: { + opacity: 0, + rotationY: 90 + }, + to: { + opacity: 1, + rotationY: 0, + ease: 'power2.out', + ...config + } + }) +}; +``` + +### Adding Emotional Presets + +```javascript +export const emotions = { + myEmotion: (element, config = {}) => { + const tl = gsap.timeline(); + tl.from(element, { /* initial state */ }) + .to(element, { /* first animation */ }) + .to(element, { /* second animation */ }, '-=0.5'); // overlap + return tl; + } +}; +``` + +### UI Component Animations + +Create component-specific files like [`mix-nav-animations.js`](../src/assets/scripts/bundle/mix-nav-animations.js): + +```javascript +import gsap from 'gsap'; +import { shouldAnimate } from './gsap-effects.js'; + +function initMyComponentAnimations() { + if (!shouldAnimate()) return; + + document.querySelectorAll('.my-element').forEach(el => { + el.addEventListener('mouseenter', () => { + gsap.to(el, { scale: 1.1, duration: 0.2 }); + }); + }); +} + +// Turbo-compatible initialization +document.addEventListener('DOMContentLoaded', initMyComponentAnimations); +if (window.Turbo) { + document.addEventListener('turbo:load', initMyComponentAnimations); +} +``` + +--- + +## Accessibility + +All animations respect `prefers-reduced-motion`. Users with this preference will see static content without animations. + +--- + +## Debugging + +Enable markers to see scroll trigger points: +```markdown +{% gsapScrollAnim { + "animationType": "fadeIn", + "markers": true +} %} +``` + +Check browser console for warnings about missing animation types or configuration errors. diff --git a/src/_data/navigation.js b/src/_data/navigation.js index ebb405a..89d952e 100644 --- a/src/_data/navigation.js +++ b/src/_data/navigation.js @@ -21,6 +21,10 @@ export default { { text: 'Style guide', url: '/styleguide/' + }, + { + text: 'GSAP Animations', + url: '/gsap-animations/' } ] }; diff --git a/src/assets/og-images/gsap-emotional-animation-demo-preview.jpeg b/src/assets/og-images/gsap-emotional-animation-demo-preview.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..16b710f6357400c4c6e303652fdd1a342a0c7984 GIT binary patch literal 42488 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 ({ + from: { + opacity: 0, + y: 50 + }, + to: { + opacity: 1, + y: 0, + ease: 'power2.out', + ...config + } + }), + + fadeInUp: (element, config = {}) => ({ + from: { + opacity: 0, + y: 100 + }, + to: { + opacity: 1, + y: 0, + ease: 'power2.out', + ...config + } + }), + + fadeInDown: (element, config = {}) => ({ + from: { + opacity: 0, + y: -100 + }, + to: { + opacity: 1, + y: 0, + ease: 'power2.out', + ...config + } + }), + + scaleIn: (element, config = {}) => ({ + from: { + opacity: 0, + scale: 0.8 + }, + to: { + opacity: 1, + scale: 1, + ease: 'back.out(1.7)', + ...config + } + }), + + slideInLeft: (element, config = {}) => ({ + from: { + opacity: 0, + x: -100 + }, + to: { + opacity: 1, + x: 0, + ease: 'power2.out', + ...config + } + }), + + slideInRight: (element, config = {}) => ({ + from: { + opacity: 0, + x: 100 + }, + to: { + opacity: 1, + x: 0, + ease: 'power2.out', + ...config + } + }), + + parallax: (element, config = {}) => ({ + from: { + y: -100 + }, + to: { + y: 100, + ease: 'none', + ...config + } + }), + + stagger: (element, config = {}) => ({ + from: { + opacity: 0, + y: 50 + }, + to: { + opacity: 1, + y: 0, + ease: 'power2.out', + stagger: 0.2, + ...config + } + }), + + shake: (element, config = {}) => ({ + from: {}, + to: { + x: 0, + duration: 0.1, + repeat: 5, + yoyo: true, + ease: 'power1.inOut', + keyframes: { + x: [-5, 5, -4, 4, -3, 3, -2, 2, -1, 0] + }, + ...config + } + }), + + tremble: (element, config = {}) => ({ + from: {}, + to: { + rotation: 0, + duration: 0.05, + repeat: -1, + yoyo: true, + ease: 'none', + keyframes: { + rotation: [-1, 1, -1, 1] + }, + ...config + } + }), + + pulse: (element, config = {}) => ({ + from: {}, + to: { + scale: 1, + duration: 0.8, + repeat: -1, + yoyo: true, + ease: 'power1.inOut', + keyframes: { + scale: [1, 1.05, 1] + }, + ...config + } + }), + + wobble: (element, config = {}) => ({ + from: {}, + to: { + rotation: 0, + duration: 0.3, + repeat: 3, + yoyo: true, + ease: 'power1.inOut', + keyframes: { + rotation: [-5, 5, -3, 3, -1, 0] + }, + ...config + } + }), + + zoomIn: (element, config = {}) => { + const { + focalX = 50, + focalY = 50, + startZoom = 1, + endZoom = 2.5 + } = config || {}; + + return { + from: { + scale: startZoom, + transformOrigin: `${focalX}% ${focalY}%`, + x: 0, + y: 0 + }, + to: { + scale: endZoom, + transformOrigin: `${focalX}% ${focalY}%`, + x: 0, + y: 0, + ease: 'power2.inOut', + ...config + } + }; + }, + + zoomOut: (element, config = {}) => { + const { + focalX = 50, + focalY = 50, + startZoom = 2.5, + endZoom = 1 + } = config || {}; + + return { + from: { + scale: startZoom, + transformOrigin: `${focalX}% ${focalY}%`, + x: 0, + y: 0 + }, + to: { + scale: endZoom, + transformOrigin: `${focalX}% ${focalY}%`, + x: 0, + y: 0, + ease: 'power2.inOut', + ...config + } + }; + }, + + vibrate: (target, config = {}) => { + return { + to: { + x: () => gsap.utils.random(-5, 5), + y: () => gsap.utils.random(-5, 5), + duration: 0.05, + repeat: config.repeat ?? 10, + ease: 'none', + ...config + } + }; + } +}; + +/** + * animation combos, express emtions, tell a story + * **************************************************** + * **************************************************** + * **************************************************** + *****************************************************/ +export const emotions = { +/** + * jumpscare + * **************************************************** + * **************************************************** + * **************************************************** + *****************************************************/ + jumpscare: (element, config = {}) => { + const tl = gsap.timeline(); + tl.from(element, { + scale: 0.5, + opacity: 0, + duration: 0.1, + ease: 'power4.out' + }) + .to(element, { + x: -15, + y: -15, + rotation: 30, + duration: 1, + repeat: 3, + yoyo: true + }) + .to(element, { + rotation: -2, + duration: 0.05, + repeat: -1, + yoyo: true + }); + return tl; + }, + +/** + * anticipation + * **************************************************** + * **************************************************** + * **************************************************** + *****************************************************/ + anticipation: (element, config = {}) => { + const tl = gsap.timeline(); + tl.to(element, { + scale: 0.95, + duration: 0.3, + ease: 'power2.in' + }) + .to(element, { + scale: 1.1, + duration: 0.2, + ease: 'back.out(4)' + }) + .to(element, { + scale: 1, + duration: 0.2, + ease: 'power1.out' + }); + return tl; + }, + +/** + * dread, alledgedly + * **************************************************** + * **************************************************** + * **************************************************** + *****************************************************/ + dread: (element, config = {}) => { + const tl = gsap.timeline(); + tl.from(element, { + opacity: 0, + scale: 1.2, + duration: 2, + ease: 'power1.in' + }) + .to(element, { + rotation: -1, + duration: 0.1, + repeat: -1, + yoyo: true, + ease: 'none' + }, '-=1.5'); + return tl; + }, + +/** + * relief, alledgedly + * **************************************************** + * **************************************************** + * **************************************************** + *****************************************************/ + relief: (element, config = {}) => { + const tl = gsap.timeline(); + tl.from(element, { + opacity: 0, + y: -30, + duration: 1, + ease: 'power2.out' + }) + .to(element, { + y: 5, + duration: 0.4, + ease: 'power1.inOut' + }) + .to(element, { + y: 0, + duration: 0.3, + ease: 'power1.out' + }); + return tl; + }, + +/** + * tension + * **************************************************** + * **************************************************** + * **************************************************** + *****************************************************/ + tension: (element, config = {}) => { + const tl = gsap.timeline(); + tl.from(element, { + scale: 1, + duration: 3, + ease: 'none' + }) + .to(element, { + scale: 1.15, + duration: 3, + ease: 'power1.in' + }, 0) + .to(element, { + x: -1, + duration: 0.1, + repeat: -1, + yoyo: true, + ease: 'none' + }, 1); + return tl; + }, + +/** + * excitement + * **************************************************** + * **************************************************** + * **************************************************** + *****************************************************/ + excitement: (element, config = {}) => { + const tl = gsap.timeline(); + tl.from(element, { + opacity: 0, + scale: 0, + duration: 0.3, + ease: 'back.out(3)' + }) + .to(element, { + y: -10, + duration: 0.3, + ease: 'power2.out' + }) + .to(element, { + y: 0, + duration: 0.3, + ease: 'bounce.out' + }); + return tl; + }, + + /** + * Image Swap: Swap image source at scroll position and vibrate + */ + imageSwap: (element, config = {}) => { + const tl = gsap.timeline({ + scrollTrigger: { + trigger: element, + start: 'center center', + toggleActions: 'play none none none', + once: true, + markers: config.markers || false, + ...config.scrollTrigger + } + }); + + // Get both images from config or data attributes + const img = element.querySelector('img'); + const secondSrc = config.secondImage || element.dataset.secondImage; + + if (!secondSrc || !img) { + console.warn('imageSwap: No second image specified or img element not found', element); + return tl; + } + + // Swap image and vibrate simultaneously + tl.call(() => { + img.src = secondSrc; + // Also update srcset if it exists + if (img.srcset) { + img.srcset = secondSrc; + } + }) + .to(element, { + x: () => gsap.utils.random(-5, 5), + y: () => gsap.utils.random(-5, 5), + duration: 0.05, + repeat: config.vibrateRepeats ?? 15, + ease: 'none' + }, 0); // 0 means start immediately + + return tl; + } +}; + +/** + * Check if user prefers reduced motion + */ +export const shouldAnimate = () => + !window.matchMedia('(prefers-reduced-motion: reduce)').matches; + +/** + * Apply multiple effects to an element + * @param {Element} element - Target element + * @param {Array} effectNames - Array of effect names to apply + * @param {Object} config - Configuration for effects + */ +export const composeEffects = (element, effectNames, config = {}) => { + if (!shouldAnimate()) return gsap.timeline(); + + const tl = gsap.timeline(); + + effectNames.forEach((effectName, index) => { + const effect = effects[effectName]; + if (effect) { + const { from, to } = effect(element, config[effectName] || {}); + + if (index === 0 && Object.keys(from).length > 0) { + tl.from(element, from); + } + if (Object.keys(to).length > 0) { + // If this is a looping effect (repeat: -1), add it at the start + if (to.repeat === -1) { + tl.to(element, to, 0); + } else { + tl.to(element, to); + } + } + } + }); + + return tl; +}; + +export default { effects, emotions, shouldAnimate, composeEffects }; diff --git a/src/assets/scripts/bundle/gsap-shortcode-init.js b/src/assets/scripts/bundle/gsap-shortcode-init.js index ca64145..843a755 100644 --- a/src/assets/scripts/bundle/gsap-shortcode-init.js +++ b/src/assets/scripts/bundle/gsap-shortcode-init.js @@ -6,6 +6,7 @@ import gsap from 'gsap'; import ScrollTrigger from 'gsap/ScrollTrigger'; +import { effects, emotions, shouldAnimate, composeEffects } from './gsap-effects.js'; // Register GSAP plugins gsap.registerPlugin(ScrollTrigger); @@ -14,7 +15,7 @@ gsap.registerPlugin(ScrollTrigger); const activeContexts = new Map(); /** - * Animation presets + * Animation presets (use effects from gsap-effects.js, but kept for backward compatibility) * Each returns GSAP animation properties for the given element(s) */ const animations = { @@ -177,9 +178,7 @@ const animations = { */ function initGsapAnimations() { // Check if reduced motion is preferred - const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - - if (prefersReducedMotion) { + if (!shouldAnimate()) { // Skip animations if user prefers reduced motion console.log('GSAP animations disabled: prefers-reduced-motion'); return; @@ -192,8 +191,12 @@ function initGsapAnimations() { try { // Parse configuration from data attribute const config = JSON.parse(container.dataset.gsapScrollAnim); + + // Extract configuration with defaults const { - animationType = 'fadeIn', + animationType, + emotion, + effects: effectsList, scrollStart = 'top 80%', scrollEnd = 'bottom 20%', scrub = true, @@ -201,51 +204,105 @@ function initGsapAnimations() { markers = false } = config; - // Get animation preset - const animationPreset = animations[animationType]; - if (!animationPreset) { - console.warn(`Unknown animation type: ${animationType}`); - return; - } - // Find all animated items within container const items = container.querySelectorAll('.gsap-item'); if (items.length === 0) return; // Create GSAP context for this container const ctx = gsap.context(() => { - // For zoom animations, apply to images directly - const targetElements = (animationType === 'zoomIn' || animationType === 'zoomOut') - ? container.querySelectorAll('.gsap-image') - : items; + let timeline; - const anim = animationPreset(targetElements, config); - - // Create timeline with ScrollTrigger - const timeline = gsap.timeline({ - scrollTrigger: { - trigger: container, - start: scrollStart, - end: scrollEnd, - scrub: scrub ? 1 : false, // Smooth scrubbing - markers: markers, // Show markers for debugging - pin: pin, - toggleActions: scrub ? undefined : 'play reverse play reverse', - // Callbacks for debugging - onEnter: () => container.dataset.gsapActive = 'true', - onLeave: () => container.dataset.gsapActive = 'false', - onEnterBack: () => container.dataset.gsapActive = 'true', - onLeaveBack: () => container.dataset.gsapActive = 'false' + // Handle emotional presets (take priority) + if (emotion && emotions[emotion]) { + timeline = gsap.timeline({ + scrollTrigger: { + trigger: container, + start: scrollStart, + end: scrollEnd, + scrub: scrub ? 1 : false, + markers: markers, + pin: pin, + toggleActions: scrub ? undefined : 'play reverse play reverse', + onEnter: () => container.dataset.gsapActive = 'true', + onLeave: () => container.dataset.gsapActive = 'false', + onEnterBack: () => container.dataset.gsapActive = 'true', + onLeaveBack: () => container.dataset.gsapActive = 'false' + } + }); + + // Apply emotional preset to each item + items.forEach(item => { + const emotionTl = emotions[emotion](item, config); + timeline.add(emotionTl, 0); // Add at start + }); + } + // Handle effect composition (multiple effects) + else if (effectsList && Array.isArray(effectsList)) { + timeline = gsap.timeline({ + scrollTrigger: { + trigger: container, + start: scrollStart, + end: scrollEnd, + scrub: scrub ? 1 : false, + markers: markers, + pin: pin, + toggleActions: scrub ? undefined : 'play reverse play reverse', + onEnter: () => container.dataset.gsapActive = 'true', + onLeave: () => container.dataset.gsapActive = 'false', + onEnterBack: () => container.dataset.gsapActive = 'true', + onLeaveBack: () => container.dataset.gsapActive = 'false' + } + }); + + // Apply each effect in sequence + items.forEach(item => { + const composedTl = composeEffects(item, effectsList, config); + timeline.add(composedTl, 0); + }); + } + // Handle single animation type (original behavior) + else if (animationType) { + const animationPreset = animations[animationType]; + if (!animationPreset) { + console.warn(`Unknown animation type: ${animationType}`); + return; } - }); - - // Apply animation - if (anim.from && anim.to) { - timeline.fromTo(items, anim.from, anim.to); - } else if (anim.from) { - timeline.from(items, anim.from); - } else if (anim.to) { - timeline.to(items, anim.to); + + // For zoom animations, apply to images directly + const targetElements = (animationType === 'zoomIn' || animationType === 'zoomOut') + ? container.querySelectorAll('.gsap-image') + : items; + + const anim = animationPreset(targetElements, config); + + // Create timeline with ScrollTrigger + timeline = gsap.timeline({ + scrollTrigger: { + trigger: container, + start: scrollStart, + end: scrollEnd, + scrub: scrub ? 1 : false, + markers: markers, + pin: pin, + toggleActions: scrub ? undefined : 'play reverse play reverse', + onEnter: () => container.dataset.gsapActive = 'true', + onLeave: () => container.dataset.gsapActive = 'false', + onEnterBack: () => container.dataset.gsapActive = 'true', + onLeaveBack: () => container.dataset.gsapActive = 'false' + } + }); + + // Apply animation + if (anim.from && anim.to) { + timeline.fromTo(targetElements, anim.from, anim.to); + } else if (anim.from) { + timeline.from(targetElements, anim.from); + } else if (anim.to) { + timeline.to(targetElements, anim.to); + } + } else { + console.warn('No animation type, emotion, or effects specified', config); + return; } }, container); diff --git a/src/assets/scripts/bundle/mix-nav-animations.js b/src/assets/scripts/bundle/mix-nav-animations.js new file mode 100644 index 0000000..3ea1f4a --- /dev/null +++ b/src/assets/scripts/bundle/mix-nav-animations.js @@ -0,0 +1,152 @@ +/** + * Mix Navigation Animations + * GSAP animations for mix track navigation UI elements + */ +import gsap from 'gsap'; +import { shouldAnimate } from './gsap-effects.js'; + +/** + * Initialize mix navigation animations + */ +function initMixNavAnimations() { + if (!shouldAnimate()) return; + + // Animate track navigation buttons on hover + const trackNavButtons = document.querySelectorAll('[data-track-nav]'); + + trackNavButtons.forEach(btn => { + const direction = btn.closest('[data-direction]')?.dataset.direction; + + // Set up hover animation + btn.addEventListener('mouseenter', () => { + gsap.to(btn, { + scale: 1.05, + duration: 0.2, + ease: 'back.out(2)' + }); + + // Slight movement based on direction + if (direction === 'prev') { + gsap.to(btn, { + x: -3, + duration: 0.2, + ease: 'power2.out' + }); + } else if (direction === 'next') { + gsap.to(btn, { + x: 3, + duration: 0.2, + ease: 'power2.out' + }); + } + }); + + btn.addEventListener('mouseleave', () => { + gsap.to(btn, { + scale: 1, + x: 0, + duration: 0.2, + ease: 'power2.inOut' + }); + }); + + // Click animation + btn.addEventListener('click', (e) => { + gsap.to(btn, { + scale: 0.95, + duration: 0.1, + yoyo: true, + repeat: 1, + ease: 'power2.inOut' + }); + }); + }); + + // Animate track list background on hover + const trackListBg = document.querySelectorAll('.track-list-bg'); + + trackListBg.forEach(bg => { + const btn = bg.querySelector('.button'); + if (!btn) return; + + btn.addEventListener('mouseenter', () => { + gsap.to(bg, { + scale: 1.03, + duration: 0.2, + ease: 'back.out(2)' + }); + }); + + btn.addEventListener('mouseleave', () => { + gsap.to(bg, { + scale: 1, + duration: 0.2, + ease: 'power2.inOut' + }); + }); + }); + + // Animate track nav backgrounds on hover + const trackNavBg = document.querySelectorAll('.track-nav-bg'); + + trackNavBg.forEach(bg => { + const btn = bg.querySelector('[data-track-nav]'); + if (!btn) return; + + const direction = bg.dataset.direction; + + btn.addEventListener('mouseenter', () => { + gsap.to(bg, { + scale: 1.02, + duration: 0.2, + ease: 'power2.out' + }); + + // Slight rotation based on direction + if (direction === 'prev') { + gsap.to(bg, { + rotation: -1, + duration: 0.2, + ease: 'power2.out' + }); + } else if (direction === 'next') { + gsap.to(bg, { + rotation: 1, + duration: 0.2, + ease: 'power2.out' + }); + } + }); + + btn.addEventListener('mouseleave', () => { + gsap.to(bg, { + scale: 1, + rotation: 0, + duration: 0.2, + ease: 'power2.inOut' + }); + }); + }); +} + +/** + * Cleanup function for animations + */ +function cleanupMixNavAnimations() { + // Kill all active tweens on nav elements + gsap.killTweensOf('[data-track-nav]'); + gsap.killTweensOf('.track-list-bg'); + gsap.killTweensOf('.track-nav-bg'); +} + +// Initialize on page load +document.addEventListener('DOMContentLoaded', initMixNavAnimations); + +// Turbo Drive compatibility +if (window.Turbo) { + document.addEventListener('turbo:before-render', cleanupMixNavAnimations); + document.addEventListener('turbo:render', initMixNavAnimations); + document.addEventListener('turbo:load', initMixNavAnimations); +} + +export { initMixNavAnimations, cleanupMixNavAnimations }; diff --git a/src/pages/gsap-animations.md b/src/pages/gsap-animations.md new file mode 100644 index 0000000..9b046ab --- /dev/null +++ b/src/pages/gsap-animations.md @@ -0,0 +1,352 @@ +--- +title: GSAP Animation Reference +description: Visual guide to all available GSAP animations and how to use them +layout: post +permalink: 'docs/animations/index.html' +--- + +# GSAP Animation Reference + +A visual reference for all available scroll-driven animations. Scroll down to see each effect in action! + +## Quick Start + +```markdown +{% raw %}{% gsapScrollAnim { "animationType": "fadeIn" } %} +[{ "src": "/path/to/image.jpg", "alt": "My image" }] +{% endgsapScrollAnim %}{% endraw %} +``` + +--- + +## Basic Effects + +### Fade In +Gentle fade and slide up entrance. + +{% gsapScrollAnim { "animationType": "fadeIn", "scrollStart": "top 80%", "scrollEnd": "bottom 20%", "scrub": true } %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/gorillaz-cracker-island.jpg", "alt": "Fade In Demo" }] +{% endgsapScrollAnim %} + +```markdown +{% raw %}{% gsapScrollAnim { "animationType": "fadeIn" } %} +[{ "src": "/image.jpg", "alt": "Demo" }] +{% endgsapScrollAnim %}{% endraw %} +``` + +### Fade In Up +Strong upward entrance. + +{% gsapScrollAnim { "animationType": "fadeInUp", "scrollStart": "top 80%", "scrollEnd": "bottom 20%", "scrub": true } %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg", "alt": "Fade In Up Demo" }] +{% endgsapScrollAnim %} + +```markdown +{% raw %}{% gsapScrollAnim { "animationType": "fadeInUp" } %} +[{ "src": "/image.jpg", "alt": "Demo" }] +{% endgsapScrollAnim %}{% endraw %} +``` + +### Fade In Down +Drops in from above. + +{% gsapScrollAnim { "animationType": "fadeInDown", "scrollStart": "top 80%", "scrollEnd": "bottom 20%", "scrub": true } %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/the-clash-london-calling.jpeg", "alt": "Fade In Down Demo" }] +{% endgsapScrollAnim %} + +```markdown +{% raw %}{% gsapScrollAnim { "animationType": "fadeInDown" } %} +[{ "src": "/image.jpg", "alt": "Demo" }] +{% endgsapScrollAnim %}{% endraw %} +``` + +### Scale In +Grows from center with bounce. + +{% gsapScrollAnim { "animationType": "scaleIn", "scrollStart": "top 80%", "scrollEnd": "bottom 20%", "scrub": false } %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/james-whiplash.jpg", "alt": "Scale In Demo" }] +{% endgsapScrollAnim %} + +```markdown +{% raw %}{% gsapScrollAnim { "animationType": "scaleIn", "scrub": false } %} +[{ "src": "/image.jpg", "alt": "Demo" }] +{% endgsapScrollAnim %}{% endraw %} +``` + +### Slide In Left + +{% gsapScrollAnim { "animationType": "slideInLeft", "scrollStart": "top 80%", "scrollEnd": "bottom 20%", "scrub": true } %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/rtj-rtj4.jpg", "alt": "Slide In Left Demo" }] +{% endgsapScrollAnim %} + +### Slide In Right + +{% gsapScrollAnim { "animationType": "slideInRight", "scrollStart": "top 80%", "scrollEnd": "bottom 20%", "scrub": true } %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/ride-nowhere.jpg", "alt": "Slide In Right Demo" }] +{% endgsapScrollAnim %} + +--- + +## Zoom Effects + +### Zoom In +Slow zoom into the image as you scroll. + +{% gsapScrollAnim { "animationType": "zoomIn", "scrollStart": "top 80%", "scrollEnd": "middle middle", "scrub": true } %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/modest-mouse-we-were-dead.jpg", "alt": "Zoom In Demo" }] +{% endgsapScrollAnim %} + +```markdown +{% raw %}{% gsapScrollAnim { + "animationType": "zoomIn", + "focalX": 50, + "focalY": 50, + "startZoom": 1, + "endZoom": 2.5, + "scrub": true +} %} +[{ "src": "/image.jpg", "alt": "Demo" }] +{% endgsapScrollAnim %}{% endraw %} +``` + +### Zoom Out +Reverse zoom effect. + +{% gsapScrollAnim { "animationType": "zoomOut", "scrollStart": "top 80%", "scrollEnd": "bottom 20%", "scrub": true } %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/morphine-yes.jpg", "alt": "Zoom Out Demo" }] +{% endgsapScrollAnim %} + +--- + +## Emotional Presets + +These animations tell stories, not just numbers. + +### Jumpscare 💥 +Sudden, intense appearance like an arrow hitting its mark. + +{% gsapScrollAnim { "emotion": "jumpscare", "scrub": false } %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/james-whiplash.jpg", "alt": "Jumpscare Demo" }] +{% endgsapScrollAnim %} + +```markdown +{% raw %}{% gsapScrollAnim { "emotion": "jumpscare", "scrub": false } %} +[{ "src": "/image.jpg", "alt": "Demo" }] +{% endgsapScrollAnim %}{% endraw %} +``` + +### Anticipation ⏳ +Wind up before the punch - builds tension. + +{% gsapScrollAnim { "emotion": "anticipation", "scrub": false } %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/gorillaz-cracker-island.jpg", "alt": "Anticipation Demo" }] +{% endgsapScrollAnim %} + +```markdown +{% raw %}{% gsapScrollAnim { "emotion": "anticipation", "scrub": false } %} +[{ "src": "/image.jpg", "alt": "Demo" }] +{% endgsapScrollAnim %}{% endraw %} +``` + +### Dread 😰 +Something ominous slowly approaches. + +{% gsapScrollAnim { "emotion": "dread", "scrub": true } %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg", "alt": "Dread Demo" }] +{% endgsapScrollAnim %} + +```markdown +{% raw %}{% gsapScrollAnim { "emotion": "dread", "scrub": true } %} +[{ "src": "/image.jpg", "alt": "Demo" }] +{% endgsapScrollAnim %}{% endraw %} +``` + +### Relief 😌 +Everything's going to be okay - gentle, calming. + +{% gsapScrollAnim { "emotion": "relief", "scrub": false } %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/the-clash-london-calling.jpeg", "alt": "Relief Demo" }] +{% endgsapScrollAnim %} + +```markdown +{% raw %}{% gsapScrollAnim { "emotion": "relief", "scrub": false } %} +[{ "src": "/image.jpg", "alt": "Demo" }] +{% endgsapScrollAnim %}{% endraw %} +``` + +### Tension 😬 +Slow zoom with subtle shake - something's wrong. + +{% gsapScrollAnim { "emotion": "tension", "scrub": true } %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/bjork-all-is-full-of-love.jpg", "alt": "Tension Demo" }] +{% endgsapScrollAnim %} + +### Excitement 🎉 +Bouncy, energetic entrance. + +{% gsapScrollAnim { "emotion": "excitement", "scrub": false } %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/parquet-courts-wide-awake.png", "alt": "Excitement Demo" }] +{% endgsapScrollAnim %} + +--- + +## Advanced: Image Swap 🔄 + +### Emotion Preset (Easier) +Swap images when scrolled halfway past, with vibration effect. + +{% gsapScrollAnim { + "emotion": "imageSwap", + "secondImage": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg", + "vibrateRepeats": 20 +} %} +[{ + "src": "/pages/projects/mixes/tomorrowsbacon/gorillaz-cracker-island.jpg", + "alt": "Image Swap Emotion Demo", + "data-second-image": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg" +}] +{% endgsapScrollAnim %} + +```markdown +{% raw %}{% gsapScrollAnim { + "emotion": "imageSwap", + "secondImage": "/path/to/second-image.jpg", + "vibrateRepeats": 20 +} %} +[{ + "src": "/path/to/first-image.jpg", + "alt": "Demo", + "data-second-image": "/path/to/second-image.jpg" +}] +{% endgsapScrollAnim %}{% endraw %} +``` + +### Effects Composition (More Control) +Combine vibrate with other effects for custom animations. + +{% gsapScrollAnim { + "effects": ["fadeIn", "vibrate"], + "scrollStart": "top 80%", + "scrollEnd": "bottom 20%", + "scrub": false +} %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/operation-ivy-energy.jpg", "alt": "Vibrate Effect Demo" }] +{% endgsapScrollAnim %} + +```markdown +{% raw %}{% gsapScrollAnim { + "effects": ["fadeIn", "vibrate"], + "scrub": false +} %} +[{ "src": "/image.jpg", "alt": "Demo" }] +{% endgsapScrollAnim %}{% endraw %} +``` + +--- + +## Effect Composition + +Combine multiple effects to create unique animations. + +### Fade + Shake + +{% gsapScrollAnim { + "effects": ["fadeIn", "shake"], + "scrollStart": "top 80%", + "scrollEnd": "bottom 20%", + "scrub": false +} %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/ramones-mania.jpg", "alt": "Fade + Shake Demo" }] +{% endgsapScrollAnim %} + +```markdown +{% raw %}{% gsapScrollAnim { + "effects": ["fadeIn", "shake"], + "scrub": false +} %} +[{ "src": "/image.jpg", "alt": "Demo" }] +{% endgsapScrollAnim %}{% endraw %} +``` + +### Scale + Wobble + Pulse + +{% gsapScrollAnim { + "effects": ["scaleIn", "wobble", "pulse"], + "scrollStart": "top 80%", + "scrollEnd": "bottom 20%", + "scrub": false +} %} +[{ "src": "/pages/projects/mixes/tomorrowsbacon/van-halen-van-halen.jpg", "alt": "Triple Combo Demo" }] +{% endgsapScrollAnim %} + +```markdown +{% raw %}{% gsapScrollAnim { + "effects": ["scaleIn", "wobble", "pulse"], + "scrub": false +} %} +[{ "src": "/image.jpg", "alt": "Demo" }] +{% endgsapScrollAnim %}{% endraw %} +``` + +--- + +## Configuration Options + +### Scrub +- `"scrub": true` - Animation progress tied to scroll position (smooth) +- `"scrub": false` - Animation plays once when triggered + +### Scroll Triggers +- `"scrollStart": "top 80%"` - When to start (element position + viewport position) +- `"scrollEnd": "bottom 20%"` - When to end +- Common values: `"top center"`, `"center center"`, `"bottom top"` + +### Pin +- `"pin": true` - Pin element in place while animating +- `"pin": false` - Element scrolls normally + +### Markers +- `"markers": true` - Show debug markers (for development) +- `"markers": false` - Hide markers (for production) + +--- + +## Tips & Best Practices + +1. **Use emotions for storytelling** - They're designed to evoke feelings +2. **Scrub for cinematic effects** - Ties animation to scroll for precise control +3. **No scrub for surprise** - Let animations play independently +4. **Compose effects carefully** - Too many can be overwhelming +5. **Test on mobile** - Animations may need adjustment for smaller screens +6. **Respect reduced motion** - All animations respect `prefers-reduced-motion` setting + +--- + +## All Available Effects + +**Base Effects:** +- `fadeIn`, `fadeInUp`, `fadeInDown` +- `scaleIn` +- `slideInLeft`, `slideInRight` +- `parallax` +- `stagger` +- `shake`, `tremble`, `pulse`, `wobble` +- `zoomIn`, `zoomOut` +- `vibrate` + +**Emotional Presets:** +- `jumpscare` - Sudden impact +- `anticipation` - Building tension +- `dread` - Ominous approach +- `relief` - Calming resolution +- `tension` - Slow building stress +- `excitement` - Bouncy energy +- `imageSwap` - Image replacement with vibration + +**Composition:** +- Combine any effects using `"effects": ["effect1", "effect2"]` +- Effects can be layered for unique results + +--- + +

View this page's source code to see exactly how each animation is configured.

diff --git a/src/posts/2025/testing/emotional-animations.md b/src/posts/2025/testing/emotional-animations.md new file mode 100644 index 0000000..03975a5 --- /dev/null +++ b/src/posts/2025/testing/emotional-animations.md @@ -0,0 +1,123 @@ +--- +title: 'GSAP Emotional Animation Demo' +description: "Demonstrating the new emotional animation presets" +date: 2026-01-05 +tags: ['test', 'gsap'] +--- + +## Emotional Presets Demo + +Testing the new emotional animation system that lets you animate feelings, not just numbers. + +### Jumpscare + +Like an arrow hitting its mark - sudden appearance with impact: + +{% gsapScrollAnim { + "emotion": "jumpscare", + "scrub": true +} %} +[{ + "src": "/pages/projects/mixes/tomorrowsbacon/james-whiplash.jpg", + "alt": "Sudden impact!" +}] +{% endgsapScrollAnim %} + +### Anticipation + +Wind up before the punch: + +{% gsapScrollAnim { + "emotion": "anticipation", + "scrub": false, + "scrollStart": "top 75%" +} %} +[{ + "src": "/pages/projects/mixes/tomorrowsbacon/gorillaz-cracker-island.jpg", + "alt": "Building up..." +}] +{% endgsapScrollAnim %} + +### Dread + +Something ominous approaches: + +{% gsapScrollAnim { + "emotion": "dread", + "scrub": true +} %} +[{ + "src": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg", + "alt": "Unsettling feeling" +}] +{% endgsapScrollAnim %} + +### Relief + +Everything's going to be okay: + +{% gsapScrollAnim { + "emotion": "relief", + "scrub": false +} %} +[{ + "src": "/pages/projects/mixes/tomorrowsbacon/the-clash-london-calling.jpeg", + "alt": "Phew, safe now" +}] +{% endgsapScrollAnim %} + +### Excitement + +Bouncy, energetic entrance: + +{% gsapScrollAnim { + "emotion": "excitement", + "scrub": false +} %} +[{ + "src": "/pages/projects/mixes/tomorrowsbacon/james-whiplash.jpg", + "alt": "So exciting!" +}] +{% endgsapScrollAnim %} + +## Effect Composition + +Combining multiple effects to create custom emotions: + +{% gsapScrollAnim { + "effects": ["fadeIn", "shake"], + "scrub": true +} %} +[{ + "src": "/pages/projects/mixes/tomorrowsbacon/gorillaz-cracker-island.jpg", + "alt": "Fade in + shake combo" +}] +{% endgsapScrollAnim %} + +### Triple Combo + +{% gsapScrollAnim { + "effects": ["scaleIn", "wobble", "pulse"], + "scrub": false +} %} +[{ + "src": "/pages/projects/mixes/tomorrowsbacon/radiohead-ok-computer.jpg", + "alt": "Scale + wobble + pulse" +}] +{% endgsapScrollAnim %} + +--- + +## Simple Animations Still Work + +The original simple syntax still works perfectly: + +{% gsapScrollAnim { + "animationType": "fadeIn", + "scrub": true +} %} +[{ + "src": "/pages/projects/mixes/tomorrowsbacon/the-clash-london-calling.jpeg", + "alt": "Classic fade in" +}] +{% endgsapScrollAnim %}