الرسوم المتحركة

Starter pack

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

إتقان الرسوم المتحركة هو مهارة أساسية لخلق تجارب غامرة. كلما زادت التقنيات التي تعرفها، زاد ما يمكنك التعبير عن إبداعك.

ملاحظة: يتم تغطية الرسوم المتحركة للنموذج ثلاثي الأبعاد في فصل النماذج.

Lerp

Lerp هو وظيفة رياضية تقوم بالمزج بين قيمتين. يعتبر مفيدًا لتحريك قيمة من نقطة إلى أخرى.

const value = THREE.MathUtils.lerp(start, end, t);

المعامل الأول هو قيمة البداية، والمعامل الثاني هو قيمة النهاية والمعامل الثالث هو عامل المزج بين 0 و1.

كلما اقترب عامل المزج من 0، كان التحريك أبطأ. وكلما اقترب عامل المزج من 1، كان التحريك أسرع للوصول إلى قيمة النهاية أو الهدف.

دعونا نجرب ذلك على مكون AnimatedBox من حزمة البداية.

يحتوي المكون على مصفوفة boxPositions التي تحتوي على مواقع المكعب في أوقات مختلفة. دعونا نضيف خطاف useFrame لتحديث الموقع للمكعب بناءً على الوقت:

import { RoundedBox } from "@react-three/drei";
import { useRef } from "react";

export const AnimatedBox = ({ boxPositions, ...props }) => {
  const box = useRef();

  useFrame(({ clock }) => {
    const seconds = parseInt(clock.getElapsedTime());
    const targetPosition = boxPositions[seconds % boxPositions.length];

    box.current.position.x = targetPosition.x;
    box.current.position.y = targetPosition.y;
    box.current.position.z = targetPosition.z;
  });
  // ...
};

هنا لا يتم تحريك المكعب. بل ينتقل بين موقع وآخر. دعونا نستخدم وظيفة lerp لتحريكه:

import * as THREE from "three";
// ...

export const AnimatedBox = ({ boxPositions, ...props }) => {
  const box = useRef();

  useFrame(({ clock }) => {
    const seconds = parseInt(clock.getElapsedTime());
    const targetPosition = boxPositions[seconds % boxPositions.length];
    box.current.position.x = THREE.MathUtils.lerp(
      box.current.position.x,
      targetPosition.x,
      0.05
    );
    box.current.position.y = THREE.MathUtils.lerp(
      box.current.position.y,
      targetPosition.y,
      0.05
    );
    box.current.position.z = THREE.MathUtils.lerp(
      box.current.position.z,
      targetPosition.z,
      0.05
    );
  });
  // ...
};

الآن المكعب محرك. إنه يتحرك بسلاسة من موقع إلى آخر.

تحتوي فئة Vector3 على دالة lerp والتي يمكن استخدامها بدلاً من وظيفة THREE.MathUtils.lerp:

// box.current.position.x = THREE.MathUtils.lerp(
//   box.current.position.x,
//   targetPosition.x,
//   0.05
// );
// box.current.position.y = THREE.MathUtils.lerp(
//   box.current.position.y,
//   targetPosition.y,
//   0.05
// );
// box.current.position.z = THREE.MathUtils.lerp(
//   box.current.position.z,
//   targetPosition.z,
//   0.05
// );
box.current.position.lerp(targetPosition, 0.05);

هذا يوفر الكثير من الأسطر للحصول على نفس النتيجة!

لا تتردد في التلاعب بالوقت واستخدام lerp على خصائص أخرى مثل الدوران أو المقياس لتجربة تأثيرات مختلفة.

Float

Float هو مكون مغلف من مكتبة Drei لمنح الانطباع ببساطة أن الكائن يطفو.

تحتوي على الخصائص التالية:

  • speed: سرعة الحركة، القيمة الافتراضية 1
  • rotationIntensity: كثافة الدوران XYZ، القيمة الافتراضية 1
  • floatIntensity: كثافة الطفو لأعلى/لأسفل، يعمل كمعامل مضاعف مع floatingRange، القيمة الافتراضية 1
  • floatingRange: نطاق قيم المحور y الذي سيطفو داخله الكائن، القيمة الافتراضية [-0.1, 0.1]

لنقم بجعل البطة تطفو في Experience.jsx:

import { Float, Scroll } from "@react-three/drei";

// ...
export const Experience = () => {
  const viewport = useThree((state) => state.viewport);

  return (
    <>
      <group position-y={-0.75}>
        <Float floatIntensity={2} speed={3}>
          <Duck />
        </Float>
        {/* ... */}
      </group>
      {/* ... */}
    </>
  );
};

الآن البطة تطفو، تبدو كأنها بالون هيليوم. إنه تأثير بسيط ولكنه يضيف حياة إلى المشهد.

GSAP

GSAP هي مكتبة رسوم متحركة JavaScript، تُستخدم بشكل واسع في صناعة الويب. هي سهلة الاستخدام وقوية.

ليست مخصصة لـ Three.js/React Three Fiber لكنها متوافقة معها.

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

دعنا ننتقل إلى مكون Background ونحرك بسلاسة لون الخلفية عند التمرير.

أولاً، نحتاج إلى تثبيت مكتبة GSAP:

yarn add gsap

ثم، دعونا ننشئ مرجعين. واحد لـ GSAP timeline وآخر لتخزين قيمة لون الخلفية:

import { gsap } from "gsap";
// ...

export const Background = () => {
  const skyMaterial = useRef();
  const tl = useRef();
  const skyData = useRef({
    color: "#313131",
  });
  // ...
};

ثم دعنا ننشئ useEffect hook لـ تهيئة الجدول الزمني:

// ...

useEffect(() => {
  tl.current = gsap.timeline();
  tl.current
    .to(skyData.current, {
      duration: 1,
      color: "#ffc544",
    })
    .to(skyData.current, {
      duration: 1,
      color: "#7c4e9f",
    });
}, []);

// ...

gsap.timeline() ينشئ جدولًا زمنيًا.

طريقة to تحرك كائن skyData. تقوم بتغيير خاصية اللون من #313131 (القيمة الافتراضية) إلى #ffc544 في ثانية واحدة. ثم تغير خاصية اللون من #ffc544 إلى #7c4e9f في ثانية واحدة.

GSAP يقوم تلقائيًا بـ استنباط قيمة اللون بين اللونين ولكن لا يمكنك استخدام أسماء الألوان بـ CSS مثل red أو blue.

أخيرًا، دعنا نحدث لون skyMaterial:

// ...
useFrame(() => {
  if (!tl.current) {
    return;
  }
  skyMaterial.current.color.set(skyData.current.color);
});
// ...

ملاحظة: نحتاج إلى skyData وندعو طريقة set على خاصية color لأن skyMaterial.current.color هو كائن THREE.Color. GSAP لا يمكنه تلقائيًا استنباطه. يمكنه فقط استنباط الأرقام والسلاسل.

وهذا يعني أنه يمكنك تحريك position, rotation و scale مباشرة على mesh أو group دون الحاجة لإنشاء كائن إضافي مثل الذي قمنا به مع skyData ولا تحتاج لتعيينها يدويًا في useFrame hook.

عند إعادة التحميل، لون الخلفية يكون متحركًا بمجرد تحميله.

ما نريده هو أن نشغل الرسوم المتحركة عندما يقوم المستخدم بالتمرير.

نحتاج أولاً إلى إيقاف الجدول الزمني بعد إنشائه:

// ...
useEffect(() => {
  tl.current = gsap.timeline();
  tl.current
    .to(skyData.current, {
      duration: 1,
      color: "#ffc544",
    })
    .to(skyData.current, {
      duration: 1,
      color: "#7c4e9f",
    });
  tl.current.pause();
}, []);
// ...

ثم نحتاج إلى الحصول على بيانات التمرير من useScroll hook وتعيين تقدم الجدول الزمني للإزاحة التمرير:

// ...
const scrollData = useScroll();
useFrame(() => {
  if (!tl.current) {
    return;
  }
  tl.current.progress(scrollData.offset);
  skyMaterial.current.color.set(skyData.current.color);
});
// ...

الآن لون الخلفية يكون متحركًا بشكل مثالي عندما يقوم المستخدم بالتمرير من قسم إلى آخر.

لاكتشاف جميع الخيارات المختلفة المتوفرة لديك مع GSAP وكيفية استخدامها تحقق من التوثيق.

للمزيد من التعمق يمكنك محاولة تحريك الموضع, التدوير والمقياس لـ البطة.

الرسوم المتحركة المعتمدة على الزنبرك

على العكس من الرسوم المتحركة الخطية التي رأيناها حتى الآن، فإن الرسوم المتحركة المعتمدة على الزنبرك هي مستندة إلى الفيزياء. فهي أكثر واقعية وتعبيرية.

بدلاً من الانتقال من نقطة إلى أخرى بسرعة ثابتة، يتم تسريع وتباطؤ الحركة بناءً على معايير قابلة للتكوين.

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

اثنتان من المكتبات الشهيرة لإنشاء رسوم متحركة معتمدة على الزنبرك هما Framer Motion وReact Spring.

كلتاهما متوافقة مع React Three Fiber وسهلة الاستخدام.

Framer Motion

لفهم كيفية عمل Framer Motion، دعونا نقوم بتحريك الأسنان في القسم الثاني.

أولاً، نحتاج إلى تثبيت مكتبة Framer Motion:

yarn add framer-motion framer-motion-3d

ملاحظة: نظرًا لأننا نريد تحريك كائنات ثلاثية الأبعاد، نحتاج إلى تثبيت الحزمة framer-motion-3d.

لتحريك كائن ثلاثي الأبعاد باستخدام Framer Motion، نحتاج إلى استخدام المكافئ motion للمكون من framer-motion-3d بدلاً من مكونات React Three Fiber:

import { motion } from "framer-motion-3d";

واستبدال مكون mesh الخاص بنا بـ motion.mesh:

<motion.mesh position-x={-1} position-y={-1}>
  {/* ... */}
</motion.mesh>

الآن يمكننا استخدام props motion لتحريك السن:

// ...
export const Teeth = () => {
  return (
    <group>
      <motion.mesh
        position-x={-1}
        position-y={-1}
        animate={{
          y: 0,
        }}
        transition={{
          repeat: Infinity,
          repeatDelay: 1,
        }}
      >
        <coneGeometry args={[0.5, 1, 4]} />
        <meshStandardMaterial color="#ffffff" />
      </motion.mesh>
      <mesh position-x={0} position-y={1} rotation-x={Math.PI}>
        <coneGeometry args={[0.5, 1, 4]} />
        <meshStandardMaterial color="#ffffff" />
      </mesh>
      <mesh position-x={1} position-y={-1}>
        <coneGeometry args={[0.5, 1, 4]} />
        <meshStandardMaterial color="#ffffff" />
      </mesh>
    </group>
  );
};

يُعد prop animate كائنًا يحتوي على الخصائص المراد تحريكها. هنا نريد تحريك الموقع y من -1 إلى 0.

يُعد prop transition كائنًا يحتوي على خيارات الرسوم المتحركة. هنا نريد تكرار الرسوم المتحركة بلا نهاية مع تأخير لمدة ثانية واحدة بين كل تكرار.

أصبح الآن السن الأيسر متحركًا.

لإنشاء نظام وتحريك جميع أسناننا، بدلاً من استخدام prop animate، سنستخدم variants. مما سيساعدنا على التحكم في الرسوم المتحركة في مكان واحد.

لننشئ حالة لتخزين قيمة النسخة للأسنان:

export const Teeth = () => {
  const [variant, setVariant] = useState("closed");

  useEffect(() => {
    const interval = setInterval(() => {
      setVariant((variant) => (variant === "opened" ? "closed" : "opened"));
    }, 1500);
    return () => clearInterval(interval);
  });
  // ...
};

كل 1.5 ثانية، نقوم بتبديل النسخة بين opened وclosed.

ثم نحتاج إلى تغليف مكونات motion.mesh الخاصة بنا بمكون motion.group وضبط prop animate بقيمة النسخة:

// ...
return <motion.group animate={variant}>{/* ... */}</motion.group>;
// ...

الآن دعونا نستبدل جميع أسناننا بـ motion.mesh ونحدد variants:

// ...
return (
  <motion.group animate={variant}>
    <motion.mesh
      position-x={-1}
      position-y={-1}
      variants={{
        closed: {
          y: -1,
          rotateY: 0,
        },
        opened: {
          scale: 1.1,
          y: 0,
          rotateY: Math.PI / 2,
        },
      }}
    >
      <coneGeometry args={[0.5, 1, 4]} />
      <motion.meshStandardMaterial
        color="#ffffff"
        variants={{
          closed: {
            color: "#ffffff",
          },
          opened: {
            color: "#7564a4",
          },
        }}
      />
    </motion.mesh>
    <motion.mesh
      position-x={0}
      position-y={1}
      rotation-x={Math.PI}
      variants={{
        closed: {
          y: 1,
          rotateY: 0,
        },
        opened: {
          scale: 1.2,
          y: 0,
          rotateY: Math.PI / 2,
        },
      }}
    >
      <coneGeometry args={[0.5, 1, 4]} />
      <motion.meshStandardMaterial
        color="#ffffff"
        variants={{
          closed: {
            color: "#ffffff",
          },
          opened: {
            color: "#7c5ecf",
          },
        }}
      />
    </motion.mesh>
    <motion.mesh
      position-x={1}
      position-y={-1}
      variants={{
        closed: {
          y: -1,
          rotateY: 0,
        },
        opened: {
          scale: 1.1,
          y: 0,
          rotateY: Math.PI / 2,
        },
      }}
    >
      <coneGeometry args={[0.5, 1, 4]} />
      <motion.meshStandardMaterial
        color="#ffffff"
        variants={{
          closed: {
            color: "#ffffff",
          },
          opened: {
            color: "#6232e6",
          },
        }}
      />
    </motion.mesh>
  </motion.group>
);
// ...

نحن نقوم أيضًا بتحريك scale وrotation لـ mesh وكذلك color لـ meshStandardMaterial للحصول على تأثير أفضل.

نستطيع تغيير إعدادات الرسوم المتحركة لجعلها أكثر واقعية.

لتطبيقها على جميع مكونات motion.mesh الخاصة بنا، يمكننا استخدام مكون MotionConfig من framer-motion:

import { MotionConfig } from "framer-motion";
import { motion } from "framer-motion-3d";
import { useEffect, useState } from "react";
export const Teeth = () => {
  const [variant, setVariant] = useState("closed");

  useEffect(() => {
    const interval = setInterval(() => {
      setVariant((variant) => (variant === "opened" ? "closed" : "opened"));
    }, 1500);
    return () => clearInterval(interval);
  });
  return (
    <MotionConfig
      transition={{
        type: "spring",
        mass: 5,
        stiffness: 500,
        damping: 42,
      }}
    >
      <motion.group animate={variant}>
        <motion.mesh
          position-x={-1}
          position-y={-1}
          variants={{
            closed: {
              y: -1,
              rotateY: 0,
            },
            opened: {
              scale: 1.1,
              y: 0,
              rotateY: Math.PI / 2,
            },
          }}
        >
          <coneGeometry args={[0.5, 1, 4]} />
          <motion.meshStandardMaterial
            color="#ffffff"
            variants={{
              closed: {
                color: "#ffffff",
              },
              opened: {
                color: "#7564a4",
              },
            }}
          />
        </motion.mesh>
        <motion.mesh
          position-x={0}
          position-y={1}
          rotation-x={Math.PI}
          variants={{
            closed: {
              y: 1,
              rotateY: 0,
            },
            opened: {
              scale: 1.2,
              y: 0,
              rotateY: Math.PI / 2,
            },
          }}
        >
          <coneGeometry args={[0.5, 1, 4]} />
          <motion.meshStandardMaterial
            color="#ffffff"
            variants={{
              closed: {
                color: "#ffffff",
              },
              opened: {
                color: "#7c5ecf",
              },
            }}
          />
        </motion.mesh>
        <motion.mesh
          position-x={1}
          position-y={-1}
          variants={{
            closed: {
              y: -1,
              rotateY: 0,
            },
            opened: {
              scale: 1.1,
              y: 0,
              rotateY: Math.PI / 2,
            },
          }}
        >
          <coneGeometry args={[0.5, 1, 4]} />
          <motion.meshStandardMaterial
            color="#ffffff"
            variants={{
              closed: {
                color: "#ffffff",
              },
              opened: {
                color: "#6232e6",
              },
            }}
          />
        </motion.mesh>
      </motion.group>
    </MotionConfig>
  );
};

أصبحت الرسوم المتحركة الآن أكثر واقعية. حيث ترتد قليلًا عندما تصل إلى الأعلى والأسفل.

يمكنك التلاعب بإعدادات الرسوم المتحركة لفهم كيف تؤثر القيم على الرسوم المتحركة.

React Spring

دعونا الأن نقوم بتحريك مكون AnimatedDodecahedron باستخدام React Spring.

سوف نقوم بضم كل من Framer Motion وReact Spring في مشروع واحد لأغراض العرض. لأنه بما أنهما متشابهين جدًا، فإن أحدهما يجب أن يكون كافيًا لمشاريعك. اختر واحدًا تفضل استخدامه.

أولا، نحن بحاجة إلى تثبيت مكتبة React Spring:

yarn add @react-spring/three

ثم لتعريف حركتنا، نحتاج لاستخدام الـuseSpring hook:

import { useSpring } from "@react-spring/three";
import { Dodecahedron } from "@react-three/drei";

export const AnimatedDodecahedron = () => {
  const { x, y, rotationX, rotationZ } = useSpring({
    from: {
      x: -1,
      y: 1,
      rotationX: 0,
      rotationZ: 0,
    },
    to: [
      {
        x: 1,
        y: 1,
        delay: 500,
      },
      {
        x: -1,
        y: 0,
        rotationX: Math.PI,
        rotationZ: Math.PI,
        delay: 50,
      },
      {
        x: 1,
        y: 0,
        delay: 50,
      },
      {
        x: -1,
        y: -1,
        rotationX: Math.PI * 2,
        rotationZ: Math.PI * 2,
        delay: 50,
      },
      {
        x: 1,
        y: -1,
        delay: 50,
      },
      {
        x: -1,
        y: 1,
        rotationX: 0,
        rotationZ: 0,
        delay: 500,
      },
    ],
    config: {
      mass: 4,
      tension: 600,
      friction: 80,
    },
    loop: true,
    immediate: true,
  });

  // ...
};
  • الخاصية from هي الحالة الأولية للـحركة.
  • الخاصية to هي مصفوفة من الـحالات. كل حالة هي كائن يحتوي على الخصائص التي ستتحرك. خاصية delay هي الفاصل الزمني قبل بدء الـحركة.
  • الخاصية config هي كائن يحتوي على إعدادات الحركة.
  • الخاصية loop هي قيمة منطقية تشير إلى ما إذا كان يجب أن تكون الـحركة متكررة.
  • الخاصية immediate هي قيمة منطقية تشير إلى ما إذا كان يجب أن تبدأ الـحركة فوراً.

سنقوم بتطبيق الـحركة على group تغلف مكون Dodecahedron. كما رأينا لـframer motion التي تتطلب استخدام المكون motion.group, نحن بحاجة إلى استخدام المكون animated من react-spring/three:

import { animated, useSpring } from "@react-spring/three";
export const AnimatedDodecahedronAnim = () => {
  const { x, y, rotationX, rotationZ } = useSpring({
    // ...
  });

  return (
    <animated.group
      position-x={x}
      position-y={y}
      rotation-x={rotationX}
      rotation-z={rotationZ}
    >
      <Dodecahedron>
        <meshStandardMaterial color="red" transparent opacity={0.6} />
      </Dodecahedron>
    </animated.group>
  );
};

إذا قمت بتجربتها، فإن Dodecahedron ربما لن يكون متحركًا وقد تواجه هذه التحذير في وحدة التحكم:

React Spring Warning

هذا بسبب أننا ثبتنا الإصدار الأحدث من React Spring ولكن مكتبة Drei لديها بالفعل React Spring كـ اعتماد ولكن ليس الإصدار الأحدث.

لحل هذه المشكلة، نحتاج إلى تثبيت نفس إصدار React Spring كما هو مستخدم من قبل Drei.

لعثور على إصدار React Spring المستخدم من قبل Drei، يمكننا الذهاب إلى ملف yarn.lock والبحث عن @react-spring/three في قسم الاعتمادات لـ @react-three/drei:

"@react-three/drei@^9.74.15":
  version "9.74.15"
  resolved "https://registry.yarnpkg.com/@react-three/drei/-/drei-9.74.15.tgz#9d33496afe45e4f164852c99671fe5a5bb863457"
  integrity sha512-w2c7V6SLmEKwImF2c/sB+8SmlkoXwn6CTVwGoWMqvrRbZs4Qo5UPP5VIxQwTjUyA0oeXKlAoLNhSlUXgOJsp7Q==
  dependencies:
    "@babel/runtime" "^7.11.2"
    "@mediapipe/tasks-vision" "0.10.2-rc2"
    "@react-spring/three" "~9.6.1"
    "@use-gesture/react" "^10.2.24"
    camera-controls "^2.4.2"
    detect-gpu "^5.0.28"
    glsl-noise "^0.0.0"
    lodash.clamp "^4.0.3"
    lodash.omit "^4.5.0"
    lodash.pick "^4.4.0"
    maath "^0.6.0"
    meshline "^3.1.6"
    react-composer "^5.0.3"
    react-merge-refs "^1.1.0"
    stats.js "^0.17.0"
    suspend-react "^0.1.3"
    three-mesh-bvh "^0.6.0"
    three-stdlib "^2.23.9"
    troika-three-text "^0.47.2"
    utility-types "^3.10.0"
    zustand "^3.5.13"

هنا يمكننا رؤية أن الإصدار من React Spring المستخدم في Drei هو ~9.6.1.

لنقم الآن بتثبيت هذا الإصدار:

yarn add @react-spring/three@~9.6.1

الآن أعد تشغيل خادم التطوير والـحركة يجب أن تعمل.

الـDodecahedron الآن متحرك. إنها تدور وتتحرك من نقطة إلى أخرى مع تأخيرات مختلفة بين كل حركة.

تحقق من وثائق React Spring لاكتشاف جميع الـhooks والـمكونات التي يمكنك استخدامها لتحريك الكائنات ثلاثية الأبعاد الخاصة بك.

الخاتمة

الآن لديك مجموعة من التقنيات المختلفة لتحريك المشاهد ثلاثية الأبعاد الخاصة بك. يمكنك الآن التعبير عن إبداعك وإحياء مشاريعك.

Three.js logoReact logo

React Three Fiber: The Ultimate Guide to 3D Web Development

✨ You have reached the end of the preview ✨

Go to the next level with Three.js and React Three Fiber!

Get full access to this lesson and the complete course when you enroll:

  • 🔓 Full lesson videos with no limits
  • 💻 Access to the final source code
  • 🎓 Course progress tracking & completion
  • 💬 Invite to our private Discord community
Unlock the Full Course – Just $85

One-time payment. Lifetime updates included.