H5的Canvas如何画N叉树数据结构

        大家好。我是猿码叔叔,一位有着 5 年Java工作经验的北漂,业余时间喜欢瞎捣鼓,学习一些新东西来丰富自己。看过上一篇 Java 方法调用关系的老铁们,也许遗留了不少疑问,这Java方法调用关系可视化页面就这?这方法溯源菜单功能也没有实现啊?代码倒是挺全,拿来用还是有点麻烦!

        好!今天就是给老铁们道歉来了,并奉上一篇 Canvas 画 N 叉树的方法,同时一一告诉大家为何上述问题迟迟没有解决。

        以下,为了不重复造轮子,关于 HTML5 的 Canvas 介绍,以及如何创建都不在本篇范围内出现。大家可以去问文心一言。

一、N-ary树(N叉树)数据结构的应用

        N 叉树的应用场景还是很多的,比如你们公司的部门结构,人员结构,还有我刚刚说到的 Java 方法调用关系等等。或许你可以使用“X-mind”或“亿图脑图”等脑图软件,但对于开发项目集成来说,使用 H5 的 Canvas 来画一棵生动的树图是一件很酷的事。

二、画 N 叉树的准备工作

        1、准备 N 叉树的样例数据

        这里我准备的是每个对象含有 3 个字段:value(string)、countOfChild(int)、children(array)。

    const result = {
        value: 'Root',
        children:
            [
                {
                    value: '1',
                    children:[
                        {
                            value: '2', children:[]
                        },
                        {
                            value: '3',
                            children:[
                                {
                                    value: '4',
                                    children: []
                                },
                                {
                                    value: '5',
                                    children: []
                                },
                                {
                                    value: '6',
                                    children: [
                                        {
                                            value: '7',
                                            children: []
                                        },
                                        {
                                            value: '8',
                                            children: [
                                                {
                                                    value: '9',
                                                    children: []
                                                },
                                                {
                                                    value: '10',
                                                    children: []
                                                },
                                                {
                                                    value: '11',
                                                    children: []
                                                },
                                                {
                                                    value: '12',
                                                    children: []
                                                },
                                                {
                                                    value: '13',
                                                    children: []
                                                }
                                            ]
                                        },
                                    ]
                                }
                            ]
                        },
                        {
                            value: '14', children:[]
                        }
                    ]
                },
                {
                    value: '15',
                    children:[
                        {
                            value: '16', children:[]
                        },
                        {
                            value: '17', children:[]
                        }
                    ]
                },
                {
                    value: '18',
                    children:[
                        {
                            value: '19', children:[]
                        },
                        {
                            value: '20', children:[]
                        }
                    ]
                },
                {
                    value: '21',
                    children:[
                        {
                            value: '22', children:[]
                        },
                        {
                            value: '23', children:[]
                        },
                        {
                            value: '24', children:[]
                        },
                        {
                            value: '25', children:[]
                        }
                    ]
                }
            ]
    }

        2、如何确定每个节点的位置

        canvas 画图与 H5 写 div 或者 span 元素不同的是,后者可以用 display 或者 margin 来设置位置。前者则需要一定的数学知识,即 xy 坐标。然后你想画什么形状?圆形(circle/arc)、长方形(rectangle)或者贝塞尔曲线(curve)等。这里我们要画的是“长方形”。所以我们得需要知道怎么用 Canvas 画一个长方形?

<!-- h5代码 -->
<canvas width="400" height="400" id="myCanvas"></canvas>

<!-- JS 代码 -->
window.onload = () => {
   draw();
};

draw = () => {
  const myCanvas = document.querySelector('#myCanvas');
  const ctx = myCanvas.getContext('2d');
  // 前面两个数字对应 x、y 坐标,后两个数字对应宽(w)和高(h)
  ctx.strokeRect(200, 200, 100, 80);
};

        IDEA中右键创建 HTML5 文件,然后运行上面的代码你就会看到一个宽100px和高80px的长方形了。

        为什么画长方形,常见的N叉树都是用圆形代表一个节点,这里画长方形你可以把他想象成圆形。

        3、如何用直线连接两个节点

        画!Canvas 既然提供画方形的函数,直线也是有的。

  draw = () => {
    const myCanvas = document.querySelector('#myCanvas');
    const ctx = myCanvas.getContext('2d');

    const rec1X = 200, rec1Y = 200, rec1W = 100, rec1H = 80;
    ctx.strokeRect(rec1X, rec1Y, rec1W, rec1H);

    // 两个矩形的 x 坐标相同,y 不同时纵向平移;x 不同,y 相同时水平平移
    const rec2X = rec1X, rec2Y = rec1Y + 200;

    ctx.strokeRect(rec2X, rec2Y, rec1W, rec1H);

    // 移动到第一个矩形的底部中央。xy中的 x 位于矩形左下角,y 位于矩形顶部中央,现在你可以计算如何移动了
    ctx.moveTo(rec1X + (rec1W >> 1), rec1Y + rec1H);
    ctx.lineTo(rec2X + (rec1W >> 1), rec2Y);
    ctx.stroke();
  };

        此部分代码要特别注意的是线段的两个端点移动的位置的计算方式。moveTo 是第一个端点的位置,lineTo 是将线段指向的目标位置。在不了解如何移动时,可以先随便填两个数字,然后根据偏差,找出计算规律即可。

        4、如何填充文字(fillText)

        将文字填充到 Canvas 画的矩形中时,与 HTML 创建一个 span 元素,然后键入文字,最后通过 css 样式设置 border、padding 等等不同的是,矩形与文字是完全两个互不相干的事情,唯一直观上让文字嵌入在矩形内部中央的是两者的 xy 坐标。这有点像刚刚画线的那个思路。

  draw = () => {
    const myCanvas = document.querySelector('#myCanvas');
    const txtHeight = 30;
    const ctx = myCanvas.getContext('2d');
    ctx.font = `${txtHeight}px Arial`;

    const root = "root", child = 'child';
    const rec1X = 200, rec1Y = 200, rec1W = 100, rec1H = 80;
    ctx.strokeRect(rec1X, rec1Y, rec1W, rec1H);

    const txtWidth = ctx.measureText(root).width;
    ctx.fillText(root, rec1X + (rec1W - txtWidth >> 1), rec1Y + (rec1H >> 1) + txtHeight / 3);
    // 两个矩形的 x 坐标相同,y 不同时纵向平移;x 不同,y 相同时水平平移
    const rec2X = rec1X, rec2Y = rec1Y + 200;

    const childTxtWidth = ctx.measureText(child).width;
    ctx.strokeRect(rec2X, rec2Y, rec1W, rec1H);
    ctx.fillText(child, rec2X + (rec1W - childTxtWidth >> 1), rec2Y + (rec1H >> 1) + txtHeight / 3);
    // 移动到第一个矩形的底部中央。xy中的 x 位于矩形左下角,y 位于矩形顶部中央,现在你可以计算如何移动了
    ctx.moveTo(rec1X + (rec1W >> 1), rec1Y + rec1H);
    ctx.lineTo(rec2X + (rec1W >> 1), rec2Y);
    ctx.stroke();
  };

        关于文字的xy坐标计算方式,咱们还是不在注释里强调。x位于矩形的左下角顶点,我们要将x移动到矩形中央,假设矩形的底部宽为 w,那么第一步就是 x + w / 2。这时候你会发现文字的 x 坐标并没有居中,这里我们还要减去文字宽度的一半才能居中,就是 (x + (矩形宽度- 文字宽度 / 2))。与 y 坐标计算不同的是,以 x 坐标为准的物体通常靠右。你可以拿起你上学时的尺子,观察它的刻度,x 坐标的标准就是,物体在刻度的右侧,而以 y 坐标为标准的物体则在刻度左侧或者是上侧,所以当 (y + 矩形高度 / 2) 同时还要再加上 (文字高度 / 3)。

三、开始画 N 叉树

        画 N 叉树之前,有一个严肃的问题。这个问题不是我刚要画就能想出来的,而是我在实践过程中发现的,现在分享给大家。N 叉树有很多个层级,为树的高度,他不像“完美二叉树”那样完美,你闭着眼睛就可以想象每个节点的分布,在计算其叶子节点时,知道高度就能知道叶子节点的个数,这样有利于我们划分每个节点的“占用宽度”。N 叉树计算每个节点的占用宽度时,需要知道以当前节点为父节点的叶子节点个数。你可以回到 2.1 查看那个样例数据。那棵树一共有 18 个叶子节点。第二层的每个节点所拥有的叶子节点是不同的,分别为 {10, 2, 2, 4}。这意味着拥有 10 个叶子节点的节点将占用 canvas 宽度的 10/18。其他节点的计算方式都是如此,但

当当前节点的叶子节点个数为 0 时,你需要考虑到为当前节点留余地,前提是你计算当前节点的占位宽度是以叶子节点个数为先决条件。

        我们知道了如何为每个节点划分占位宽度,那又如何为每个节点设置 xy 坐标呢?这里如果你对第二部分的内容阅读的够仔细,相信你应该能够有点眉目,也就是 2.3 和 2.4 那些部分。 

    function draw() {
        // const ret = {};
        cntOfEachLevel();
        console.log(result);
        const ctx = myCanvas.getContext('2d');
        ctx.font = "20px Arial";
        const text = result.value;
        const div = canvasWidth / result.cntOfChild;
        // 使用 measureText 测量文本宽度
        const textWidth = ctx.measureText(text).width;
        const cx1 = canvasWidth / 2 - textWidth, cy1 = 10;
        const shapeWidth = textWidth + 20;
        const txtX = cx1 + 10, txtY = cy1 + 19;
        ctx.beginPath();
        ctx.fillText(text, txtX, txtY);
        ctx.strokeRect(cx1, cy1, shapeWidth, 25);
        drawChildren(ctx, cx1, cy1, 0, shapeWidth, result.children, div, 1);
    }

    const levelHeight = 100, canvasWidth = 2000;

    drawChildren = (ctx, cx1, cy1, l, pShapeWidth, arr, div, level) => {
        let n = arr.length;
        if (n === 0) return;
        let pre = l;
        for (let i = 0; i < n; ++i) {
            let curWidth = div * arr[i].cntOfChild;
            curWidth = curWidth === 0 ? div : curWidth;
            const textWidth = ctx.measureText(arr[i].value).width;
            const shapeWidth = textWidth + 20;
            const cx2 = pre + (curWidth >> 1), cy2 = cy1 + levelHeight;
            ctx.beginPath();
            ctx.strokeRect(cx2, cy2, shapeWidth, 25);
            const textX = cx2 + 10, textY = cy2 + 19;
            ctx.fillText(arr[i].value, textX, textY);
            ctx.stroke();
            ctx.beginPath();
            ctx.moveTo(cx1 + pShapeWidth / 2, cy1 + 25);
            ctx.lineTo(cx2 + shapeWidth / 2, cy2);
            ctx.stroke();
            // xys[i] = {x: cx2, y: cy2, x2: cx2 + shapeWidth, y2: cy2 + 25};
            if (arr[i].children.length > 0) {
                drawChildren(ctx, cx2, cy2, pre, shapeWidth, arr[i].children, div, level + 1)
            }
            pre += curWidth;
        }

    }

    // myCanvas.addEventListener('click', function(e) {
    //     const x = e.offsetX, y = e.offsetY;
    //     for (let i = 0; i < xys.length; ++i) {
    //         const obj = xys[i];
    //         if (obj.x <= x && x <= obj.x2 && obj.y <= y && y <= obj.y2) {
    //             alert(arr[i]);
    //         }
    //     }
    // });

    function countNodeOfEachLevel(obj, ret, level) {
        ret[level] = ret[level] == null ? 1 : ret[level] + 1;
        for (const c of obj.children) {
            countNodeOfEachLevel(c, ret, level + 1);
        }
    }

    function cntOfEachLevel() {
        result['cntOfChild'] = dfs(result);
    }

    function dfs(obj) {
        if (obj.children.length === 0) {
            obj['cntOfChild'] = 0;
            return 1;
        }
        let cnt = 0;
        for (const c of obj.children) {
            cnt += dfs(c);
        }
        obj['cntOfChild'] = cnt;
        return cnt;
    }

        以上代码就是成品了。其中涉及到计算当前节点的叶子节点个数的小算法 你可以拿去运行,就可以看到如下图片。

        如果你想自定义文字内容,记得将 canvas 的宽度设定的足够大来适应每个节点的占位分布。最好是途中的数字。

四、结语

        创作不易,欢迎读者的支持与点评。之所以想通过 Canvas 画 N 叉树,是因为上一篇的 Java 方法调用关系可视化页面不够精彩,但 Canvas 的学习和实践着实有挑战性,想要玩转它并非易事。我近乎是 0 基础学习的 Canvas,现在看来困难不是最可怕的,可怕的是不去动手实践。我在力扣学习动态规划时,总觉得他理解起来很费事,但当你先用暴力解法解开一道算法题时,再优化暴力解法,你会发现动态规划其实就是暴力解法的升级版。大问题是可以拆分的,咱们今天这个 N 叉树本质就是两个节点如何连线的基础问题,解决了这个问题,剩下的就是复制了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/797740.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

护网HW面试——redis利用方式即复现

参考&#xff1a;https://xz.aliyun.com/t/13071 面试中经常会问到ssrf的打法&#xff0c;讲到ssrf那么就会讲到配合打内网的redis&#xff0c;本篇就介绍redis的打法。 未授权 原理&#xff1a; Redis默认情况下&#xff0c;会绑定在0.0.0.0:6379&#xff0c;如果没有采用相关…

基于SpringBoot的校园志愿者管理系统

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果有相关需求&#xff0c;可以私信联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot框架 工具&#xff1a;MyEclipse、Tomcat 系统展示 首页 个人中心 志愿者管理 活动信息…

黑马头条微服务学习day01-环境搭建、SpringCloud微服务(注册发现、网关)

文章目录 项目介绍环境搭建项目背景业务功能技术栈说明 nacos服务器环境准备nacos安装 初始工程搭建环境准备主体结构 app登录需求分析表结构分析手动加密微服务搭建接口定义功能实现登录功能实现 Swagger使用app端网关nginx配置 项目介绍 环境搭建 项目背景 业务功能 技术栈说…

数据结构(Java):树二叉树

目录 1、树型结构 1.1 树的概念 1.2 如何判断树与非树 1.3 树的相关概念 1.4 树的表示形式 1.4.1 孩子兄弟表示法 2、二叉树 2.1 二叉树的概念 2.2 特殊的二叉树 2.3 二叉树的性质 2.4 二叉树的存储 2.5 二叉树的遍历 1、树型结构 1.1 树的概念 树型结构是一种非线…

MySQL复合查询(重点)

前面我们讲解的mysql表的查询都是对一张表进行查询&#xff0c;在实际开发中这远远不够。 基本查询回顾 查询工资高于500或岗位为MANAGER的雇员&#xff0c;同时还要满足他们的姓名首字母为大写的J mysql> select * from emp where (sal>500 or jobMANAGER) and ename l…

数据湖仓一体(一) 编译hudi

目录 一、大数据组件版本信息 二、数据湖仓架构 三、数据湖仓组件部署规划 四、编译hudi 一、大数据组件版本信息 hudi-0.14.1zookeeper-3.5.7seatunnel-2.3.4kafka_2.12-3.5.2hadoop-3.3.5mysql-5.7.28apache-hive-3.1.3spark-3.3.1flink-1.17.2apache-dolphinscheduler-3.1.9…

[Vulnhub] Sedna BuilderEngine-CMS+Kernel权限提升

信息收集 IP AddressOpening Ports192.168.8.104TCP:22, 53, 80, 110, 111, 139, 143, 445, 993, 995, 8080, 55679 $ nmap -p- 192.168.8.104 --min-rate 1000 -sC -sV PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 6.6.1p1 Ubuntu 2ubuntu2 …

C++20中的consteval说明符

在C20中&#xff0c;立即函数(immediate function)是指每次调用该函数都会直接或间接产生编译时常量表达式(constant expression)的函数。这些函数在其返回类型前使用consteval关键字进行声明。 立即函数是constexpr函数&#xff0c;具体情况取决于其要求。与constexpr相同&…

半小时获得一张ESG入门证书【详细中英文笔记一】

前些日子&#xff0c;有朋友转发了一则小红书的笔记给我&#xff0c; 标题是《半小时获CFI官方高颜值免费证书 ESG认证》。这对考证狂魔的我来说&#xff0c;必然不能错过啊&#xff0c;有免费的羊毛不薅白不薅。 ESG课程的 CFI 官方网址戳这里&#xff1a;CFI 于是信心满满的…

清华大学孙富春教授团队开发了多模态数字孪生环境,辅助机器人获得复杂的 3C 装配技能

中国是全球3C产品&#xff08;电脑、通信和消费电子&#xff09;的主要生产国&#xff0c;全球70%的3C产品产能集中在中国。3C智能制造装备的突破与产业化&#xff0c;对于提升我国制造产业的全球竞争力意义重大。 机器人在计算机、通信和消费电子 &#xff08;3C&#xff09; …

常用的设计模式和使用案例汇总

常用的设计模式和使用案例汇总 【一】常用的设计模式介绍【1】设计模式分类【2】软件设计七大原则(OOP原则) 【二】单例模式【1】介绍【2】饿汉式单例【3】懒汉式单例【4】静态内部类单例【5】枚举&#xff08;懒汉式&#xff09; 【三】工厂方法模式【1】简单工厂模式&#xf…

springboot 程序运行一段时间后收不到redis订阅的消息

springboot 程序运行一段时间后收不到redis订阅的消息 问题描述 程序启动后redis.user.two主题正常是可以收到消息的&#xff0c;发一条收一条&#xff0c;但是隔一段时间后&#xff1b;就收不到消息了&#xff1b; 此时如果你手动调用发送另外一个消息订阅redis.user.two2&…

vmware workstation 虚拟机安装

vmware workstation 虚拟机安装 VMware Workstation Pro是VMware&#xff08;威睿公司&#xff09;发布的一代虚拟机软件&#xff0c;中文名称一般称 为"VMware 工作站".它的主要功能是可以给用户在单一的桌面上同时运行不同的操作系统&#xff0c;它也是可进 行开发…

c# 容器变换

List<Tuple<int, double, bool>> 变为List<Tuple<int, bool>>集合 如果您有一个List<Tuple<int, double, bool>>并且您想要将其转换为一个List<Tuple<int, bool>>集合&#xff0c;忽略double值&#xff0c;您可以使用LINQ的S…

3U 与 SV630A 伺服实现 CANLINK 通讯

1、打开 AUTOSHOP&#xff0c;点击工具>系统选项&#xff0c;勾选自动生成 canlink 轴 控通讯配置和 canlink 轴控指令增强功能。 2、检查 plc 的拨码是否已经拨上去。 1 代表 485 通讯&#xff0c;2 代表 can 通讯&#xff0c;将 2 打到 ON 状态。还有9&#xff0c;10拨…

Matlab 计算一个平面与一条直线的交点

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 这里使用一种很有趣的坐标:Plucker线坐标,它的定义如下所示: 这个坐标有个很有趣的性质,将直线 L L L与由其齐次坐标 V = (

IDEA社区版使用Maven archetype 创建Spring boot 项目

1.新建new project 2.选择Maven Archetype 3.命名name 4.选择存储地址 5.选择jdk版本 6.Archetype使用webapp 7.create创建项目 创建好长这样。 检查一下自己的Maven是否是自己的。 没问题的话就开始增添java包。 [有的人连resources包也没有&#xff0c;那就需要自己添…

AI人工智能开源大模型生态体系分析

人工智能开源大模型生态体系研究 "人工智能开源大模型生态体系研究报告v1.0"揭示&#xff0c;AI(A)的飞速发展依赖于三大核心&#xff1a;数据、算法和算力。这一理念已得到业界广泛认同&#xff0c;三者兼备才能推动AI的壮大发展。随着AI大模型的扩大与普及&#xf…

el-table 动态添加删除 -- 鼠标移入移出显隐删除图标

<el-table class"list-box" :data"replaceDataList" border><el-table-column label"原始值" prop"original" align"center" ><template slot-scope"scope"><div mouseenter"showClick…

finalshell替换背景图片

&#x1f4d1;打牌 &#xff1a; da pai ge的个人主页 &#x1f324;️个人专栏 &#xff1a; da pai ge的博客专栏 ☁️宝剑锋从磨砺出&#xff0c;梅花香自苦寒来 ☁️运维工程师的职责&#xff1a;监…