实现一朵渐变且会旋转的花花

76 阅读4分钟

GIF 2024-10-14 17-24-54.gif

花朵的实现

  1. 搭建场景

    1. 创建变量

              const theme = useTheme();
              const { rgbBackground, themeId, colorWhite } = theme; 
              console.log(colorWhite, 'colorWhite');
              const start = useRef(Date.now());
              const mouse = useRef();
              const renderer = useRef();
              const camera = useRef();
              const scene = useRef();
              const lights = useRef();
              const uniforms = useRef();
              const material = useRef();
              const geometry = useRef();
              const sphere = useRef();
              const reduceMotion = useReducedMotion();
              const isInViewport = useInViewport(canvasRef);
              const windowSize = useWindowSize();
              // useSpring 用于创建和控制弹簧动画,允许定义动画的初始状态,目标状态和过渡配置
              // 返回一个包含动画值和控制函数的对象
              const rotationX = useSpring(0, springConfig);
              const rotationY = useSpring(0, springConfig);
2.  创建canvas对象作为webGLRenderer的canvas
  const canvasRef = useRef();
              return (    
            		  <canvas
                      aria-hidden
                      className={styles.canvas}
                      data-visible={visible}
                      ref={canvasRef}
                      {...props}
                    />)
3.  处理基础的渲染器 相机 场景 着色器 材质
  // 处理基础的渲染器 相机 场景 着色器 材质  
          useEffect(() => {
            const { innerWidth, innerHeight } = window;
            mouse.current = new Vector2(0.8, 0.5);
            renderer.current = new WebGLRenderer({
              canvas: canvasRef.current,
              antialias: false,//不要锯齿 提升性能
              alpha: true,//透明度
              powerPreference: 'high-performance',
              failIfMajorPerformanceCaveat: true,//性能有问题就不要初始化
            });
            renderer.current.setSize(innerWidth, innerHeight);
            renderer.current.setPixelRatio(1);//设置设备的像素
            renderer.current.outputEncoding = sRGBEncoding;//精确的颜色 防止颜色太暗或太亮

            // 参数分别为 水平方向的视野、纵横比、near面离相机的距离、far面离相机的距离
            camera.current = new PerspectiveCamera(54, innerWidth / innerHeight, 0.1, 100);
            camera.current.position.z = 52;

            scene.current = new Scene();

            //onBeforeCompile作用: 在编译着色器之前,允许开发者对着色器进行自定义修改,以满足特定的渲染需求
            material.current.onBeforeCompile = shader => {
              // 合并原始着色器的uniforms和自定义的uniforms,并赋值给uniforms
              uniforms.current = UniformsUtils.merge([
                shader.uniforms,
                { time: { type: 'f', value: 0 } },
              ]);
              shader.uniforms = uniforms.current;

              // 将自定义的顶点着色器和片段着色器分别赋值给shader的vertexShader和fragmentShader
              shader.vertexShader = vertShader;
              shader.fragmentShader = fragShader;
            };

          }, []);

  1. 创建花朵

    在useEffect中

    
        material.current = new MeshPhongMaterial();//材质类型
        
      		//HACK 真正创建花朵的地方 startTransition可以延迟更新,做渐进式渲染,确保浏览器可以更快响应用户交互,不会因为要加载模型而阻塞用户交互
        startTransition(() => {
          geometry.current = new SphereBufferGeometry(32, 128, 128);//创建一个球体 第一个参数是r 半径
          sphere.current = new Mesh(geometry.current, material.current);//将材质和几何体结合在一起
          sphere.current.position.z = 0;
          sphere.current.modifier = Math.random();
          scene.current.add(sphere.current);//往场景中添加球体 就可以和光、相机和其他对象交互
        });
    
  2. 优化性能1: useEffect卸载

      return () => {
          // 卸载时清除场景中的几何体,材质和渲染器
          cleanScene(scene.current);
          cleanRenderer(renderer.current);
        };
    
  3. 优化性能2:useInViewport自定义hook检查元素是不是在视口 如果是才开启动画 才让花朵随着鼠标移动

 import { useEffect, useState } from 'react';

        // 检测元素是否在视口中可见
        export function useInViewport(
          elementRef,
          unobserveOnIntersect,
          options = {},
          shouldObserve = true
        ) {
          const [intersect, setIntersect] = useState(false);
          const [isUnobserved, setIsUnobserved] = useState(false);

          useEffect(() => {
            if (!elementRef?.current) return;

            // IntersectionObserver 的主要作用是提供一种有效的方式来检测元素是否进入视口或离开视口,
            // 并在相应的交叉状态发生变化时触发回调函数
            // 它在实现可见性检测、懒加载、无限滚动等场景中非常有用。
            const observer = new IntersectionObserver(([entry]) => {
              //  isIntersecting表示元素是否进入视口
              //  target表示被观察的目标元素
              const { isIntersecting, target } = entry;

              setIntersect(isIntersecting);

              if (isIntersecting && unobserveOnIntersect) {
                observer.unobserve(target); //将指定的目标元素从观察列表中移除
                setIsUnobserved(true);
              }
            }, options);

            if (!isUnobserved && shouldObserve) {
              observer.observe(elementRef.current); ////将指定的目标元素添加到观察列表
            }

            return () => observer.disconnect(); //停止观察所有目标元素的交叉状态,并释放资源
          }, [elementRef, unobserveOnIntersect, options, isUnobserved, shouldObserve]);

          return intersect;
        }

花朵的动画

控制花随着鼠标的移动而做旋转 和 球体会绕着z轴转动

  1. 控制花随着鼠标的移动而做旋转
 useEffect(() => {
            const onMouseMove = event => {
              const position = {
                // 鼠标的坐标相对于创建窗口的位置
                x: event.clientX / window.innerWidth,
                y: event.clientY / window.innerHeight,
              };

              // rotationX表示元素绕x轴旋转角度,通过调用set方法可以更新rotationX的值,从而实现元素在x轴上的旋转
              rotationX.set(position.y / 2);
              rotationY.set(position.x / 2);
            };

            // reduceMotion == false表示未开启禁用动画
            if (!reduceMotion && isInViewport) {
              window.addEventListener('mousemove', onMouseMove);
            }

            return () => {
              window.removeEventListener('mousemove', onMouseMove);
            };
          }, [isInViewport, reduceMotion, rotationX, rotationY]);
  1. 鼠标不动的情况下 球体会绕着z轴转动
 useEffect(() => {
            let animation;

            const animate = () => {
              animation = requestAnimationFrame(animate);

              // uniforms是一种特殊的变量 用来将数据从JS运行的CPU传递给shader运行的GPU
              if (uniforms.current !== undefined) {
                uniforms.current.time.value = 0.00005 * (Date.now() - start.current);
              }

              // b.鼠标不动的情况下 球体会绕着z轴转动
              sphere.current.rotation.z += 0.001;
              sphere.current.rotation.x = rotationX.get();
              sphere.current.rotation.y = rotationY.get();

              renderer.current.render(scene.current, camera.current);
            };

            if (!reduceMotion && isInViewport) {
              animate();
            } else {
              renderer.current.render(scene.current, camera.current);
            }

            return () => {
              cancelAnimationFrame(animation);
            };
          }, [isInViewport, reduceMotion, rotationX, rotationY]);

处理光

 // 处理光
      useEffect(() => {
        const dirLight = new DirectionalLight(colorWhite, 0.6);

        const ambientLight = new AmbientLight(colorWhite, themeId === 'light' ? 0.8 : 0.2);

        dirLight.position.z = 200;
        dirLight.position.x = 100;
        dirLight.position.y = 100;

        lights.current = [dirLight, ambientLight];
        // 当使用 RGB 值或字符串值来创建 Color 对象时,颜色通道的值范围应该是 0 到 1,而不是常见的 0 到 255 范围
        // 所以调用rgbToThreeColor函数来转换
        scene.current.background = new Color(...rgbToThreeColor(rgbBackground)); //  rgbBackground: '17 17 17',

        lights.current.forEach(light => scene.current.add(light));

        return () => {
          removeLights(lights.current);
        };
      }, [rgbBackground, colorWhite, themeId]);

最后将canvas包裹在Transition 组件中 做组件进入和离开DOM时的动画效果

  return (
    <Transition in timeout={3000}>
      {visible => (
        <canvas
          aria-hidden//在一些浏览器(盲人的只听模式)隐藏canvas
          className={styles.canvas}
          data-visible={visible}
          ref={canvasRef}
          {...props}
        />
      )}
    </Transition>
  );