agentleFS
soilmasssoilmass/motion-design-agent/skills/anticipation/SKILL.md

anticipation

Skill7 starsChanged 10 months agoMITView on GitHub

Disney's principle of preparation before action - the wind-up that makes motion feel intentional. Use when elements need clear directional cues, dramatic reveals, or physically believable movement.

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 3 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Too long anticipation: - Should be 15-25% of total, not 50%

    line 455
  2. Distinctive:

    Wrong direction: - Must be opposite to main action

    line 456
  3. Distinctive:

    Overuse: - Not every action needs anticipation

    line 458

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.