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

效果介绍

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

完整代码

生长的树木和花朵

相关推荐
anyup_前端梦工厂2 小时前
了解几个 HTML 标签属性,实现优化页面加载性能
前端·html
前端御书房2 小时前
前端PDF转图片技术调研实战指南:从踩坑到高可用方案的深度解析
前端·javascript
2301_789169542 小时前
angular中使用animation.css实现翻转展示卡片正反两面效果
前端·css·angular.js
风口上的猪20153 小时前
thingboard告警信息格式美化
java·服务器·前端
程序员黄同学3 小时前
请谈谈 Vue 中的响应式原理,如何实现?
前端·javascript·vue.js
爱编程的小庄4 小时前
web网络安全:SQL 注入攻击
前端·sql·web安全
宁波阿成5 小时前
vue3里组件的v-model:value与v-model的区别
前端·javascript·vue.js
柯腾啊5 小时前
VSCode 中使用 Snippets 设置常用代码块
开发语言·前端·javascript·ide·vscode·编辑器·代码片段
weixin_535854225 小时前
oppo,汤臣倍健,康冠科技,高途教育25届春招内推
c语言·前端·嵌入式硬件·硬件工程·求职招聘
扣丁梦想家5 小时前
设计模式教程:装饰器模式(Decorator Pattern)
java·前端·装饰器模式