- Published on
Canvas随机生成树
- Authors
- Name
- Tian Haipeng
第一步:设置坐标系
const canvas = document.getElementById('treeCanvas');
const ctx = canvas.getContext('2d');
建立坐标系统:
- Canvas坐标系:左上角(0,0),向右为X轴正方向,向下为Y轴正方向
- 画布大小:800px × 600px
- 树的起始位置:画布底部中央
(400, 550)
为什么要先考虑坐标系: 所有的绘制都基于坐标计算,搞清楚坐标系是绘图的基础。
第二步:确定drawBranch方法要接收哪些参数
思考:要画一根树枝,我们需要知道什么信息?
function drawBranch(x, y, length, angle, thickness) {
// x, y: 起点坐标 - 这根树枝从哪里开始
// length: 分支的长度 - 这根树枝有多长
// angle: 分支的角度 - 这根树枝朝哪个方向长
// thickness: 分支的粗细 - 这根树枝有多粗
}
参数设计思考:
- 起点坐标(x,y) - 必须知道从哪里开始画
- 长度(length) - 决定树枝的大小
- 角度(angle) - 决定树枝的方向
- 粗细(thickness) - 决定树枝的视觉效果
第三步:首先画主干
function drawMainTrunk() {
const startX = canvas.width / 2; // 画布中央
const startY = canvas.height - 50; // 距离底部50px
// 设置主干样式
ctx.strokeStyle = '#8B4513'; // 棕色
ctx.lineWidth = 8; // 粗线
ctx.lineCap = 'round'; // 圆形端点
// 绘制主干
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(startX, startY - 60); // 向上画60px
ctx.stroke();
return {
endX: startX,
endY: startY - 60 // 主干的终点,也是第一根分支的起点
};
}
为什么先画主干:
- 主干是固定的,不需要复杂计算
- 主干的终点是分支的起点
- 建立整棵树的基础结构
第四步:已知起点、长度、角度,如何算出终点
这是关键的数学计算!
// 已知条件:
const startX = 400; // 起点X坐标
const startY = 490; // 起点Y坐标
const length = 80; // 分支长度
const angle = -Math.PI / 4; // 分支角度(-45度,向左上)
// 计算终点:
const endX = startX + Math.cos(angle) * length;
const endY = startY + Math.sin(angle) * length;
数学原理图解:
终点(endX, endY)
↗
/
/ length
/
/ angle
起点(startX, startY)
水平位移 = length × cos(angle)
垂直位移 = length × sin(angle)
终点X = 起点X + 水平位移
终点Y = 起点Y + 垂直位移
角度说明:
- 0度:向右
- -Math.PI/2 (-90度):向上
- Math.PI/2 (90度):向下
- Math.PI (180度):向左
第五步:画线段连接到终点
function drawBranchLine(startX, startY, endX, endY, thickness, color) {
// 设置线条样式
ctx.strokeStyle = color;
ctx.lineWidth = thickness;
ctx.lineCap = 'round';
// 绘制线段
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.stroke();
}
绘制过程:
- 设置线条颜色和粗细
- 开始新路径
- 移动到起点
- 画线到终点
- 执行绘制
第六步:画出左分支和右分支
function drawBranch(x, y, length, angle, thickness, depth) {
// 计算当前分支的终点
const endX = x + Math.cos(angle) * length;
const endY = y + Math.sin(angle) * length;
// 绘制当前分支
drawBranchLine(x, y, endX, endY, thickness, getColor(depth));
// 生成左分支
const leftAngle = angle - Math.PI / 6; // 向左偏转30度
const leftLength = length * 0.7; // 长度缩短到70%
const leftThickness = thickness * 0.8; // 粗细减少到80%
// 生成右分支
const rightAngle = angle + Math.PI / 6; // 向右偏转30度
const rightLength = length * 0.7;
const rightThickness = thickness * 0.8;
// 递归绘制子分支(下一步会加递归控制)
drawBranch(endX, endY, leftLength, leftAngle, leftThickness, depth - 1);
drawBranch(endX, endY, rightLength, rightAngle, rightThickness, depth - 1);
}
分支生成策略:
- 左分支向左偏转30度
- 右分支向右偏转30度
- 子分支长度是父分支的70%
- 子分支粗细是父分支的80%
第七步:防止无限递归
function drawBranch(x, y, length, angle, thickness, depth) {
// 递归终止条件
if (depth <= 0 || length < 5) {
return; // 停止递归
}
// 计算终点
const endX = x + Math.cos(angle) * length;
const endY = y + Math.sin(angle) * length;
// 绘制当前分支
drawBranchLine(x, y, endX, endY, thickness, getColor(depth));
// 添加随机性,让树更自然
const numBranches = Math.random() > 0.3 ? 2 : 3; // 70%概率2分支,30%概率3分支
for (let i = 0; i < numBranches; i++) {
// 随机角度偏移
const angleOffset = (Math.random() - 0.5) * Math.PI / 3; // ±60度范围
const newAngle = angle + angleOffset;
// 随机长度缩放
const lengthScale = 0.6 + Math.random() * 0.3; // 60%-90%
const newLength = length * lengthScale;
const newThickness = thickness * 0.8;
// 异步绘制,创造生长动画
setTimeout(() => {
drawBranch(endX, endY, newLength, newAngle, newThickness, depth - 1);
}, Math.random() * 100);
}
}
防止无限递归的方法:
- 深度控制:
depth <= 0时停止 - 长度控制:
length < 5时停止(太小没必要画) - 深度递减:每次递归
depth - 1
第八步:画一些花(叶子)
function maybeDrawLeaf(x, y, depth) {
// 只在细分支上画叶子
if (depth <= 2 && Math.random() > 0.6) {
drawLeaf(x, y);
}
}
function drawLeaf(x, y) {
// 随机叶子颜色(绿色系)
const hue = 100 + Math.random() * 40; // 100°-140°绿色
ctx.fillStyle = `hsl(${hue}, 70%, 45%)`;
// 随机叶子大小
const size = 2 + Math.random() * 3;
// 画圆形叶子
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
// 在drawBranch函数末尾添加
function drawBranch(x, y, length, angle, thickness, depth) {
// ... 前面的代码 ...
// 在分支末端可能添加叶子
maybeDrawLeaf(endX, endY, depth);
}
叶子绘制策略:
- 只在细分支(depth ≤ 2)上画叶子
- 40%的概率在分支末端添加叶子
- 叶子颜色:绿色系随机变化
- 叶子大小:2-5像素的圆形
第九步:启动
function generateTree() {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 画主干
const trunk = drawMainTrunk();
// 开始递归绘制分支
const initialAngle = -Math.PI / 2; // 向上
const initialLength = 100;
const initialThickness = 6;
const maxDepth = 8;
drawBranch(trunk.endX, trunk.endY, initialLength, initialAngle, initialThickness, maxDepth);
}
// 页面加载后生成第一棵树
generateTree();
最后让Claude美化一下样式 就完成啦
在线演示
下面是一个使用 Canvas 生成随机树的在线演示,点击"生成新树"按钮可以创建不同的树形图案。