reactjsthree.jsreact-three-fiberframer-motionreact-motion

How to animate a React Three Fiber "group" object with NEW framer motion?


I am building a 3D website with different pages. I am trying to make "prior" sections disappear when advancing to "further" ones by setting their y-position to -5 when not on that page.

I'm using React Three Fiber, so I've got my page elements in group objects. The default position is -5, but I am trying to add a variant that sets y:0 when the scroll is on that page.

The issue is, I'm not sure how to make the R3F group component, a motion component. With the previous version of framer-motion, we could animate a group in R3F by importing motion from framer-motion-3d, then changing any group to motion.group. It seems with the new "Motion" we are restricted to adding motion to DOM elements only? How would I animate an R3F group object directly, (rather than say, a "div"), if it is still possible?

I tried adding motion from react/motion, and making a motion.group, but it would not accept non-DOM elements. I also tried turning my groups into divs, but HTML was not allowed within the R3F Canvas.

This is my current Experience.jsx code, which I import onto the canvas in a separate file:

The groups need motion somehow, so that I can add the variants.

// Distance along z-axis between sections (as char walks fwd/backward)
const SECTION_DISTANCE = 10;

// Variants for animating sections (hiding/unhiding)
const variants = {
  home: { y: 0 },
};

export const Experience = () => {
  // State machine for revealing/hiding sections (starting at home page)
  const [section, setSection] = useState(config.sections[0]);

  const sceneContainer = useRef();
  const scrollData = useScroll();

  // Animate sceneContainer group to move through different sections
  useFrame(() => {
    sceneContainer.current.position.z =
      -scrollData.offset * SECTION_DISTANCE * (scrollData.pages - 1);
    // This moves groups in -z dir (towards camera) to sim camera moving backward (in +z dir)

    // Get current section number (current state), and acquire title from config
    setSection(
      config.sections[Math.round(scrollData.offset * (scrollData.pages - 1))]
    );
  });
  console.log(section);

  return (
    <>
      <Environment preset="sunset" />
      <Avatar />
      {/* Group containing different website sections; must match array defined in config.js */}
      <group ref={sceneContainer} animate={section}>
        {/* HOME */}
        <group variants={variants} position-y={-5}>
          <Star
            position-x={-0.009}
            position-z={0}
            position-y={1.97}
            scale={0.3}
          />
          <Float floatIntensity={2} speed={2}>
            <MacBookPro
              position-x={-1}
              position-y={0.5}
              position-z={0}
              scale={0.3}
              rotation-y={Math.PI / 4}
            />
          </Float>
          <PalmTree
            scale={0.018}
            rotation-y={THREE.MathUtils.degToRad(140)}
            position={[4, 0, -5]}
          />
          <Float
            floatIntensity={0.4}
            rotationIntensity={0.2}
            speed={2}
            floatingRange={[-0.05, 0.05]}
          >
            <Center disableY disableZ>
              <SectionTitle
                size={0.8}
                position-x={2}
                position-y={1.6}
                position-z={-3}
                bevelEnabled
                bevelThickness={0.3}
              >
                {config.home.title}
              </SectionTitle>
            </Center>
          </Float>
          <Center disableY disableZ>
            <SectionTitle
              size={1.2}
              position-x={5}
              position-z={-3}
              bevelEnabled
              bevelThickness={0.3}
              rotation-y={Math.PI / 10}
            >
              {config.home.subtitle}
            </SectionTitle>
          </Center>
        </group>

        {/* SKILLS */}
        <group position-z={SECTION_DISTANCE}>
          <group position-x={-2}>
            <SectionTitle position-x={0.4}>SKILLS</SectionTitle>
            <BookCase position-z={-2} />
            <CouchSmall
              scale={0.4}
              position-z={0}
              position-x={-0.2}
              rotation-y={Math.PI / 3}
            />
            <Lamp
              position-z={0.6}
              position-x={-0.4}
              position-y={-0.8}
              rotation-y={-Math.PI}
            />
          </group>
        </group>

        {/* CONTACT */}
        <group position-z={SECTION_DISTANCE * 4}>
          <SectionTitle position-x={0.4}>CONTACT</SectionTitle>
        </group>
      </group>
    </>
  );
};

Solution

  • you can use "useSpring" for 3ds instead of using framer-motion.

    import { useState, useRef } from "react";
    import { useFrame } from "@react-three/fiber";
    import { useSpring, a } from "@react-spring/three";
    import { Environment } from "@react-three/drei";
    
    const SECTION_DISTANCE = 10;
    
    export const Experience = () => {
      const [section, setSection] = useState(config.sections[0]);
      const sceneContainer = useRef();
      const scrollData = useScroll();
    
      // Create spring animation for smooth transitions
      const { z } = useSpring({
        z: -scrollData.offset * SECTION_DISTANCE * (scrollData.pages - 1),
        config: { mass: 1, tension: 170, friction: 26 },
      });
    
      useFrame(() => {
        // Update section based on scroll
        setSection(
          config.sections[Math.round(scrollData.offset * (scrollData.pages - 1))]
        );
      });
    
      return (
        <>
          <Environment preset="sunset" />
          <Avatar />
          {/* Animated group containing website sections */}
          <a.group ref={sceneContainer} position-z={z}>
            {/* HOME */}
            <group position-y={-5}>
              <Star position-x={-0.009} position-z={0} position-y={1.97} scale={0.3} />
              <Float floatIntensity={2} speed={2}>
                <MacBookPro
                  position-x={-1}
                  position-y={0.5}
                  position-z={0}
                  scale={0.3}
                  rotation-y={Math.PI / 4}
                />
              </Float>
              <PalmTree
                scale={0.018}
                rotation-y={THREE.MathUtils.degToRad(140)}
                position={[4, 0, -5]}
              />
              <Float floatIntensity={0.4} rotationIntensity={0.2} speed={2} floatingRange={[-0.05, 0.05]}>
                <Center disableY disableZ>
                  <SectionTitle
                    size={0.8}
                    position-x={2}
                    position-y={1.6}
                    position-z={-3}
                    bevelEnabled
                    bevelThickness={0.3}
                  >
                    {config.home.title}
                  </SectionTitle>
                </Center>
              </Float>
              <Center disableY disableZ>
                <SectionTitle
                  size={1.2}
                  position-x={5}
                  position-z={-3}
                  bevelEnabled
                  bevelThickness={0.3}
                  rotation-y={Math.PI / 10}
                >
                  {config.home.subtitle}
                </SectionTitle>
              </Center>
            </group>
    
            {/* SKILLS */}
            <group position-z={SECTION_DISTANCE}>
              <group position-x={-2}>
                <SectionTitle position-x={0.4}>SKILLS</SectionTitle>
                <BookCase position-z={-2} />
                <CouchSmall scale={0.4} position-z={0} position-x={-0.2} rotation-y={Math.PI / 3} />
                <Lamp position-z={0.6} position-x={-0.4} position-y={-0.8} rotation-y={-Math.PI} />
              </group>
            </group>
    
            {/* CONTACT */}
            <group position-z={SECTION_DISTANCE * 4}>
              <SectionTitle position-x={0.4}>CONTACT</SectionTitle>
            </group>
          </a.group>
        </>
      );
    };