当前位置: 欣欣网 > 码农

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

2024-06-11码农

前言

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

用 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多名前端小伙伴在等着一起学习哦 -->

  • 完整代码

    <template>
    <div style="background-color: cadetblue">
    <canvasref="canvasRef"width="1000"height="750"></canvas>
    </div>
    </template>
    <scriptsetuplang="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.1return;
    if (thick < 2) {
    // 绘制果实
    ctx.beginPath();
    ctx.arc(...coordinate, 502 * Math.PI);
    ctx.fill style = '#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.stroke style = '#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([00], 1002090);
    // // 坐标、长度、粗细、角度
    // 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.fill style = '#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.stroke style = '#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>