春天来了,来生成一棵独属自己的花树吧!

春天来了,来生成一棵独属自己的花树吧!

春天来了,又到了姹紫嫣红开遍的季节,闲来无事尝试复刻写了一个赛博花树生成器,来看看吧。

  • 在线体验地址(做了移动适配的):点我访问。

  • 效果演示:

实现原理

相信大家肯定一眼就看出来了,主体的花树是使用canvas生成的,枝丫是随机生成的。下面就一点一点和大家分享一下具体是怎么写的~

基本页面搭建

这部分简单过一下,整体结构非常简单,canvas元素居中放置,然后设置一下border-radius50%就可以得到一个圆形的canvas了,但是需要注意的是,这个圆形仅仅是视觉效果,实际上canvas在使用时还是一个方形的。

canvas相关流程

设置尺寸

需要设置canvas尺寸,保证其像素尺寸与CSS尺寸一致,同时需要与设备像素比相乘,防止canvas模糊。

js 复制代码
// 1. 确保Canvas的像素尺寸与CSS尺寸一致
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
设置canvas背景色与底部陆地色块

使用fillStylefillRect设置即可:

js 复制代码
// 2. 设置纯色背景与陆地
ctx.fillStyle = "rgb(147,181,173)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "rgb(37,77,67)";
ctx.fillRect(0, canvas.height - 100, canvas.width, 100);
调整坐标系

canvas的默认坐标系和绝对定位的坐标系是保持一致的,即:坐标原点在左上角,向右为x轴正方向,向下为y轴正方向。可以看到,我们的树其实是位于整个canvas元素的正中间的,所以,为了整体的方便,这时需要把坐标轴进行调整,将x轴位置移到底部,y轴位置移到中间,同时正方向进行反向:

通过canvastranslate方法和scale方法进行实现:

js 复制代码
// 3. 设置坐标系:原点在Canvas中心底部,Y轴向上 填充陆地
ctx.translate(canvas.width / 2, canvas.height);
ctx.scale(1, -1);
花树生成逻辑

整体是通过递归去实现的,类似二叉树的生成过程,只是在生成时调整了树枝的粗细与长度,越生成越细,越短,分别递归去生成其左右子树枝,直到到达临界条件,递归停止。

初始起点位于调整后坐标系的原点向上位置(位于陆地之上),角度为90°,即垂直生成树干。整体的伪代码如下:

js 复制代码
// 花树参数
const flowerColors = ["#ff4400", "#ffffff", "#ff6699", "#ffecf1", "#ff8cb0"]; // 花朵颜色数组,随机选择
const initialBranchLength = 120; // 初始枝条长度
const initialBranchThick = 20; // 初始枝条粗细
const endBranchThick = 2; // 递归结束条件:枝条粗细
const possibleEndBranchThick = 8; // 防止树枝生成的太规整茂密,可以进行随机剪枝的临界条件
const branchColor = "rgb(48,41,30)"; // 枝条颜色

/**
 * 画树
 * @param {number[]} v0 线段起点
 * @param {number} length 线段长度
 * @param {number} thick 线段粗细
 * @param {number} deg 角度
 * @param {string} branchColor 树枝颜色
 * @param {string[]} flowerColors 花对应的颜色数组
 * @param {number} generateTimes 递归次数,防止初始就进行剪枝,保底生成一下枝丫
 */
function drawBranch(
    v0,
    length,
    thick,
    deg,
    branchColor,
    flowerColors,
    generateTimes
) {
    if (终止条件(可以是树枝粗细小于某个值,或是长短小于某个值)) {
        生成花朵;
        return;
    }
    计算线段终点,绘制线段;

    // 递归生成左子树
    drawBranch(
        v1,
        length * (Math.random() * 0.1 + 0.75), // 树枝长度随机减小
        thick * (Math.random() * 0.1 + 0.75), // 树枝粗细随机减小
        deg + (Math.random() * 30 + 5), // 角度增大,即向左偏移
        branchColor,
        flowerColors,
        generateTimes + 1
    );
    // 递归生成右子树
    drawBranch(
        v1,
        length * (Math.random() * 0.1 + 0.75), // 树枝长度随机减小
        thick * (Math.random() * 0.1 + 0.75), // 树枝粗细随机减小
        deg - (Math.random() * 30 + 5), // 角度减小,即向右偏移
        branchColor,
        flowerColors,
        generateTimes + 1
    );
}

drawBranch(// 初始调用
	[0, 50],
	initialBranchLength,
	initialBranchThick,
	90,
	branchColor,
	flowerColors,
	1
);

树枝的本质其实是一段线段,生成树枝其实就是在上一条线段的终点位置,以这个终点作为新线段的起点,随机幅度调整线段的粗细和长短,去生成新的线段。下面是计算线段终点,绘制线段部分的代码:

js 复制代码
ctx.beginPath();
ctx.moveTo(...v0); // 画笔移动到起点
const v1 = [ // 三角函数计算终点xy坐标
    v0[0] + length * Math.cos((deg * Math.PI) / 180),
    v0[1] + length * Math.sin((deg * Math.PI) / 180),
];
ctx.lineTo(...v1);
ctx.lineWidth = thick;
ctx.lineCap = "round";
ctx.strokeStyle = branchColor;
ctx.stroke();

由于是递归调用,需要给出终止条件,也就是树梢。在树梢可以绘制花朵,花朵绘制很简单,这里是采用了绘制一个纯色的圆形,圆形颜色从给出的颜色数组中进行配置:

js 复制代码
if (thick < endBranchThick) {
    ctx.beginPath();
    ctx.arc(v0[0], v0[1], 5, 0, Math.PI * 2);
    ctx.fillStyle =
            flowerColors[Math.floor(Math.random() * flowerColors.length)];
    ctx.fill();
    return;
}

同时,为了防止最终生成的树枝太过于规整和茂密,又给出了可以进行随机剪枝的阈值,也是线条的粗细来进行处理:

js 复制代码
if (thick < possibleEndBranchThick && Math.random() < 0.4) return;

也就是当线条粗细缩减到endBranchThick(此处设置为2)后,不再生成新枝条,生成结束,缩减到possibleEndBranchThick(此处设置为8)后,有40%的概率会提前中断该分支的剩余枝条的生成。

同时,后面进行递归生成左右子树时,也设置了相关的概率,提前中断子树的生成,但是为了保证初始生成的顺利,引入迭代次数进行了限制:

js 复制代码
if (generateTimes <= 2 || Math.random() > 0.05) {
    drawBranch(
        v1,
        length * (Math.random() * 0.1 + 0.75),
        thick * (Math.random() * 0.1 + 0.75),
        deg + (Math.random() * 30 + 5),
        branchColor,
        flowerColors,
        generateTimes + 1
    );
}

if (generateTimes <= 2 || Math.random() > 0.05) {
    drawBranch(
        v1,
        length * (Math.random() * 0.1 + 0.75),
        thick * (Math.random() * 0.1 + 0.75),
        deg - (Math.random() * 30 + 5),
        branchColor,
        flowerColors,
        generateTimes + 1
    );
}

完整代码如下:

js 复制代码
// 花树参数
const flowerColors = ["#ff4400", "#ffffff", "#ff6699", "#ffecf1", "#ff8cb0"];
const initialBranchLength = 120;
const initialBranchThick = 20;
const endBranchThick = 2;
const possibleEndBranchThick = 8;
const branchColor = "rgb(48,41,30)";

/**
 * 画树
 * @param {number[]} v0 线段起点
 * @param {number} length 线段长度
 * @param {number} thick 线段粗细
 * @param {number} deg 角度
 * @param {string} branchColor 树枝颜色
 * @param {string[]} flowerColors 花对应的颜色数组
 * @param {number} generateTimes 递归次数,保底生成一下树枝丫
 */
function drawBranch(
	v0,
	length,
	thick,
	deg,
	branchColor,
	flowerColors,
	generateTimes
) {
	if (thick < endBranchThick) {
		ctx.beginPath();
		ctx.arc(v0[0], v0[1], 5, 0, Math.PI * 2);
		ctx.fillStyle =
			flowerColors[Math.floor(Math.random() * flowerColors.length)];
		ctx.fill();
		return;
	}
	if (thick < possibleEndBranchThick && Math.random() < 0.4) return;

	ctx.beginPath();
	ctx.moveTo(...v0);
	const v1 = [
		v0[0] + length * Math.cos((deg * Math.PI) / 180),
		v0[1] + length * Math.sin((deg * Math.PI) / 180),
	];
	ctx.lineTo(...v1);
	ctx.lineWidth = thick;
	ctx.lineCap = "round";
	ctx.strokeStyle = branchColor;
	ctx.stroke();

	if (generateTimes <= 2 || Math.random() > 0.05) {
            drawBranch(
                v1,
                length * (Math.random() * 0.1 + 0.75),
                thick * (Math.random() * 0.1 + 0.75),
                deg + (Math.random() * 30 + 5),
                branchColor,
                flowerColors,
                generateTimes + 1
            );
	}

	if (generateTimes <= 2 || Math.random() > 0.05) {
            drawBranch(
                v1,
                length * (Math.random() * 0.1 + 0.75),
                thick * (Math.random() * 0.1 + 0.75),
                deg - (Math.random() * 30 + 5),
                branchColor,
                flowerColors,
                generateTimes + 1
            );
	}
}
重绘制&保存图像逻辑

重绘制比较简单,从头走一遍canvas的配置逻辑就好了:

js 复制代码
regenerateButton.addEventListener("click", () => {
	// 重置坐标系并重新绘制
	ctx.setTransform(1, 0, 0, 1, 0, 0); // 重置所有变换
	ctx.clearRect(0, 0, canvas.width, canvas.height);
	ctx.fillStyle = "rgb(147,181,173)";
	ctx.fillRect(0, 0, canvas.width, canvas.height);
	ctx.fillStyle = "rgb(37,77,67)";
	ctx.fillRect(0, canvas.height - 100, canvas.width, 100);
	ctx.translate(canvas.width / 2, canvas.height);
	ctx.scale(1, -1);
	drawBranch(
		[0, 50],
		initialBranchLength,
		initialBranchThick,
		90,
		branchColor,
		flowerColors,
		1
	);
});

保存图像这里,因为最终想要将图像和视觉效果一致,也就是图像需要是一个圆形的,又单独裁切了一下:

js 复制代码
saveButton.addEventListener("click", () => {
  // 创建一个临时的Canvas元素用于处理圆形裁剪
  var tempCanvas = document.createElement('canvas');
  var tempCtx = tempCanvas.getContext('2d');
  
  var centerX = canvas.width / 2;
  var centerY = canvas.height / 2;
  var radius = Math.min(centerX, centerY); // 圆的半径
  
  // 设置临时Canvas的尺寸与原始Canvas相同
  tempCanvas.width = canvas.width;
  tempCanvas.height = canvas.height;

  // 在临时Canvas上开始圆形裁剪区域
  tempCtx.beginPath();
  tempCtx.arc(centerX, centerY, radius, 0, Math.PI * 2);
  tempCtx.clip();

  // 将原始Canvas的内容绘制到临时Canvas上
  tempCtx.drawImage(canvas, 0, 0);

  // 使用临时Canvas的数据URL来下载图片
  const dataURL = tempCanvas.toDataURL("image/png");
  const link = document.createElement("a");
  link.style.display = "none";
  link.href = dataURL;
  link.download = "your-flower-tree.png";
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
});

这样,完成了整体的效果~

相关推荐
申朝先生4 分钟前
JS:什么是闭包,以及它的应用场景和缺点是什么?
开发语言·javascript·ecmascript
念九_ysl7 分钟前
暴力搜索算法详解与TypeScript实战
javascript·算法
beibeibeiooo28 分钟前
【CSS3】02-选择器 + CSS特性 + 背景属性 + 显示模式
前端·css·css3
uhakadotcom1 小时前
Meta Horizon OS 开发工具:打造更好的 MR/VR 体验
javascript·后端·面试
helbyYoung1 小时前
【零基础JavaScript入门 | Day7】三大交互案例深度解析|从DOM操作到组件化开发
开发语言·javascript
uhakadotcom2 小时前
刚刚发布的React 19.1提供了什么新能力?
前端·javascript·面试
uhakadotcom2 小时前
Expo 简介:跨平台移动应用开发的强大工具
前端·javascript·面试
markzzw2 小时前
浏览器插件钱包(一) - 区块链世界的入口
前端·web3·区块链
夕水2 小时前
终于,我也能够写出一款代码编辑器
前端
red润2 小时前
npm包autocannon牛逼的后台压力测试库
前端·javascript·node.js