太离谱了!产品要求我用 JavaScript 画一颗【随机树】

前言

大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~

用 JavaScript 画一棵树?

产品说要让前端用 JavaScript 画一棵树出来,但是这难道不能直接让 UI 给一张图片吗?

后来一问才知道,产品要的是一颗 随机树 ,也就是树的茂盛程度、长度、枝干粗细都是随机的,那这确实没办法叫 UI 给图,毕竟 UI 不可能给我 10000 张树的图片吧?

所以第一时间想到的就是 Canvas ,用它来画这棵随机树

Canvas 画一颗随机树

接下来使用 Canvas 去画这棵随机树

基础页面

我们需要在页面上写一个 canvas 标签,并设置好宽高,同时需要获取它的 Dom 节点、绘制上下文,以便后续的绘制

坐标调整

默认的 Canvas 坐标系是这样的

但是我们现在需要从中间去向上去画一棵树,所以坐标得调整成这样:

  • X 轴从最上面移动到最下面
  • Y 轴的方向由往下调整成往上 ,并且从最左边移动到画布中间

这些操作可以使用 Canvas 的方法

  • ctx.translate: 坐标系移动
  • ctx.scale: 坐标系缩放

绘制一棵树的要素

绘制一棵树的要素是什么呢?其实就是树枝果实 ,但是其实树枝 才是第一要素,那么树枝又有哪些要素呢?无非就这几个点

  • 起始点
  • 树枝长度、树枝粗细
  • 生长角度
  • 终点

开始绘制

所以我们可以写一个 drawBranch 来进行绘制,并且初始调用肯定是绘制树干,树干的参数如下:

  • 起始点:(0, 0)
  • 树枝长度、树枝粗细:这些可以自己自定义
  • 生长角度:90度
  • 终点:需要算

这个终点应该怎么算呢?其实很简单,根据树枝长度、生长角度就可以算出来了,这是初高中的知识

于是我们可以使用 Canvas 的绘制方法,去绘制线段 ,其实树枝就是一个一个的线段

到现在我绘制出了一个树干 出来

但是我们是想让这棵树开枝散叶,所以需要继续递归继续去绘制更多的树枝出来~

递归绘制

其实往哪开枝散叶呢?无非就是往左或者往右

所以需要递归画左边和右边的树枝,并且子树枝肯定要比父树枝更短、比父树枝更细,比如我们可以定义一个比例

  • 子树枝是父树枝长度的 0.8
  • 子树枝是父树枝粗细的 0.75

而子树枝的生长角度,其实可以随机,我们可以在 0° - 30° 之间随机选一个角度,于是增加了递归调用的代码

但是这个时候会发现,报错了,爆栈了,因为我们只递归开始,但却没有在某个时刻递归停止

我们可以自己定义一个停止规则(规则可以自己定义,这会决定你这棵树的茂盛程度):

  • 粗细小于 2 时马上停止
  • (粗细小于 10 时 + 随机数)决定是否停止

现在可以看到我们已经大致绘制出一棵树了

不过还少了树的果实

绘制果实

绘制果实很简单,只需要在绘制树枝结束的时候,去把果实绘制出来就行,其实果实就是一个个的白色实心圆

至此这棵树完整绘制完毕

绘制部分的代码如下

结语 & 加学习群 & 摸鱼群

我是林三心

  • 一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手;
  • 一个偏前端的全干工程师;
  • 一个不正经的掘金作者;
  • 一个逗比的B站up主;
  • 一个不帅的小红书博主;
  • 一个喜欢打铁的篮球菜鸟;
  • 一个喜欢历史的乏味少年;
  • 一个喜欢rap的五音不全弱鸡

如果你想一起学习前端,一起摸鱼,一起研究简历优化,一起研究面试进步,一起交流历史音乐篮球rap,可以来俺的摸鱼学习群哈哈,点这个,有7000多名前端小伙伴在等着一起学习哦 --> 摸鱼沸点

完整代码

html 复制代码
<template>
  <div style="background-color: cadetblue">
    <canvas ref="canvasRef" width="1000" height="750"></canvas>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';

// 获取 canvas 的 dom 节点
const canvasRef = ref<HTMLCanvasElement | null>(null);

onMounted(() => {
  const canvasEle = canvasRef.value;
  if (!canvasEle) return;

  // 获取 canvas 上下文
  const ctx = canvasEle.getContext('2d')!;
  // 坐标系移动
  ctx.translate(canvasEle.width / 2, canvasEle.height);
  // y轴反向
  ctx.scale(1, -1);

  //   coordinate 起始点
  //   len 树枝长度
  //   thick 树枝粗细
  //   angle 生长角度
  const drawBranch = (coordinate: [number, number], len: number, thick: number, angle: number) => {
    // 绘制结束条件
    if (thick < 10 && Math.random() < 0.1) return;
    if (thick < 2) {
      // 绘制果实
      ctx.beginPath();
      ctx.arc(...coordinate, 5, 0, 2 * Math.PI);
      ctx.fillStyle = '#fff';
      ctx.fill();
      return;
    }

    ctx.beginPath(); // 开启线段绘制
    ctx.moveTo(...coordinate); // 初始起始点

    // 计算结束点
    const endCoordinate: [number, number] = [
      coordinate[0] + len * Math.cos((angle * Math.PI) / 180),
      coordinate[1] + len * Math.sin((angle * Math.PI) / 180),
    ];

    ctx.lineTo(...endCoordinate); // 线段终点
    ctx.strokeStyle = '#333'; // 线段颜色
    ctx.lineWidth = thick; // 线段粗细
    ctx.lineCap = 'round';
    ctx.stroke(); // 开始画

    // 左分支
    drawBranch(endCoordinate, len * 0.8, thick * 0.75, angle + Math.random() * 30);
    // 右分支
    drawBranch(endCoordinate, len * 0.8, thick * 0.75, angle - Math.random() * 30);
  };

  // 先画树干
  drawBranch([0, 0], 100, 20, 90);

  //   // 坐标、长度、粗细、角度
  //   const drawBranch = (coordinate: [number, number], len: number, thick: number, angle: number) => {
  //     if (thick < 10 && Math.random() < 0.1) return;
  //     if (thick < 2) {
  //       ctx.beginPath();
  //       ctx.arc(...coordinate, 5, 0, 2 * Math.PI);
  //       ctx.fillStyle = '#fff';
  //       ctx.fill();
  //       return;
  //     }

  //     ctx.beginPath(); // 开启线段绘制
  //     ctx.moveTo(...coordinate); // 初始起始点

  //     const endCoordinate: [number, number] = [
  //       coordinate[0] + len * Math.cos((angle * Math.PI) / 180),
  //       coordinate[1] + len * Math.sin((angle * Math.PI) / 180),
  //     ];

  //     ctx.lineTo(...endCoordinate); // 线段终点
  //     ctx.strokeStyle = '#333'; // 线段颜色
  //     ctx.lineWidth = thick; // 线段粗细
  //     ctx.lineCap = 'round';
  //     ctx.stroke(); // 开始画

  //     // 左分支
  //     drawBranch(endCoordinate, len * 0.8, thick * 0.75, angle + Math.random() * 30);
  //     // 右分支
  //     drawBranch(endCoordinate, len * 0.8, thick * 0.75, angle - Math.random() * 30);
  //   };
});
</script>
相关推荐
小怪瘦794 分钟前
JS实现Table表格数据跑马灯效果
开发语言·javascript·信息可视化
励志成为大佬的小杨40 分钟前
c语言中的枚举类型
java·c语言·前端
罗_三金1 小时前
微信小程序打印生产环境日志
javascript·微信小程序·小程序·bug
shuishen491 小时前
Web Bluetooth API 开发记录
javascript·web·js
前端熊猫1 小时前
Element Plus 日期时间选择器大于当天时间置灰
前端·javascript·vue.js
傻小胖1 小时前
React 组件通信完整指南 以及 自定义事件发布订阅系统
前端·javascript·react.js
JaxNext1 小时前
开发 AI 应用的无敌配方,半小时手搓学英语利器
前端·javascript·aigc
万亿少女的梦1681 小时前
高校网络安全存在的问题与对策研究
java·开发语言·前端·网络·数据库·python
Python私教2 小时前
Vue3中的`ref`与`reactive`:定义、区别、适用场景及总结
前端·javascript·vue.js
CQU_JIAKE2 小时前
12.12【java exp4】react table全局搜索tailwindcss 布局 (Layout) css美化 3. (rowId: number
前端·javascript·react.js