anticipation
با یک حرکت آمادهساز، انیمیشنها را طبیعیتر و هدفمندتر کنید.
این Skill اصل Anticipation از اصول انیمیشن دیزنی را برای آمادهسازی حرکت پیش از اجرای اصلی به کار میگیرد؛ مثل جمعشدن پیش از پرش یا کشیدهشدن دکمه در خلاف جهت حرکت. با این رویکرد، تعاملها، revealها و ترنزیشنها واضحتر و باورپذیرتر میشوند.
برای پروژههای CSS، GSAP، Framer Motion و Remotion، توکنها و الگوهای آماده در شدتهای مختلف ارائه میکند. همچنین نسبت زمانی مناسب، جهت حرکت، مرحلهی settle و پشتیبانی از prefers-reduced-motion را در نظر میگیرد.
چه چیزی میگیرید
- توکنهای آماده برای شدتهای `micro` تا `extreme` با duration، scale و offset مشخص
- الگوهای پیادهسازی برای CSS، GSAP، Framer Motion و Remotion
- راهنمای انتخاب جهت و نسبت زمانی Anticipation نسبت به حرکت اصلی
- نمونههای آماده برای button press، reveal، pop-in و jump animation
- نکات accessibility و جلوگیری از خطاهایی مثل حرکت طولانی یا استفادهی بیشازحد
برای چه کاری خوب است
- طراحان و توسعهدهندگان UI که میخواهند تعاملهای دکمه، کارت و modal طبیعیتر شوند
- ساخت revealهای نمایشی، hero animation و ترنزیشنهای دارای cue بصری
- پروژههای motion design با GSAP، Framer Motion، Remotion یا CSS
برای چه کاری نیست
- حرکتهای کاملاً فوری و بدون انیمیشن که تأخیر حتی چند میلیثانیهای در آنها نامطلوب است
- صفحههایی که بهدلیل محدودیت عملکرد یا accessibility باید فقط از fade ساده استفاده کنند
- زمانی که حرکت اصلی نیاز به نشانهی آمادهساز یا تغییر جهت پیش از اجرا ندارد
دربارهٔ این اسکیل
Anticipation
The preparatory movement that precedes the main action. Like a pitcher's wind-up or a jumper's crouch, anticipation signals intent and builds energy.
Core Principle
Movement in the opposite direction before the main action creates expectation and makes motion feel intentional.
Without anticipation: -------> (sudden, jarring)
With anticipation: <-- -------> (prepared, natural)
^
wind-up phase
Token Reference
Load from: tokens/anticipation.tokens.json
| Token | Duration | Scale | Offset | Use Case |
|---|---|---|---|---|
micro | 50ms | 0.98 | -2px | Toggles, micro-interactions |
small | 80ms | 0.95 | -5px | Buttons, icons |
medium | 120ms | 0.90 | -10px | Cards, modals |
dramatic | 200ms | 0.85 | -20px | Hero moments, reveals |
extreme | 300ms | 0.75 | -40px | Cartoon, gaming |
Timing Ratios
Anticipation should be 15-25% of total animation duration:
| Total Duration | Anticipation | Main Action |
|---|---|---|
| 200ms | 30-50ms | 150-170ms |
| 300ms | 45-75ms | 225-255ms |
| 500ms | 75-125ms | 375-425ms |
Direction Rules
Anticipation direction = opposite of main action
| Main Action | Anticipation |
|---|---|
| Move right | Pull left |
| Move up | Dip down |
| Scale up | Shrink first |
| Rotate clockwise | Counter-rotate |
Implementation Presets
CSS
/* Button with anticipation */
.button-anticipate {
transition: transform 200ms var(--ease-standard);
}
.button-anticipate:active {
animation: anticipate-press 200ms ease-out;
}
@keyframes anticipate-press {
0% { transform: scale(1); }
30% { transform: scale(0.95) translateY(2px); } /* Wind-up */
100% { transform: scale(1.05); } /* Action */
}
/* Slide-in with anticipation */
@keyframes slide-in-anticipate {
0% {
transform: translateX(-100%);
opacity: 0;
}
15% {
transform: translateX(-110%); /* Pull back further */
opacity: 0.5;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
/* Pop-in with scale anticipation */
@keyframes pop-in-anticipate {
0% {
transform: scale(0);
opacity: 0;
}
20% {
transform: scale(0.85); /* Compress before pop */
opacity: 0.8;
}
50% {
transform: scale(1.1); /* Overshoot */
opacity: 1;
}
100% {
transform: scale(1);
}
}
GSAP
import { gsap } from 'gsap';
import tokens from '../tokens/anticipation.tokens.json';
// Anticipation utility function
function withAnticipation(element, mainAction, intensity = 'small') {
const { duration, scale, offsetY } = tokens.anticipation[intensity].$value;
const anticipationMs = parseInt(duration);
const tl = gsap.timeline();
// Anticipation phase (opposite direction)
tl.to(element, {
scale: scale,
y: -parseInt(offsetY), // Negative because offsetY is already negative
duration: anticipationMs / 1000,
ease: 'power2.out'
});
// Main action
tl.add(mainAction);
return tl;
}
// Button press with anticipation
function pressButton(button) {
return withAnticipation(
button,
gsap.to(button, {
scale: 1.05,
y: 0,
duration: 0.15,
ease: 'back.out(2)'
}),
'small'
);
}
// Dramatic reveal with anticipation
function revealElement(element) {
const { scale, offsetY } = tokens.anticipation.dramatic.$value;
return gsap.timeline()
// Start hidden
.set(element, { opacity: 0, scale: 0.5, y: 50 })
// Anticipation - pull back
.to(element, {
opacity: 0.7,
scale: scale,
y: 50 - parseInt(offsetY),
duration: 0.2,
ease: 'power1.out'
})
// Main reveal - burst forward
.to(element, {
opacity: 1,
scale: 1.1,
y: -20,
duration: 0.3,
ease: 'back.out(1.7)'
})
// Settle
.to(element, {
scale: 1,
y: 0,
duration: 0.2,
ease: 'power2.out'
});
}
// Jump animation with crouch anticipation
function jumpAnimation(element, height = 100) {
return gsap.timeline()
// Crouch (anticipation)
.to(element, {
scaleY: 0.8,
scaleX: 1.1,
y: 10,
duration: 0.15,
ease: 'power2.out'
})
// Jump up (main action)
.to(element, {
scaleY: 1.2,
scaleX: 0.9,
y: -height,
duration: 0.25,
ease: 'power2.out'
})
// Fall down
.to(element, {
y: 0,
duration: 0.2,
ease: 'power2.in'
})
// Land squash
.to(element, {
scaleY: 0.85,
scaleX: 1.1,
duration: 0.1,
ease: 'power3.out'
})
// Recover
.to(element, {
scaleY: 1,
scaleX: 1,
duration: 0.15,
ease: 'elastic.out(1, 0.5)'
});
}
Framer Motion
import { motion, useAnimation } from 'framer-motion';
import tokens from '../tokens/anticipation.tokens.json';
// Anticipation variants
const anticipationVariants = {
initial: { scale: 1, y: 0 },
anticipate: (intensity: string = 'small') => {
const { scale, offsetY } = tokens.anticipation[intensity].$value;
return {
scale: scale,
y: parseInt(offsetY),
transition: { duration: 0.08, ease: 'easeOut' }
};
},
action: {
scale: 1.05,
y: 0,
transition: {
type: 'spring',
stiffness: 400,
damping: 15
}
},
rest: {
scale: 1,
y: 0,
transition: { duration: 0.15 }
}
};
export const AnticipatedButton: React.FC<{ children: React.ReactNode }> = ({
children
}) => {
const controls = useAnimation();
const handlePress = async () => {
await controls.start('anticipate');
await controls.start('action');
await controls.start('rest');
};
return (
<motion.button
variants={anticipationVariants}
animate={controls}
onTapStart={handlePress}
custom="small"
>
{children}
</motion.button>
);
};
// Pop-in with anticipation
const popInVariants = {
hidden: {
scale: 0,
opacity: 0
},
anticipate: {
scale: 0.85,
opacity: 0.8,
transition: { duration: 0.1 }
},
visible: {
scale: [0.85, 1.1, 1],
opacity: 1,
transition: {
scale: {
times: [0, 0.6, 1],
duration: 0.3,
ease: 'easeOut'
}
}
}
};
export const PopInElement: React.FC<{ children: React.ReactNode }> = ({
children
}) => (
<motion.div
variants={popInVariants}
initial="hidden"
animate={['anticipate', 'visible']}
>
{children}
</motion.div>
);
Remotion
import { useCurrentFrame, interpolate, spring } from 'remotion';
import tokens from '../tokens/anticipation.tokens.json';
interface AnticipationProps {
actionFrame: number;
anticipationFrames?: number;
intensity: 'micro' | 'small' | 'medium' | 'dramatic';
children: React.ReactNode;
}
export const WithAnticipation: React.FC<AnticipationProps> = ({
actionFrame,
anticipationFrames = 6,
intensity = 'small',
children
}) => {
const frame = useCurrentFrame();
const { scale: targetScale, offsetY } = tokens.anticipation[intensity].$value;
const offset = parseInt(offsetY);
const anticipationStart = actionFrame - anticipationFrames;
// Anticipation phase: ease into wind-up position
const anticipationProgress = interpolate(
frame,
[anticipationStart, actionFrame - 1],
[0, 1],
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
);
// Main action phase: spring back
const actionProgress = spring({
frame: frame - actionFrame,
fps: 30,
config: { damping: 12, stiffness: 200, mass: 1 }
});
// Calculate current values
let scale: number;
let y: number;
if (frame < actionFrame) {
// During anticipation
scale = interpolate(anticipationProgress, [0, 0.7, 1], [1, targetScale, targetScale]);
y = interpolate(anticipationProgress, [0, 0.7, 1], [0, offset, offset]);
} else {
// During/after action
scale = interpolate(actionProgress, [0, 1], [targetScale, 1]);
y = interpolate(actionProgress, [0, 1], [offset, 0]);
}
return (
<div style={{
transform: `scale(${scale}) translateY(${y}px)`,
transformOrigin: 'center bottom'
}}>
{children}
</div>
);
};
// Jump anticipation (crouch before jump)
export const JumpAnticipation: React.FC<{
jumpFrame: number;
jumpHeight: number;
children: React.ReactNode;
}> = ({ jumpFrame, jumpHeight, children }) => {
const frame = useCurrentFrame();
const crouchFrames = 5;
const crouchStart = jumpFrame - crouchFrames;
// Crouch phase
const crouchProgress = interpolate(
frame,
[crouchStart, jumpFrame],
[0, 1],
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
);
// Jump phase
const jumpDuration = 15; // frames
const jumpProgress = interpolate(
frame,
[jumpFrame, jumpFrame + jumpDuration / 2, jumpFrame + jumpDuration],
[0, 1, 0],
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
);
const scaleY = frame < jumpFrame
? interpolate(crouchProgress, [0, 1], [1, 0.8])
: interpolate(jumpProgress, [0, 0.5, 1], [0.8, 1.15, 1]);
const y = frame < jumpFrame
? interpolate(crouchProgress, [0, 1], [0, 10])
: interpolate(jumpProgress, [0, 0.5, 1], [10, -jumpHeight, 0]);
return (
<div style={{
transform: `scaleY(${scaleY}) scaleX(${1/scaleY}) translateY(${y}px)`,
transformOrigin: 'center bottom'
}}>
{children}
</div>
);
};
Brand Attitude Mapping
| Brand | Anticipation Level | Character |
|---|---|---|
| Luxury | micro | Elegant, restrained |
| Enterprise | micro-small | Professional, efficient |
| Consumer | small-medium | Friendly, clear |
| Gaming | medium-dramatic | Energetic, impactful |
| Kids | dramatic-extreme | Playful, exaggerated |
Accessibility
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// Skip anticipation for reduced motion
const animation = prefersReducedMotion
? { opacity: [0, 1] } // Direct fade
: { /* full anticipation sequence */ };
Common Mistakes
- Too long anticipation - Should be 15-25% of total, not 50%
- Wrong direction - Must be opposite to main action
- No settle - Motion feels abrupt without recovery phase
- Overuse - Not every action needs anticipation
- Same intensity everywhere - Match to brand and context
Related Skills
- squash-stretch - Anticipation often includes a slight squash before the main action
- follow-through - The settling phase after main action completes the anticipation cycle
- exaggeration - Determines intensity of the anticipation wind-up
- arc-motion - Anticipation affects the starting trajectory of arc paths
- secondary-action - Supporting elements may have their own micro-anticipations