效果介绍
使用 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 }}
/>
);
};