arc-motion

حرکت‌های طبیعی و فیزیکی را با مسیرهای کمانی و شبیه‌سازی گرانش طراحی کنید.

این مهارت برای طراحی حرکت‌های کمانی طبیعی بر پایه‌ی گرانش، مومنتوم و اینرسی است. از آن برای جابه‌جایی کارت‌ها و آیکون‌ها، رهاسازی Drag-and-drop، پرتاب، حذف با Swipe و حرکت‌های فیزیکی استفاده کنید.

مسیرها و الگوهای آماده را برای CSS offset-path، GSAP، Framer Motion و Remotion دریافت می‌کنید و می‌توانید شدت قوس، سرعت و جهت حرکت را با رفتار فیزیکی هماهنگ کنید. راهنمایی‌های دسترس‌پذیری برای prefers-reduced-motion نیز در نظر گرفته شده است.

چه چیزی می‌گیرید

  • توکن‌های آماده برای قوس‌های ظریف، متوسط، بزرگ، پرتاب، سقوط و صعود
  • نمونه‌کد CSS offset-path و GSAP MotionPathPlugin
  • الگوهای پیاده‌سازی برای Framer Motion، react-spring و Remotion
  • فرمول‌های حرکت سهموی، گرانش، سرعت و زاویه‌ی پرتاب
  • راهنمای انتخاب حرکت کمانی یا خطی و رعایت prefers-reduced-motion

برای چه کاری خوب است

  • طراحی تعامل‌های فیزیکی مانند Drag-and-drop، پرتاب و Swipe برای حذف
  • ساخت انیمیشن طبیعی برای کارت‌ها، تصاویر، آیکون‌ها و عناصر رابط کاربری
  • تولید ویدیوهای متحرک در Remotion با مسیرهای قابل‌کنترل و فریم‌محور

برای چه کاری نیست

  • تغییر وضعیت‌های ساده مانند Toggle، Checkbox و Progress bar
  • حرکت‌های عمداً مکانیکی، رباتیک یا تعامل مستقیم هنگام Drag کردن

دربارهٔ این اسکیل

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

دانلود ZIP