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
| Token | Path | Use Case |
|---|---|---|
arc-subtle | M0,0 Q25,10 50,0 | Small movements, icons |
arc-medium | M0,0 Q50,30 100,0 | Standard travel, cards |
arc-large | M0,0 Q75,50 150,0 | Dramatic motion, hero |
arc-throw | M0,0 Q100,80 200,20 | Toss, dismiss gestures |
arc-drop | M0,0 Q10,50 50,100 | Gravity drop |
arc-rise | M0,100 Q40,50 50,0 | Upward 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
- Linear for physical objects - Real objects don't move in straight lines
- Arc for UI state changes - Toggles and checkmarks should be linear
- Wrong arc direction - Upward motion should fight gravity (decelerate at top)
- Ignoring momentum - Objects continue moving after release
- 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