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

效果介绍

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

完整代码

生长的树木和花朵

相关推荐
卸任7 小时前
Vibe coding工具使用心得
前端·ai编程
brien97 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance8 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M8 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写8 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李8 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易8 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马8 小时前
前沿视界
前端
kyriewen8 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易9 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员