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

效果介绍

使用 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 }}
    />
  );
};

完整代码

生长的树木和花朵

相关推荐
星就前端叭37 分钟前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
m0_7482345240 分钟前
前端Vue3字体优化三部曲(webFont、font-spider、spa-font-spider-webpack-plugin)
前端·webpack·node.js
Web阿成41 分钟前
3.学习webpack配置 尝试打包ts文件
前端·学习·webpack·typescript
jwensh2 小时前
【Jenkins】Declarative和Scripted两种脚本模式有什么具体的区别
运维·前端·jenkins
关你西红柿子2 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
益达是我2 小时前
【Chrome】浏览器提示警告Chrome is moving towards a new experience
前端·chrome
济南小草根2 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js
聪小陈2 小时前
圣诞节:记一次掘友让我感动的时刻
前端·程序员
LUwantAC2 小时前
CSS(一):选择器
前端·css
Web阿成2 小时前
5.学习webpack配置 babel基本配置
前端·学习·webpack