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

arc-motion

Skill7 starsChanged 10 months agoMITView on GitHub

Natural arc motion paths following gravity and momentum. Disney's principle that organic motion follows curved trajectories. Use for object movement, drag-release, toss gestures, and any physics-affected animation.

The file

Files
SKILL.md

Arc Motion Paths

Living things and physical objects move in arcs, not straight lines. Gravity, momentum, and inertia create curved trajectories that feel natural.

Core Principle

Objects in motion follow parabolic arcs due to gravity and momentum.

Linear Motion:  A ------------------> B  (robotic, mechanical)

Arc Motion:     A ~~~~~~~~~~~~~~~> B  (natural, organic)
                    ^
              apex of arc

Token Reference

Load from: tokens/motion-path.tokens.json

TokenPathUse Case
arc-subtleM0,0 Q25,10 50,0Small movements, icons
arc-mediumM0,0 Q50,30 100,0Standard travel, cards
arc-largeM0,0 Q75,50 150,0Dramatic motion, hero
arc-throwM0,0 Q100,80 200,20Toss, dismiss gestures
arc-dropM0,0 Q10,50 50,100Gravity drop
arc-riseM0,100 Q40,50 50,0Upward against gravity

When to Use Arcs vs Linear

Use Arc Motion For:

  • Physical objects - Cards, images, icons moving through space
  • Drag-and-drop release - Object thrown by user
  • Dismiss gestures - Swipe-away, toss-to-delete
  • Gravity-affected motion - Drops, rises, bounces
  • Organic/character animation - Limbs, eyes, living things

Use Linear Motion For:

  • UI feedback - Progress bars, loading indicators
  • State changes - Toggle switches, checkboxes
  • Direct manipulation - While user is actively dragging
  • Mechanical/robotic motion - Intentionally artificial

Implementation Presets

CSS offset-path

/* Native CSS Motion Path */
.arc-motion {
  offset-path: path('M0,0 Q50,30 100,0');
  animation: follow-arc 500ms var(--ease-standard) forwards;
}

@keyframes follow-arc {
  from { offset-distance: 0%; }
  to { offset-distance: 100%; }
}

/* Throw/dismiss arc */
.toss-dismiss {
  offset-path: path('M0,0 Q100,80 200,20');
  animation: toss 400ms cubic-bezier(0.2, 0, 0.4, 1) forwards;
}

@keyframes toss {
  from {
    offset-distance: 0%;
    opacity: 1;
  }
  to {
    offset-distance: 100%;
    opacity: 0;
  }
}

GSAP MotionPathPlugin

import { gsap } from 'gsap';
import { MotionPathPlugin } from 'gsap/MotionPathPlugin';
import tokens from '../tokens/motion-path.tokens.json';

gsap.registerPlugin(MotionPathPlugin);

// Basic arc motion
gsap.to('.element', {
  motionPath: {
    path: tokens['motion-path']['arc-medium'].$value,
    align: 'self',
    alignOrigin: [0.5, 0.5]
  },
  duration: 0.5,
  ease: 'power2.inOut'
});

// Throw gesture with physics
function throwElement(element, velocity, angle) {
  const radians = angle * (Math.PI / 180);
  const distance = velocity * 0.5; // Simplified physics

  // Calculate parabolic path points
  const endX = Math.cos(radians) * distance;
  const endY = Math.sin(radians) * distance + 100; // Gravity pulls down
  const apexX = endX * 0.4;
  const apexY = Math.min(Math.sin(radians) * distance * 0.6, -50); // Arc peak

  const path = `M0,0 Q${apexX},${apexY} ${endX},${endY}`;

  return gsap.to(element, {
    motionPath: { path, align: 'self' },
    duration: 0.6,
    ease: 'power1.out',
    opacity: 0
  });
}

// Gravity drop
function dropWithGravity(element, height = 200) {
  return gsap.to(element, {
    motionPath: {
      path: `M0,0 Q10,${height * 0.5} 50,${height}`,
      align: 'self'
    },
    duration: 0.4,
    ease: 'power2.in' // Accelerating (gravity)
  });
}

Framer Motion

import { motion, useAnimation } from 'framer-motion';

// Arc motion using keyframes
const arcVariants = {
  initial: { x: 0, y: 0 },

  arcMove: {
    x: [0, 25, 50, 75, 100],
    y: [0, -15, -30, -15, 0], // Parabolic y values
    transition: {
      duration: 0.5,
      ease: 'easeInOut',
      times: [0, 0.25, 0.5, 0.75, 1]
    }
  },

  throwAway: {
    x: [0, 50, 100, 150, 200],
    y: [0, -40, -60, -40, 20],
    opacity: [1, 1, 1, 0.5, 0],
    transition: {
      duration: 0.4,
      ease: [0.2, 0, 0.4, 1]
    }
  }
};

export const ArcMotionElement = () => (
  <motion.div
    variants={arcVariants}
    initial="initial"
    animate="arcMove"
  />
);

// Physics-based arc with useSpring
import { useSpring, animated } from '@react-spring/web';

export const PhysicsArc = ({ isThrown }) => {
  const props = useSpring({
    x: isThrown ? 200 : 0,
    y: isThrown ? 50 : 0,
    config: { tension: 200, friction: 20, mass: 1 }
  });

  // Add gravity effect to y
  const gravityY = useSpring({
    y: isThrown ? 100 : 0,
    config: { tension: 100, friction: 10 }
  });

  return (
    <animated.div
      style={{
        x: props.x,
        y: props.y.to((y) => y + gravityY.y.get())
      }}
    />
  );
};

Remotion

import { useCurrentFrame, interpolate } from 'remotion';
import tokens from '../tokens/motion-path.tokens.json';

interface ArcMotionProps {
  startFrame: number;
  endFrame: number;
  start: { x: number; y: number };
  end: { x: number; y: number };
  arcHeight?: number;
  children: React.ReactNode;
}

export const ArcMotion: React.FC<ArcMotionProps> = ({
  startFrame,
  endFrame,
  start,
  end,
  arcHeight = 50,
  children
}) => {
  const frame = useCurrentFrame();

  const progress = interpolate(
    frame,
    [startFrame, endFrame],
    [0, 1],
    { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
  );

  // Linear interpolation for X (horizontal motion)
  const x = interpolate(progress, [0, 1], [start.x, end.x]);

  // Parabolic arc for Y (gravity simulation)
  // y = start.y + (end.y - start.y) * progress - arcHeight * sin(progress * PI)
  const linearY = start.y + (end.y - start.y) * progress;
  const arcOffset = arcHeight * Math.sin(progress * Math.PI);
  const y = linearY - arcOffset; // Subtract because negative Y is up

  return (
    <div style={{ transform: `translate(${x}px, ${y}px)` }}>
      {children}
    </div>
  );
};

// Gravity-affected drop with acceleration
export const GravityDrop: React.FC<{
  startFrame: number;
  dropHeight: number;
  children: React.ReactNode;
}> = ({ startFrame, dropHeight, children }) => {
  const frame = useCurrentFrame();
  const gravity = tokens['gravity-simulation']['earth-gravity'].$value.acceleration;

  // Use accelerating easing to simulate gravity
  const progress = interpolate(
    frame,
    [startFrame, startFrame + 20], // ~0.67s at 30fps
    [0, 1],
    { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
  );

  // Quadratic easing (acceleration) for gravity
  const easedProgress = progress * progress;
  const y = easedProgress * dropHeight;

  return (
    <div style={{ transform: `translateY(${y}px)` }}>
      {children}
    </div>
  );
};

// Throw with velocity and angle
export const ThrowMotion: React.FC<{
  startFrame: number;
  velocity: number;
  angle: number; // degrees
  children: React.ReactNode;
}> = ({ startFrame, velocity, angle, children }) => {
  const frame = useCurrentFrame();
  const radians = (angle * Math.PI) / 180;
  const gravity = 980; // pixels/s^2
  const fps = 30;

  const t = (frame - startFrame) / fps; // time in seconds

  if (t < 0) {
    return <div>{children}</div>;
  }

  // Projectile motion equations
  const vx = velocity * Math.cos(radians);
  const vy = velocity * Math.sin(radians);

  const x = vx * t;
  const y = vy * t + 0.5 * gravity * t * t; // gravity pulls down

  return (
    <div style={{ transform: `translate(${x}px, ${y}px)` }}>
      {children}
    </div>
  );
};

Physics Reference

Parabolic Arc Formula

x(t) = x0 + vx * t
y(t) = y0 + vy * t + 0.5 * g * t^2

Where:
- x0, y0 = starting position
- vx, vy = initial velocity components
- g = gravity (9.8 m/s^2 or ~980 px/s^2)
- t = time

Simplified Quadratic Bezier

For a simple arc from (0,0) to (100,0) with height h:
Control point: (50, -h)
Path: M0,0 Q50,-h 100,0

Accessibility

const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

// Reduced motion: use linear fade instead of arc
const animation = prefersReducedMotion
  ? { opacity: [1, 0], x: [0, 100] }
  : { /* full arc path */ };

Common Mistakes

  1. Linear for physical objects - Real objects don't move in straight lines
  2. Arc for UI state changes - Toggles and checkmarks should be linear
  3. Wrong arc direction - Upward motion should fight gravity (decelerate at top)
  4. Ignoring momentum - Objects continue moving after release
  5. Same arc for all sizes - Larger distances need larger arcs

Related Skills

  • anticipation - Anticipation affects the starting trajectory of arc paths
  • follow-through - Objects continue along arc trajectory after primary action
  • squash-stretch - Objects stretch along their arc trajectory direction
  • exaggeration - Exaggeration affects arc height and trajectory drama
  • secondary-action - Secondary elements may follow their own arc trajectories

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.