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

效果介绍

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

完整代码

生长的树木和花朵

相关推荐
MarisolHu19 分钟前
前端学习笔记-JS进阶篇-01
前端·javascript·笔记·学习
被迫学习Java1 小时前
前端工程化17-邂逅原生的ajax、跨域、JSONP
前端·ajax·okhttp
Justinc.2 小时前
CSS基础
前端·css
全栈练习生3 小时前
pnpm在monorepo架构下不能引用其他模块的问题
前端
qq_427506083 小时前
前端项目npm install报错解决的解决办法
前端·npm·node.js
CiL#7 小时前
css动态边框
前端·css
风清云淡_A7 小时前
uniapp中检测应用更新的两种方式-升级中心之uni-upgrade-center-app
前端·uni-app
Citrus_737 小时前
css的选择器及优先级
前端·css
ZL_5677 小时前
uniapp中实现评分组件,多用于购买商品后,对商品进行评价等场景
前端·javascript·uni-app
剑亦未配妥8 小时前
前端vue相关常见面试题,包含MVVM、双向绑定原理、性能优化、vue2和vue3性能对比等
前端