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

TokenDurationScaleOffsetUse Case
micro50ms0.98-2pxToggles, micro-interactions
small80ms0.95-5pxButtons, icons
medium120ms0.90-10pxCards, modals
dramatic200ms0.85-20pxHero moments, reveals
extreme300ms0.75-40pxCartoon, gaming

Timing Ratios

Anticipation should be 15-25% of total animation duration:

Total DurationAnticipationMain Action
200ms30-50ms150-170ms
300ms45-75ms225-255ms
500ms75-125ms375-425ms

Direction Rules

Anticipation direction = opposite of main action

Main ActionAnticipation
Move rightPull left
Move upDip down
Scale upShrink first
Rotate clockwiseCounter-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

BrandAnticipation LevelCharacter
LuxurymicroElegant, restrained
Enterprisemicro-smallProfessional, efficient
Consumersmall-mediumFriendly, clear
Gamingmedium-dramaticEnergetic, impactful
Kidsdramatic-extremePlayful, 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

  1. Too long anticipation - Should be 15-25% of total, not 50%
  2. Wrong direction - Must be opposite to main action
  3. No settle - Motion feels abrupt without recovery phase
  4. Overuse - Not every action needs anticipation
  5. 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

دانلود ZIP