Rough pass at an effects library
This commit is contained in:
parent
9d3778f74e
commit
821e47a019
9 changed files with 1588 additions and 53 deletions
|
|
@ -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:
|
||||
|
|
|
|||
309
docs/GSAP_USAGE.md
Normal file
309
docs/GSAP_USAGE.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -21,6 +21,10 @@ export default {
|
|||
{
|
||||
text: 'Style guide',
|
||||
url: '/styleguide/'
|
||||
},
|
||||
{
|
||||
text: 'GSAP Animations',
|
||||
url: '/gsap-animations/'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
|||
BIN
src/assets/og-images/gsap-emotional-animation-demo-preview.jpeg
Normal file
BIN
src/assets/og-images/gsap-emotional-animation-demo-preview.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
494
src/assets/scripts/bundle/gsap-effects.js
Normal file
494
src/assets/scripts/bundle/gsap-effects.js
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
/**
|
||||
* GSAP Effects Library
|
||||
* Shared animation effects for both shortcodes and UI components
|
||||
*/
|
||||
import gsap from 'gsap';
|
||||
|
||||
/**
|
||||
* Base animation effects
|
||||
* Each returns { from, to } objects for GSAP
|
||||
*/
|
||||
export const effects = {
|
||||
fadeIn: (element, config = {}) => ({
|
||||
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 };
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
152
src/assets/scripts/bundle/mix-nav-animations.js
Normal file
152
src/assets/scripts/bundle/mix-nav-animations.js
Normal file
|
|
@ -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 };
|
||||
352
src/pages/gsap-animations.md
Normal file
352
src/pages/gsap-animations.md
Normal file
|
|
@ -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
|
||||
|
||||
---
|
||||
|
||||
<p class="text-step-min-1"><a href="https://git.hypnagaga.com/wires/hypnagaga/src/branch/main/src/pages/gsap-animations.md">View this page's source code</a> to see exactly how each animation is configured.</p>
|
||||
123
src/posts/2025/testing/emotional-animations.md
Normal file
123
src/posts/2025/testing/emotional-animations.md
Normal file
|
|
@ -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 %}
|
||||
Loading…
Reference in a new issue