仿树木生长开花的动画效果

效果介绍

使用 canvas 进行绘制树木生长的效果,会逐渐长出树干,长出树叶,开出花。当窗口大小发生变化时进行重新渲染。

实现效果展示

实现步骤

创建画布

tsx 复制代码
import React, { useEffect, useRef } from 'react'

function TreeCanvas(props: {
  width: number;
  height: number;
}) {
  const { width = 400, height = 400 } = props;
  const canvasRef = useRef<HTMLCanvasElement>(null);
  useEffect(() => {
    const canvas = canvasRef.current;
    const context = canvas?.getContext('2d');
    if (!canvas || !context) return;
    context.strokeStyle = '#a06249';
  }, [])
  return (
    <canvas ref={canvasRef} width={width} height={height} />
  )
}

export default TreeCanvas

封装创建树枝的方法

  • 树枝需要起点,终点,树枝宽度
tsx 复制代码
 function lineTo(p1: PointType, p2: PointType, lineWidth: number) {
      context?.beginPath();
      context?.moveTo(p1.x, p1.y);
      context?.lineTo(p2.x, p2.y);
      context.lineWidth = lineWidth;
      context?.stroke();
}

绘制树叶和花朵的方法封装

  • 提前生成图片实例
  • 传递图片和坐标进行绘制
tsx 复制代码
   // 花的实例
    const image = new Image();
    image.src ='https://i.postimg.cc/D0LLWwKy/flower1.png';
   // 叶子的实例
    const imageLeaves = new Image();
    imageLeaves.src = 'https://i.postimg.cc/PJShQmH6/leaves.png';

    function drawTmg(imageUrl: any, p1: PointType) {
      context?.drawImage(imageUrl, p1.x, p1.y, 20 * Math.random(), 20 * Math.random());
    }

封装绘制处理

  • 提供绘制的起点,计算绘制的终点
  • 根据起点和终点进行绘制
scss 复制代码
// 计算终点
  function getEnd(b: BranchType) {
      const { start, theta, length } = b;
      return {
        x: start.x + Math.cos(theta) * length,
        y: start.y + Math.sin(theta) * length
      };
}
// 绘制整理
function drawBranch(b: BranchType) {
      // 绘制树干
      lineTo(b.start, getEnd(b), b.lineWidth);
      if (Math.random() < 0.4) { // 绘制花朵的密度
        drawTmg(image, getEnd(b));
      }
      if (Math.random() < 0.4) {
        drawTmg(imageLeaves, b.start);  // 绘制树叶的密度
      }
    }

绘制树的方法

  • 起点和终点的计算及绘制数的角度计算
  • 绘制左边树和右边树
  • 随机绘制
tsx 复制代码
function step(b: BranchType, depth: number = 0) {
      const end = getEnd(b);
      drawBranch(b);
      if (depth < depthNum || Math.random() < 0.5) {
          step(
            {
              start: end,
              theta: b.theta - 0.3 * Math.random(),
              length: b.length + (Math.random() * 10 - 5),
              lineWidth: depthNum - depth
            },
            depth + 1
          );
      }
      if (depth < depthNum || Math.random() < 0.5) {
          step(
            {
              start: end,
              theta: b.theta + 0.3 * Math.random(),
              length: b.length + (Math.random() * 10 - 5),
              lineWidth: depthNum - depth
            },
            depth + 1
          );
      }
    }

动画处理

  • 把所有绘制添加到动画处理中
tsx 复制代码
const pendingTasks: Function[] = []; // 动画数组
    function step(b: BranchType, depth: number = 0) {
      const end = getEnd(b);
      drawBranch(b);
      if (depth < depthNum || Math.random() < 0.5) {
        pendingTasks.push(() => { // 添加左侧动画
          step(
            {
              start: end,
              theta: b.theta - 0.3 * Math.random(), // 角度变化
              length: b.length + (Math.random() * 10 - 5), // 长度变化
              lineWidth: depthNum - depth
            },
            depth + 1
          );
        });
      }
      if (depth < depthNum || Math.random() < 0.5) {
        pendingTasks.push(() => { // 添加右侧动画
          step(
            {
              start: end,
              theta: b.theta + 0.3 * Math.random(), // 角度变化
              length: b.length + (Math.random() * 10 - 5),// 长度变化
              lineWidth: depthNum - depth
            },
            depth + 1
          );
        });
      }
    }
    function frame() {
      const tasks = [...pendingTasks];
      pendingTasks.length = 0;
      tasks.forEach((fn) => fn());
    }
    let framesCount = 0;
    function satrtFrame() {
      requestAnimationFrame(() => {
        framesCount += 1;
        // if (framesCount % 10 === 0) {
        frame();
        satrtFrame();
        // }
      });
    }

封装执行方法

tsx 复制代码
useEffect(() => {
    function init() {
      step(startBranch);
    }
    satrtFrame();
    init();
  }, []);

添加常用场景封装

  • 宽高获取当前屏幕大小
  • 屏幕发生变化时进行重新渲染
tsx 复制代码
export const TreeCanvasInner = () => {
  const [innerSize, setInnerSize] = useState({ x: window.innerWidth, y: window.innerHeight });
  useEffect(() => {
    const resizeFunc = () => {
      setInnerSize({ x: window.innerWidth, y: window.innerHeight });
    };
    window.addEventListener('resize', resizeFunc);
    return () => {
      window.removeEventListener('resize', resizeFunc);
    };
  }, []);
  return (
    <TreeCanvas
      key={JSON.stringify(innerSize)}
      width={innerSize.x}
      height={innerSize.y}
      startBranch={{ start: { x: 0, y: 0 }, theta: 20, length: 25, lineWidth: 3 }}
    />
  );
};

完整代码

生长的树木和花朵

相关推荐
小小小小宇2 小时前
TS泛型笔记
前端
小小小小宇3 小时前
前端canvas手动实现复杂动画示例
前端
codingandsleeping3 小时前
重读《你不知道的JavaScript》(上)- 作用域和闭包
前端·javascript
小小小小宇3 小时前
前端PerformanceObserver使用
前端
zhangxingchao4 小时前
Flutter中的页面跳转
前端
烛阴5 小时前
Puppeteer入门指南:掌控浏览器,开启自动化新时代
前端·javascript
全宝5 小时前
🖲️一行代码实现鼠标换肤
前端·css·html
小小小小宇6 小时前
前端模拟一个setTimeout
前端
萌萌哒草头将军6 小时前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js
芝士加7 小时前
Playwright vs MidScene:自动化工具“双雄”谁更适合你?
前端·javascript