gesture-motion

طراحی تعامل‌های لمسی و اشاره‌ای طبیعی با فیزیک، اینرسی و نقاط چسبش

این Skill الگوهای فیزیک‌محور برای تعامل‌های مستقیم کاربر را طراحی می‌کند؛ از drag و swipe تا pull-to-refresh، pinch-zoom و rubber-banding. حرکت عنصر هنگام لمس دقیقاً با pointer هماهنگ می‌شود و پس از رهاکردن، با momentum، friction و snap point ادامه پیدا می‌کند.

برای ساخت رابط‌هایی طبیعی و قابل‌اعتماد مفید است و منطق تشخیص سرعت، آستانه‌ها، مقاومت مرزی و بازگشت فنری را در اختیار می‌گذارد. همچنین به accessibility توجه دارد و برای gestureها جایگزین‌های صفحه‌کلیدی و اعلان‌های مناسب پیشنهاد می‌کند.

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

  • الگوهای آماده برای drag، swipe، pull-to-refresh و pinch-zoom
  • محاسبه velocity، اعمال momentum و انتخاب snap point بر اساس سرعت حرکت
  • مقاومت rubber-band در مرزها و بازگشت طبیعی با spring physics
  • نمونه‌های پیاده‌سازی برای CSS، GSAP، Framer Motion و Remotion
  • راهنمای تعیین threshold و افزودن جایگزین‌های keyboard و aria-live

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

  • توسعه‌دهندگان رابط‌های موبایل، لمسی یا pointer-based
  • ساخت کارت‌های قابل‌کشیدن، swipe action، carousel و bottom sheet
  • طراحی تعامل‌هایی که باید حس فیزیکی، اینرسی و snap طبیعی داشته باشند

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

  • انیمیشن‌های صرفاً تزئینی که به تعامل کاربر وابسته نیستند
  • پروژه‌هایی که به افکت‌های ساده CSS بدون منطق gesture یا physics نیاز دارند
  • رابط‌هایی که هیچ ورودی لمسی، pointer یا drag ندارند

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

Gesture Motion Patterns

Motion that responds to user gestures with physics-based feedback. Direct manipulation should feel connected to the finger/pointer.

Core Principles

  1. 1:1 tracking during manipulation - Element follows pointer exactly
  2. Physics on release - Momentum, friction, snap points
  3. Resistance at boundaries - Rubber-banding effect
  4. Clear thresholds - When gesture triggers action

Pattern Categories

PatternGesturePhysics
DragPointer down + moveDirect tracking, momentum on release
SwipeQuick directional moveVelocity-based, snap points
Pull-to-refreshPull down past thresholdResistance, spring back
Pinch-zoomTwo-finger pinchScale with damping
Rubber-bandDrag past boundaryExponential resistance

Drag Physics

Velocity Calculation

// Track velocity over last N samples
const velocityTracker = {
  samples: [],
  maxSamples: 5,

  addSample(position, timestamp) {
    this.samples.push({ position, timestamp });
    if (this.samples.length > this.maxSamples) {
      this.samples.shift();
    }
  },

  getVelocity() {
    if (this.samples.length < 2) return { x: 0, y: 0 };

    const first = this.samples[0];
    const last = this.samples[this.samples.length - 1];
    const dt = (last.timestamp - first.timestamp) / 1000;

    return {
      x: (last.position.x - first.position.x) / dt,
      y: (last.position.y - first.position.y) / dt
    };
  }
};

Momentum Decay

function applyMomentum(element, velocity, friction = 0.95) {
  let vx = velocity.x;
  let vy = velocity.y;

  function animate() {
    vx *= friction;
    vy *= friction;

    // Continue until velocity is negligible
    if (Math.abs(vx) > 0.5 || Math.abs(vy) > 0.5) {
      requestAnimationFrame(animate);
    }
  }

  animate();
}

Swipe Patterns

Swipe Detection

const swipeConfig = {
  threshold: 50,          // Minimum distance
  velocityThreshold: 0.5, // Minimum velocity (px/ms)
  timeThreshold: 300      // Maximum time (ms)
};

function detectSwipe(start, end, duration) {
  const dx = end.x - start.x;
  const dy = end.y - start.y;
  const distance = Math.sqrt(dx * dx + dy * dy);
  const velocity = distance / duration;

  if (distance < swipeConfig.threshold ||
      velocity < swipeConfig.velocityThreshold) {
    return null;
  }

  const angle = Math.atan2(dy, dx) * 180 / Math.PI;

  if (angle >= -45 && angle < 45) return 'right';
  if (angle >= 45 && angle < 135) return 'down';
  if (angle >= -135 && angle < -45) return 'up';
  return 'left';
}

Snap Points

function snapToPoint(current, snapPoints, velocity) {
  let target;

  if (Math.abs(velocity) > 100) {
    // High velocity: snap to next point in direction
    target = velocity > 0
      ? snapPoints.find(p => p > current) || snapPoints[snapPoints.length - 1]
      : [...snapPoints].reverse().find(p => p < current) || snapPoints[0];
  } else {
    // Low velocity: snap to nearest
    target = snapPoints.reduce((nearest, point) =>
      Math.abs(point - current) < Math.abs(nearest - current) ? point : nearest
    );
  }

  return target;
}

Rubber Banding

// Exponential resistance past boundary
function rubberBand(offset, maxOffset, elasticity = 0.55) {
  if (Math.abs(offset) <= maxOffset) return offset;

  const direction = offset > 0 ? 1 : -1;
  const excess = Math.abs(offset) - maxOffset;

  // Logarithmic falloff
  const rubberBanded = maxOffset +
    (1 - (1 / ((excess * elasticity / maxOffset) + 1))) * maxOffset;

  return direction * rubberBanded;
}

Implementation Presets

CSS Snap Points

/* Horizontal snap scroll */
.snap-container {
  scroll-snap-type: x mandatory;
  overflow-x: auto;
  display: flex;
}

.snap-item {
  scroll-snap-align: center;
  flex-shrink: 0;
  width: 80%;
}

Framer Motion Draggable

// Draggable card with snap back
<motion.div
  drag="x"
  dragConstraints={{ left: 0, right: 0 }}
  dragElastic={0.2}
  onDragEnd={(e, info) => {
    if (Math.abs(info.velocity.x) > 500 || Math.abs(info.offset.x) > 150) {
      // Dismiss
    } else {
      // Snap back
    }
  }}
/>

See references/gesture-implementations.md for complete GSAP, Framer Motion, and Remotion examples.

Accessibility

// Provide keyboard alternatives for gestures
const handleKeyDown = (e) => {
  switch (e.key) {
    case 'ArrowLeft': navigatePrev(); break;
    case 'ArrowRight': navigateNext(); break;
    case 'Delete': dismissItem(); break;
  }
};

// Announce gesture actions
<div aria-live="polite">
  {isDragging && 'Dragging item. Release to drop.'}
  {isOverThreshold && 'Release to dismiss.'}
</div>

Common Mistakes

  1. Not tracking velocity - Momentum needs velocity, not just position
  2. Hard boundaries - Use rubber-banding for natural feel
  3. Same physics for all gestures - Light swipe != heavy drag
  4. Ignoring inertia - Objects don't stop instantly
  5. No snap affordance - Visual hint where items will snap

Related Skills

  • animation-recipes - Pre-built gesture animation recipes
  • accessibility - Keyboard alternatives for gestures
  • easing - Spring physics for gesture animations

دانلود ZIP