Motion لـ React: بناء رسوم متحركة وإيماءات وانتقالات بمستوى إنتاجي

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

مقدمة

تحوّل الرسوم المتحركة واجهة المستخدم الجيدة إلى واجهة رائعة. فهي توجه المستخدمين عبر تغييرات الحالة، وتقدم ملاحظات على التفاعلات، وتجعل التطبيقات تبدو حية. Motion (المعروفة سابقاً بـ Framer Motion) هي أشهر مكتبة رسوم متحركة لـ React، تستخدمها شركات مثل Vercel وLinear وStripe لإنشاء واجهات سلسة وعالية الأداء.

في هذا الدليل، ستتعلم Motion من الصفر. بدءاً من تأثيرات الظهور البسيطة، ستتقدم عبر الإيماءات ورسوم التخطيط المتحركة وتأثيرات التمرير والتسلسلات المعقدة المنسقة. في النهاية، ستكون قد بنيت معرض بطاقات منتجات متحرك بالكامل مع تأثيرات تحوم تفاعلية وانتقالات تخطيط سلسة وكشف عند التمرير.

ما ستتعلمه

  • تثبيت وتهيئة Motion في مشروع Next.js أو React
  • تحريك المكونات باستخدام مكون motion وخاصية animate
  • إنشاء إيماءات تفاعلية: التحوم والنقر والسحب والتركيز
  • بناء رسوم تخطيط متحركة تنتقل بسلاسة بين الحالات
  • استخدام AnimatePresence لرسوم الدخول والخروج
  • تنفيذ رسوم متحركة مدفوعة بالتمرير باستخدام useScroll وuseTransform
  • تنسيق تسلسلات معقدة مع المتغيرات وتأثيرات التتابع
  • تحسين أداء الرسوم المتحركة للإنتاج

المتطلبات الأساسية

قبل البدء، تأكد من أن لديك:

  • Node.js 18+ مثبت على جهازك
  • معرفة أساسية بـ React وTypeScript
  • إلمام بـ تحولات وانتقالات CSS (مفيد لكن غير مطلوب)
  • محرر أكواد (يُنصح بـ VS Code)

الخطوة 1: إعداد المشروع

أنشئ مشروع Next.js جديد وثبّت Motion:

npx create-next-app@latest motion-demo --typescript --tailwind --app --src-dir
cd motion-demo

ثبّت مكتبة Motion:

npm install motion

Motion الإصدار 11 وما بعده يُنشر تحت اسم حزمة motion (حزمة framer-motion لا تزال تعمل لكنها تُعيد التوجيه إلى motion). واجهة البرمجة نفسها — فقط اسم الحزمة تغيّر.

تحقق من التثبيت بفحص package.json:

{
  "dependencies": {
    "motion": "^11.18.0",
    "next": "^15.2.0",
    "react": "^19.0.0"
  }
}

الخطوة 2: أول رسم متحرك لك

استبدل محتويات src/app/page.tsx بمكون متحرك بسيط:

"use client";
 
import { motion } from "motion/react";
 
export default function Home() {
  return (
    <main className="flex min-h-screen items-center justify-center bg-gray-950">
      <motion.div
        initial={{ opacity: 0, y: 40 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.6, ease: "easeOut" }}
        className="rounded-2xl bg-white p-8 shadow-2xl"
      >
        <h1 className="text-2xl font-bold text-gray-900">
          مرحباً، Motion!
        </h1>
        <p className="mt-2 text-gray-600">
          هذه البطاقة ظهرت تدريجياً وانزلقت للأعلى بسلاسة.
        </p>
      </motion.div>
    </main>
  );
}

شغّل خادم التطوير بـ npm run dev وافتح http://localhost:3000. سترى البطاقة تظهر تدريجياً وتنزلق للأعلى عند تحميل الصفحة.

كيف يعمل

  • motion.div هو بديل مباشر لـ <div> يدعم خصائص الرسوم المتحركة
  • initial تحدد حالة البداية (غير مرئي، منزاح 40 بكسل للأسفل)
  • animate تحدد الحالة المستهدفة (مرئي بالكامل، الموضع الأصلي)
  • transition تتحكم في التوقيت والتخفيف

يقوم Motion تلقائياً بالاستيفاء بين قيم initial وanimate، ويتعامل مع كل منطق requestAnimationFrame والتحولات المسرّعة بالـ GPU نيابةً عنك.

الخطوة 3: فيزياء النوابض وأنواع الانتقالات

يدعم Motion ثلاثة أنواع من الانتقالات: tween (مبني على المدة)، وspring (مبني على الفيزياء)، وinertia (مبني على الزخم). رسوم النوابض المتحركة تبدو الأكثر طبيعية لتفاعلات الواجهة.

"use client";
 
import { motion } from "motion/react";
 
export default function SpringDemo() {
  return (
    <div className="flex min-h-screen items-center justify-center gap-8 bg-gray-950">
      {/* Tween: قابل للتنبؤ، مبني على المدة */}
      <motion.div
        initial={{ scale: 0 }}
        animate={{ scale: 1 }}
        transition={{ type: "tween", duration: 0.5 }}
        className="h-24 w-24 rounded-2xl bg-blue-500"
      />
 
      {/* Spring: طبيعي، مبني على الفيزياء */}
      <motion.div
        initial={{ scale: 0 }}
        animate={{ scale: 1 }}
        transition={{ type: "spring", stiffness: 260, damping: 20 }}
        className="h-24 w-24 rounded-2xl bg-green-500"
      />
 
      {/* Spring مع ارتداد */}
      <motion.div
        initial={{ scale: 0 }}
        animate={{ scale: 1 }}
        transition={{ type: "spring", bounce: 0.5 }}
        className="h-24 w-24 rounded-2xl bg-purple-500"
      />
    </div>
  );
}

معاملات النوابض

المعاملالوصفالقيمة الافتراضية
stiffnessمدى صلابة النابض (أعلى = أسرع)100
dampingمقدار المقاومة (أعلى = تذبذب أقل)10
massوزن العنصر المتحرك1
bounceاختصار: 0 = بدون ارتداد، 1 = ارتداد كبير0.25

نصيحة: لمعظم رسوم الواجهة المتحركة، type: "spring" بالقيم الافتراضية يعمل بشكل مثالي. عدّل المعاملات فقط عندما لا يتطابق الإحساس الافتراضي مع نية التصميم.

الخطوة 4: الإيماءات التفاعلية

يجعل Motion من السهل جداً إضافة رسوم متحركة للتحوم والنقر والسحب والتركيز. هذه الخصائص تستجيب لتفاعلات المستخدم بدون أي إدارة حالة.

التحوم والنقر

"use client";
 
import { motion } from "motion/react";
 
export default function GestureDemo() {
  return (
    <div className="flex min-h-screen items-center justify-center gap-8 bg-gray-950">
      <motion.button
        whileHover={{ scale: 1.05, backgroundColor: "#3b82f6" }}
        whileTap={{ scale: 0.95 }}
        transition={{ type: "spring", stiffness: 400, damping: 17 }}
        className="rounded-xl bg-blue-600 px-8 py-4 text-lg font-semibold text-white"
      >
        اضغط هنا
      </motion.button>
 
      <motion.div
        whileHover={{
          rotate: 5,
          boxShadow: "0 25px 50px -12px rgba(0,0,0,0.5)",
        }}
        whileTap={{ rotate: -5, scale: 0.9 }}
        className="flex h-32 w-32 cursor-pointer items-center justify-center rounded-2xl bg-gradient-to-br from-pink-500 to-orange-400 text-white font-bold"
      >
        تفاعل
      </motion.div>
    </div>
  );
}

السحب

يدعم Motion السحب الكامل مع القيود والحدود المرنة والزخم:

"use client";
 
import { motion } from "motion/react";
import { useRef } from "react";
 
export default function DragDemo() {
  const constraintsRef = useRef<HTMLDivElement>(null);
 
  return (
    <div className="flex min-h-screen items-center justify-center bg-gray-950">
      {/* حاوية حدود السحب */}
      <motion.div
        ref={constraintsRef}
        className="relative flex h-96 w-96 items-center justify-center rounded-3xl border-2 border-dashed border-gray-700"
      >
        <p className="absolute top-4 text-sm text-gray-500">
          اسحب الكرة
        </p>
 
        <motion.div
          drag
          dragConstraints={constraintsRef}
          dragElastic={0.2}
          dragTransition={{ bounceStiffness: 300, bounceDamping: 20 }}
          whileDrag={{ scale: 1.2, cursor: "grabbing" }}
          className="h-20 w-20 cursor-grab rounded-full bg-gradient-to-br from-violet-500 to-cyan-400 shadow-lg"
        />
      </motion.div>
    </div>
  );
}

خصائص السحب الرئيسية:

  • drag تمكّن السحب (true أو "x" أو "y" لقفل المحور)
  • dragConstraints تحدّ من الحركة (مرجع أو قيم بكسل)
  • dragElastic تتحكم في مدى السحب خارج القيود (0-1)
  • whileDrag تطبّق أنماط أثناء السحب

الخطوة 5: المتغيرات والتنسيق

عندما تحتاج لتحريك عناصر أبناء متعددة بالتتابع، تتيح لك المتغيرات تعريف حالات رسوم متحركة مسماة ونشرها عبر شجرة المكونات.

"use client";
 
import { motion } from "motion/react";
 
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,
      delayChildren: 0.2,
    },
  },
};
 
const itemVariants = {
  hidden: { opacity: 0, y: 20, scale: 0.95 },
  visible: {
    opacity: 1,
    y: 0,
    scale: 1,
    transition: { type: "spring", stiffness: 300, damping: 24 },
  },
};
 
const features = [
  { title: "سريع", description: "رسوم متحركة مسرّعة بالـ GPU بمعدل 60 إطار/ثانية" },
  { title: "بسيط", description: "واجهة برمجة تصريحية بدون كود معياري" },
  { title: "قوي", description: "دعم الإيماءات والتخطيط والتمرير" },
  { title: "صغير", description: "قابل للتقليم، يشحن فقط ما تستخدمه" },
];
 
export default function StaggerDemo() {
  return (
    <div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
      <motion.div
        variants={containerVariants}
        initial="hidden"
        animate="visible"
        className="grid max-w-2xl grid-cols-2 gap-4"
      >
        {features.map((feature) => (
          <motion.div
            key={feature.title}
            variants={itemVariants}
            className="rounded-xl bg-gray-900 p-6 border border-gray-800"
          >
            <h3 className="text-lg font-bold text-white">{feature.title}</h3>
            <p className="mt-1 text-sm text-gray-400">{feature.description}</p>
          </motion.div>
        ))}
      </motion.div>
    </div>
  );
}

كيف تنتشر المتغيرات

  1. الأب motion.div يحدد initial="hidden" وanimate="visible"
  2. الأبناء الذين لديهم variants يرثون هذه الحالات تلقائياً
  3. staggerChildren: 0.1 يضيف تأخير 100 مللي ثانية بين كل رسم متحرك للأبناء
  4. delayChildren: 0.2 ينتظر 200 مللي ثانية قبل بدء أول ابن

هذا النمط مثالي لتحريك القوائم والشبكات وقوائم التنقل وأي مجموعة من العناصر المرتبطة.

الخطوة 6: AnimatePresence لرسوم الخروج

يزيل React العناصر من DOM فوراً. يعترض AnimatePresence هذا ويسمح لرسوم الخروج بالاكتمال قبل الإزالة.

"use client";
 
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
 
const notifications = [
  { id: 1, text: "رسالة جديدة وصلت", color: "bg-blue-500" },
  { id: 2, text: "تم رفع الملف بنجاح", color: "bg-green-500" },
  { id: 3, text: "تمت معالجة الدفع", color: "bg-purple-500" },
  { id: 4, text: "اكتمل النشر", color: "bg-orange-500" },
];
 
export default function ExitDemo() {
  const [items, setItems] = useState(notifications);
 
  const removeItem = (id: number) => {
    setItems((prev) => prev.filter((item) => item.id !== id));
  };
 
  const resetItems = () => setItems(notifications);
 
  return (
    <div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
      <div className="w-full max-w-md space-y-3">
        <AnimatePresence mode="popLayout">
          {items.map((item) => (
            <motion.div
              key={item.id}
              layout
              initial={{ opacity: 0, x: -40, scale: 0.95 }}
              animate={{ opacity: 1, x: 0, scale: 1 }}
              exit={{ opacity: 0, x: 40, scale: 0.95 }}
              transition={{ type: "spring", stiffness: 350, damping: 25 }}
              className="flex items-center justify-between rounded-xl bg-gray-900 p-4 border border-gray-800"
            >
              <div className="flex items-center gap-3">
                <div className={`h-3 w-3 rounded-full ${item.color}`} />
                <span className="text-white">{item.text}</span>
              </div>
              <button
                onClick={() => removeItem(item.id)}
                className="text-gray-500 hover:text-red-400 transition-colors"
              >
                تجاهل
              </button>
            </motion.div>
          ))}
        </AnimatePresence>
 
        {items.length === 0 && (
          <motion.button
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            onClick={resetItems}
            className="w-full rounded-xl bg-blue-600 py-3 text-white font-semibold"
          >
            إعادة تعيين الإشعارات
          </motion.button>
        )}
      </div>
    </div>
  );
}

أوضاع AnimatePresence

الوضعالسلوك
"sync" (افتراضي)رسوم الخروج والدخول تحدث معاً
"wait"ينتظر اكتمال الخروج قبل الدخول
"popLayout"العناصر الخارجة تُزال من تدفق التخطيط

خاصية layout على كل عنصر تضمن أن العناصر المتبقية تعيد تموضعها بسلاسة عند إزالة عنصر شقيق.

الخطوة 7: رسوم التخطيط المتحركة

رسوم التخطيط المتحركة هي واحدة من أقوى ميزات Motion. بإضافة خاصية layout، تنتقل العناصر بسلاسة بين تخطيطات CSS المختلفة — بدون حسابات إحداثيات يدوية.

"use client";
 
import { useState } from "react";
import { motion } from "motion/react";
 
export default function LayoutDemo() {
  const [selected, setSelected] = useState<string | null>(null);
 
  const items = ["تصميم", "تطوير", "نشر", "مراقبة"];
 
  return (
    <div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
      <div className="flex gap-2 rounded-2xl bg-gray-900 p-2">
        {items.map((item) => (
          <button
            key={item}
            onClick={() => setSelected(item)}
            className="relative rounded-xl px-6 py-3 text-sm font-medium text-white outline-none"
          >
            {selected === item && (
              <motion.div
                layoutId="active-tab"
                className="absolute inset-0 rounded-xl bg-blue-600"
                transition={{ type: "spring", stiffness: 380, damping: 30 }}
              />
            )}
            <span className="relative z-10">{item}</span>
          </button>
        ))}
      </div>
    </div>
  );
}

سحر layoutId

عندما تعطي عناصر متعددة نفس layoutId، يعاملها Motion كعنصر واحد عبر عمليات العرض. عندما يُزال أحدها ويُضاف آخر، يحرّك Motion بين مواقعها وأحجامها تلقائياً. هذه هي الطريقة لإنشاء:

  • انتقالات عناصر مشتركة (مثل مؤشر التبويب أعلاه)
  • توسيع من بطاقة إلى نافذة
  • رسوم إعادة ترتيب القوائم

مثال توسيع البطاقة

"use client";
 
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
 
const cards = [
  { id: "1", title: "التحليلات", description: "تتبع سلوك المستخدم ومقاييس التفاعل عبر منصتك.", color: "from-blue-500 to-cyan-400" },
  { id: "2", title: "الأمان", description: "تشفير شامل وبنية تحتية متوافقة مع معايير الامتثال.", color: "from-purple-500 to-pink-400" },
  { id: "3", title: "السرعة", description: "منشور على الحافة عالمياً مع أوقات استجابة أقل من 100 مللي ثانية.", color: "from-orange-500 to-yellow-400" },
];
 
export default function CardExpansion() {
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const selectedCard = cards.find((c) => c.id === selectedId);
 
  return (
    <div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
      <div className="grid grid-cols-3 gap-4">
        {cards.map((card) => (
          <motion.div
            key={card.id}
            layoutId={`card-${card.id}`}
            onClick={() => setSelectedId(card.id)}
            className={`cursor-pointer rounded-2xl bg-gradient-to-br ${card.color} p-6`}
            whileHover={{ scale: 1.03 }}
            whileTap={{ scale: 0.98 }}
          >
            <motion.h3
              layoutId={`title-${card.id}`}
              className="text-xl font-bold text-white"
            >
              {card.title}
            </motion.h3>
          </motion.div>
        ))}
      </div>
 
      <AnimatePresence>
        {selectedId && selectedCard && (
          <>
            <motion.div
              initial={{ opacity: 0 }}
              animate={{ opacity: 0.5 }}
              exit={{ opacity: 0 }}
              onClick={() => setSelectedId(null)}
              className="fixed inset-0 bg-black"
            />
            <motion.div
              layoutId={`card-${selectedId}`}
              className={`fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md rounded-2xl bg-gradient-to-br ${selectedCard.color} p-8`}
            >
              <motion.h3
                layoutId={`title-${selectedId}`}
                className="text-2xl font-bold text-white"
              >
                {selectedCard.title}
              </motion.h3>
              <motion.p
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
                className="mt-4 text-white/90"
              >
                {selectedCard.description}
              </motion.p>
              <motion.button
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
                onClick={() => setSelectedId(null)}
                className="mt-6 rounded-lg bg-white/20 px-4 py-2 text-white font-medium backdrop-blur"
              >
                إغلاق
              </motion.button>
            </motion.div>
          </>
        )}
      </AnimatePresence>
    </div>
  );
}

ينشئ هذا توسعاً سلساً من البطاقة إلى النافذة بدون أي تتبع يدوي للموضع. يحسب Motion رسم FLIP المتحرك تلقائياً.

الخطوة 8: الرسوم المتحركة المدفوعة بالتمرير

يوفر Motion خطافات لإنشاء رسوم متحركة تستجيب لموضع التمرير. هذا مثالي لأقسام البطل ومؤشرات التقدم وتأثيرات المنظور والكشف عند التمرير.

مؤشر تقدم التمرير

"use client";
 
import { motion, useScroll, useSpring } from "motion/react";
 
export default function ScrollProgress() {
  const { scrollYProgress } = useScroll();
  const scaleX = useSpring(scrollYProgress, {
    stiffness: 100,
    damping: 30,
    restDelta: 0.001,
  });
 
  return (
    <>
      <motion.div
        style={{ scaleX }}
        className="fixed top-0 left-0 right-0 z-50 h-1 origin-left bg-blue-500"
      />
      <main className="space-y-8 p-8">
        {Array.from({ length: 20 }).map((_, i) => (
          <div
            key={i}
            className="mx-auto h-48 max-w-2xl rounded-2xl bg-gray-900 border border-gray-800"
          />
        ))}
      </main>
    </>
  );
}

الكشف عند التمرير

"use client";
 
import { motion, useInView } from "motion/react";
import { useRef } from "react";
 
function RevealSection({ children }: { children: React.ReactNode }) {
  const ref = useRef<HTMLDivElement>(null);
  const isInView = useInView(ref, { once: true, margin: "-100px" });
 
  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 60 }}
      animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 60 }}
      transition={{ duration: 0.7, ease: "easeOut" }}
    >
      {children}
    </motion.div>
  );
}
 
export default function ScrollReveal() {
  const sections = [
    { title: "صمّم", text: "أنشئ واجهات جميلة مع Motion" },
    { title: "حرّك", text: "أضف رسوم متحركة سائلة لكل تفاعل" },
    { title: "انشر", text: "انشر رسوم متحركة عالية الأداء للإنتاج" },
  ];
 
  return (
    <main className="min-h-screen bg-gray-950 p-8">
      <div className="mx-auto max-w-2xl space-y-32 py-32">
        {sections.map((section) => (
          <RevealSection key={section.title}>
            <h2 className="text-4xl font-bold text-white">{section.title}</h2>
            <p className="mt-4 text-xl text-gray-400">{section.text}</p>
          </RevealSection>
        ))}
      </div>
    </main>
  );
}

تأثير المنظور

"use client";
 
import { motion, useScroll, useTransform } from "motion/react";
import { useRef } from "react";
 
export default function ParallaxHero() {
  const ref = useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["start start", "end start"],
  });
 
  const y = useTransform(scrollYProgress, [0, 1], [0, 200]);
  const opacity = useTransform(scrollYProgress, [0, 0.8], [1, 0]);
  const scale = useTransform(scrollYProgress, [0, 1], [1, 0.85]);
 
  return (
    <div ref={ref} className="relative h-screen overflow-hidden bg-gray-950">
      <motion.div
        style={{ y, opacity, scale }}
        className="flex h-full flex-col items-center justify-center"
      >
        <h1 className="text-6xl font-bold text-white">بطل متحرك</h1>
        <p className="mt-4 text-xl text-gray-400">مرّر للأسفل لرؤية التأثير</p>
      </motion.div>
    </div>
  );
}

الخطوة 9: بناء معرض منتجات متحرك كامل

لنجمع كل شيء في مكون حقيقي — معرض بطاقات منتجات متحرك مع دخول متتابع وتأثيرات تحوم وعروض تفصيلية قابلة للتوسيع.

"use client";
 
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
 
interface Product {
  id: string;
  name: string;
  price: string;
  category: string;
  gradient: string;
  description: string;
}
 
const products: Product[] = [
  {
    id: "1",
    name: "سماعات احترافية",
    price: "$299",
    category: "صوت",
    gradient: "from-violet-600 to-indigo-600",
    description:
      "سماعات لاسلكية فاخرة مع إلغاء ضوضاء نشط وبطارية 40 ساعة وصوت بجودة الاستوديو.",
  },
  {
    id: "2",
    name: "شاشة ألترا",
    price: "$899",
    category: "عرض",
    gradient: "from-cyan-600 to-blue-600",
    description:
      "شاشة 32 بوصة 4K مع HDR1000 ومعدل تحديث 165 هرتز وألوان معايرة مصنعياً للمحترفين المبدعين.",
  },
  {
    id: "3",
    name: "لوحة مفاتيح ميكانيكية",
    price: "$179",
    category: "إدخال",
    gradient: "from-orange-600 to-red-600",
    description:
      "لوحة مفاتيح ميكانيكية قابلة للتبديل السريع مع إضاءة RGB لكل مفتاح وتصميم حشية وأغطية مفاتيح PBT فاخرة.",
  },
  {
    id: "4",
    name: "فأرة مريحة",
    price: "$129",
    category: "إدخال",
    gradient: "from-emerald-600 to-teal-600",
    description:
      "فأرة عمودية مريحة مع مستشعر 8K DPI وبلوتوث 5.3 وأزرار جانبية قابلة للتخصيص.",
  },
];
 
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: { staggerChildren: 0.08, delayChildren: 0.1 },
  },
};
 
const cardVariants = {
  hidden: { opacity: 0, y: 30, scale: 0.96 },
  visible: {
    opacity: 1,
    y: 0,
    scale: 1,
    transition: { type: "spring", stiffness: 300, damping: 24 },
  },
};
 
export default function ProductGallery() {
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const selectedProduct = products.find((p) => p.id === selectedId);
 
  return (
    <div className="min-h-screen bg-gray-950 p-8">
      <motion.h1
        initial={{ opacity: 0, y: -20 }}
        animate={{ opacity: 1, y: 0 }}
        className="mb-12 text-center text-4xl font-bold text-white"
      >
        المنتجات المميزة
      </motion.h1>
 
      <motion.div
        variants={containerVariants}
        initial="hidden"
        animate="visible"
        className="mx-auto grid max-w-5xl grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4"
      >
        {products.map((product) => (
          <motion.div
            key={product.id}
            layoutId={`product-${product.id}`}
            variants={cardVariants}
            onClick={() => setSelectedId(product.id)}
            whileHover={{ y: -8, transition: { type: "spring", stiffness: 400 } }}
            whileTap={{ scale: 0.97 }}
            className="cursor-pointer overflow-hidden rounded-2xl bg-gray-900 border border-gray-800"
          >
            <div
              className={`h-40 bg-gradient-to-br ${product.gradient} flex items-center justify-center`}
            >
              <motion.span
                layoutId={`emoji-${product.id}`}
                className="text-5xl"
              >
                {product.category === "صوت"
                  ? "🎧"
                  : product.category === "عرض"
                    ? "🖥️"
                    : product.category === "إدخال"
                      ? "⌨️"
                      : "🖱️"}
              </motion.span>
            </div>
            <div className="p-4">
              <motion.h3
                layoutId={`name-${product.id}`}
                className="font-bold text-white"
              >
                {product.name}
              </motion.h3>
              <motion.span
                layoutId={`price-${product.id}`}
                className="mt-1 block text-sm text-gray-400"
              >
                {product.price}
              </motion.span>
            </div>
          </motion.div>
        ))}
      </motion.div>
 
      <AnimatePresence>
        {selectedId && selectedProduct && (
          <>
            <motion.div
              key="backdrop"
              initial={{ opacity: 0 }}
              animate={{ opacity: 0.6 }}
              exit={{ opacity: 0 }}
              onClick={() => setSelectedId(null)}
              className="fixed inset-0 z-40 bg-black"
            />
            <motion.div
              key="modal"
              layoutId={`product-${selectedId}`}
              className="fixed z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-lg overflow-hidden rounded-2xl bg-gray-900 border border-gray-800"
            >
              <div
                className={`h-48 bg-gradient-to-br ${selectedProduct.gradient} flex items-center justify-center`}
              >
                <motion.span
                  layoutId={`emoji-${selectedId}`}
                  className="text-7xl"
                >
                  {selectedProduct.category === "صوت"
                    ? "🎧"
                    : selectedProduct.category === "عرض"
                      ? "🖥️"
                      : selectedProduct.category === "إدخال"
                        ? "⌨️"
                        : "🖱️"}
                </motion.span>
              </div>
              <div className="p-6">
                <motion.h3
                  layoutId={`name-${selectedId}`}
                  className="text-2xl font-bold text-white"
                >
                  {selectedProduct.name}
                </motion.h3>
                <motion.span
                  layoutId={`price-${selectedId}`}
                  className="mt-1 block text-lg text-gray-400"
                >
                  {selectedProduct.price}
                </motion.span>
                <motion.p
                  initial={{ opacity: 0 }}
                  animate={{ opacity: 1, transition: { delay: 0.2 } }}
                  exit={{ opacity: 0 }}
                  className="mt-4 text-gray-300"
                >
                  {selectedProduct.description}
                </motion.p>
                <motion.button
                  initial={{ opacity: 0, y: 10 }}
                  animate={{ opacity: 1, y: 0, transition: { delay: 0.3 } }}
                  exit={{ opacity: 0 }}
                  onClick={() => setSelectedId(null)}
                  className="mt-6 w-full rounded-xl bg-white/10 py-3 text-white font-semibold hover:bg-white/20 transition-colors"
                >
                  إغلاق
                </motion.button>
              </div>
            </motion.div>
          </>
        )}
      </AnimatePresence>
    </div>
  );
}

يوضح هذا المعرض:

  • دخول متتابع مع containerVariants وcardVariants
  • تأثير رفع عند التحوم مع whileHover
  • انتقال تخطيط مشترك من البطاقة إلى النافذة مع layoutId
  • رسم خروج متحرك مع AnimatePresence
  • كشف محتوى متأخر داخل النافذة الموسعة

الخطوة 10: تحسين الأداء

صُمم Motion ليكون عالي الأداء، لكن هناك أنماط للحفاظ على سلاسة الرسوم المتحركة في الإنتاج.

1. حرّك Transform وOpacity فقط

خصائص التحويل والشفافية المسرّعة بالـ GPU هي الأرخص في التحريك. تجنب تحريك width وheight وpadding أو top/left — استخدم scale وx وy وopacity بدلاً من ذلك.

// بطيء: يُفعّل إعادة حساب التخطيط
<motion.div animate={{ width: 200, height: 200 }} />
 
// سريع: تحويل مسرّع بالـ GPU
<motion.div animate={{ scale: 1.5 }} />

2. استخدم layout بحكمة

خاصية layout قوية لكنها مكلفة للقوائم الكبيرة. إذا كنت تحتاج فقط تغييرات الموضع، استخدم layout="position" بدلاً من layout={true}:

// يحرّك الموضع والحجم ونصف القطر
<motion.div layout />
 
// يحرّك الموضع فقط (أرخص)
<motion.div layout="position" />

3. قلّل الحركة لإمكانية الوصول

احترم دائماً تفضيل المستخدم لتقليل الحركة:

"use client";
 
import { motion, useReducedMotion } from "motion/react";
 
export default function AccessibleAnimation() {
  const shouldReduceMotion = useReducedMotion();
 
  return (
    <motion.div
      initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 40 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: shouldReduceMotion ? 0.01 : 0.6 }}
      className="rounded-2xl bg-gray-900 p-8"
    >
      <p className="text-white">يحترم prefers-reduced-motion</p>
    </motion.div>
  );
}

4. تحميل كسول لمكونات Motion

إذا كانت الرسوم المتحركة تُستخدم فقط في صفحات محددة، استورد Motion ديناميكياً لتقليل حجم الحزمة:

import dynamic from "next/dynamic";
 
const AnimatedHero = dynamic(() => import("@/components/AnimatedHero"), {
  ssr: false,
  loading: () => <div className="h-screen bg-gray-950" />,
});

5. استخدم will-change باعتدال

يطبّق Motion بالفعل will-change: transform عند الحاجة. لا تضفه يدوياً إلا بعد التحليل والتأكد من أنه يساعد — الاستخدام المفرط يستهلك ذاكرة GPU.

استكشاف الأخطاء وإصلاحها

المشاكل الشائعة

وميض الرسوم المتحركة عند أول عرض مع Next.js App Router

أضف "use client" لأي مكون يستخدم Motion. لا يمكن لـ Server Components استخدام خطافات أو خصائص إيماءات Motion.

رسوم التخطيط تسبب قفز العناصر

تأكد من أن العناصر الأب لها أبعاد مستقرة. تجنب العرض بالنسب المئوية على العناصر التي لها layout — استخدم أحجام ثابتة أو مرنة بدلاً من ذلك.

رسم الخروج لا يعمل

تأكد من:

  1. AnimatePresence يحيط بالعناصر الشرطية
  2. كل ابن لديه خاصية key فريدة
  3. خاصية exit معرّفة على الابن المباشر لـ AnimatePresence

حجم الحزمة كبير جداً

Motion v11+ قابل للتقليم بالكامل. استورد فقط ما تحتاجه:

// جيد: يستورد فقط ما هو مطلوب
import { motion, AnimatePresence } from "motion/react";
 
// تجنب: استيراد كل شيء
import * as Motion from "motion/react";

الخطوات التالية

الآن بعد فهمك لأساسيات Motion، إليك بعض الاتجاهات للاستكشاف:

  • انتقالات الصفحات — ادمج Motion مع انتقالات تخطيط Next.js للتنقل السلس
  • رسوم SVG المتحركة — استخدم motion.path وpathLength لتأثيرات الرسم
  • تحولات ثلاثية الأبعاد — استخدم rotateX وrotateY وperspective لتأثيرات قلب البطاقات
  • منظور مرتبط بالتمرير — ابنِ صفحات هبوط غامرة مع useScroll وuseTransform
  • خطافات رسوم قابلة لإعادة الاستخدام — استخرج الأنماط في خطافات مخصصة مثل useFadeIn أو useStagger

الخلاصة

يقدم Motion رسوم متحركة بمستوى إنتاجي لـ React مع واجهة برمجة بسيطة وتصريحية. تعلمت كيف تنشئ رسوم متحركة أساسية مع النوابض والتخفيفات، وتبني إيماءات تفاعلية للتحوم والنقر والسحب، وتنسّق تسلسلات معقدة بالمتغيرات، وتتعامل مع انتقالات الدخول والخروج مع AnimatePresence، وتحرّك تغييرات التخطيط تلقائياً، وتبني تأثيرات مدفوعة بالتمرير.

المفتاح لرسوم واجهة مستخدم رائعة هو الاعتدال — حرّك بهدف، واحترم تفضيلات تقليل الحركة، وحسّن الأداء دائماً. ابدأ بمعرض المنتجات الذي بنيته في هذا الدليل وجرّب إضافة Motion لمشاريعك الخاصة.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على Vitest و React Testing Library مع Next.js 15: الدليل الشامل لاختبارات الوحدة في 2026.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة

إضافة المصادقة لتطبيق Next.js 15 باستخدام Auth.js v5: البريد الإلكتروني وOAuth والتحكم بالأدوار

تعلم كيفية إضافة نظام مصادقة جاهز للإنتاج لتطبيق Next.js 15 باستخدام Auth.js v5. يغطي هذا الدليل الشامل تسجيل الدخول عبر Google OAuth وبيانات الاعتماد بالبريد الإلكتروني وكلمة المرور والمسارات المحمية والتحكم بالوصول حسب الأدوار.

30 د قراءة·