03、指令式绘图:利用Canvas绘制层次关系图

利用Canvas绘制层次关系图

一、Canvas简介

Canvas API 提供了一个通过JavaScript 和 HTML的canvas元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。

二、Canvas元素与2D上下文

1、canvas元素

对于浏览器而言,canvas也是HTML元素,可以利用canvas标签将其插入HTML内容中,例如:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <canvas width="512" height="512"></canvas>
</body>
</html>

其中关于canvas标签,只有两个属性:width与height,这决定了canvas的画布宽高 ,与CSS的属性中设置的样式宽高决定这canvas中画布的坐标系。

如果不设置canvas元素的样式宽高,样式宽高的像素值就是画布宽高值。

如果样式宽高设置小于画布宽高,canvas的坐标系会发生什么变化?

画布宽高决定了可视区域的坐标范围,所以canvas将画布宽高与样式宽高分开的原因是能方便适配不同的显示设备。

三、canvas坐标系

默认左上角为坐标原点,X轴水平向右,Y轴垂直向下。

四、canvas绘制几何图形

1、插入canvas标签
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <canvas width="512" height="512"></canvas>
</body>
</html>
2、获取canvas上下文

获取canvas元素

js 复制代码
const canvas =document.querySelector('canvas');

获取上下文

js 复制代码
const ctx = canvas.getContext('2d');
3、绘制图形

例如中心绘制一个100*100的正方形,并填充为红色对应的指令代码是:

js 复制代码
    const canvas = document.querySelector("canvas");
    const ctx = canvas.getContext("2d");
    const rectSize = [100, 100];
    ctx.fillStyle = "red";
    ctx.beginPath();
    ctx.rect(
      0.5 * (canvas.width - rectSize[0]),
      0.5 * (canvas.height - rectSize[0]),
      ...rectSize
    );
    ctx.fill();

除了在计算的时候改变顶点坐标,我们还可以对图形进行平移变换。

js 复制代码
    ctx.fillStyle = "blue";
    ctx.beginPath();
    ctx.translate(-25, -25);
    ctx.rect(0.5 * canvas.width, 0.5 * canvas.height, 50, 50);
    ctx.fill();

对比改变图形的顶点坐标与对图形进行平移变换,这两者各有优缺点。

  1. 更改顶点坐标:

    • 优点:直接,便捷;

    • 缺点:对于不规则多边形,顶点计算会很麻烦;

  2. 平移变换:

    • 优点:整体平移画布,不需要改变顶点状态;
    • 缺点:画布状态改变了,会影响后续图形绘制的画布的起始状态。

对于平移操作,如果需要进行后续绘制,那么需要将画布状态恢复,恢复的方式有两种:

  1. canvas上下文提供的save和restore方法:

    js 复制代码
        const canvas = document.querySelector("canvas");
        const ctx = canvas.getContext("2d");
        const rectSize = [100, 100];
        ctx.fillStyle = "red";
        ctx.beginPath();
        ctx.save();//保存状态
        ctx.translate(-50, -50);
        ctx.rect(0.5 * canvas.width, 0.5 * canvas.height, ...rectSize);
        ctx.restore();//恢复状态
        ctx.fill();
    
        ctx.fillStyle = "blue";
        ctx.beginPath();
        ctx.save();
        ctx.translate(-25, -25);
        ctx.rect(0.5 * canvas.width, 0.5 * canvas.height, 50, 50);
        ctx.fill();
        ctx.restore();
  2. 反向平移回去

    js 复制代码
        const canvas = document.querySelector("canvas");
        const ctx = canvas.getContext("2d");
        const rectSize = [100, 100];
        ctx.fillStyle = "red";
        ctx.beginPath();
        ctx.translate(-50, -50);//第一次平移
        ctx.rect(0.5 * canvas.width, 0.5 * canvas.height, ...rectSize);
        ctx.fill();
        ctx.translate(50,50);//反向平移恢复画布初始状态
    
        ctx.fillStyle = "blue";
        ctx.beginPath();
        ctx.translate(-25, -25);//在画布初始状态下再平移
        ctx.rect(0.5 * canvas.width, 0.5 * canvas.height, 50, 50);
        ctx.fill();

不进行任何操作,只在初始状态进行绘制:

js 复制代码
    const canvas = document.querySelector("canvas");
    const ctx = canvas.getContext("2d");
    const rectSize = [100, 100];
    ctx.fillStyle = "red";
    ctx.beginPath();
    ctx.rect(0.5 * canvas.width, 0.5 * canvas.height, ...rectSize);
    ctx.fill();

    ctx.fillStyle = "blue";
    ctx.beginPath();
    ctx.rect(0.5 * canvas.width, 0.5 * canvas.height, 50, 50);
    ctx.fill();

五、canvas绘制层级关系图

绘制的图形对应的数据如下:

json 复制代码
export default {
    "name": "中国",
    "children": [
        {
            "name": "浙江",
            "children": [
                {
                    "name": "杭州"
                },
                {
                    "name": "宁波"
                },
                {
                    "name": "温州"
                },
                {
                    "name": "绍兴"
                }
            ]
        },
        {
            "name": "广西",
            "children": [
                {
                    "name": "桂林"
                },
                {
                    "name": "南宁"
                },
                {
                    "name": "柳州"
                },
                {
                    "name": "防城港"
                }
            ]
        },
        {
            "name": "黑龙江",
            "children": [
                {
                    "name": "哈尔滨"
                },
                {
                    "name": "齐齐哈尔"
                },
                {
                    "name": "牡丹江"
                },
                {
                    "name": "大庆"
                }
            ]
        },
        {
            "name": "新疆",
            "children": [
                {
                    "name": "乌鲁木齐"
                },
                {
                    "name": "克拉玛依"
                },
                {
                    "name": "吐鲁番"
                },
                {
                    "name": "哈密"
                }
            ]
        },
        {
            "name": "河北",
            "children": [
                {
                    "name": "石家庄"
                },
                {
                    "name": "唐山"
                },
                {
                    "name": "邯郸"
                },
                {
                    "name": "秦皇岛"
                }
            ]
        },
        {
            "name": "西藏",
            "children": [
                {
                    "name": "拉萨"
                },
                {
                    "name": "昌都"
                },
                {
                    "name": "林芝"
                }
            ]
        },
        {
            "name": "江苏",
            "children": [
                {
                    "name": "南京"
                },
                {
                    "name": "无锡"
                },
                {
                    "name": "徐州"
                },
                {
                    "name": "常州"
                },
                {
                    "name": "连云港"
                },
                {
                    "name": "淮安"
                }
            ]
        },
        {
            "name": "江苏",
            "children": [
                {
                    "name": "南京"
                },
                {
                    "name": "无锡"
                },
                {
                    "name": "徐州"
                },
                {
                    "name": "常州"
                },
                {
                    "name": "连云港"
                },
                {
                    "name": "淮安"
                }
            ]
        },
        {
            "name": "湖南",
            "children": [
                {
                    "name": "长沙"
                },
                {
                    "name": "株洲"
                },
                {
                    "name": "湘潭"
                },
                {
                    "name": "衡阳"
                },
                {
                    "name": "邵阳"
                },
                {
                    "name": "岳阳"
                }
            ]
        },
        {
            "name": "海南",
            "children": [
                {
                    "name": "海口"
                },
                {
                    "name": "三亚"
                },
                {
                    "name": "三沙"
                }
            ]
        },
        {
            "name": "陕西",
            "children": [
                {
                    "name": "西安"
                },
                {
                    "name": "咸阳"
                },
                {
                    "name": "汉中"
                },
                {
                    "name": "安康"
                },
                {
                    "name": "榆林"
                },
                {
                    "name": "延安"
                }
            ]
        },
        {
            "name": "甘肃",
            "children": [
                {
                    "name": "兰州"
                },
                {
                    "name": "酒泉"
                },
                {
                    "name": "金昌"
                },
                {
                    "name": "天水"
                },
                {
                    "name": "嘉峪关"
                },
                {
                    "name": "武威"
                }
            ]
        }
    ]
}

我们需要把数据的层级、位置和圆的半径、位置对应起来。

使用d3-hierarchy工具将上述json转换为下面的效果:

js 复制代码
{
  data: {name: '中国', children: [...]},
  children: [
    {
      data: {name: '江苏', children: [...]},
      value: 7,
      r: 186.00172579386546,
      x: 586.5048250548921,
      y: 748.2441892254667,
    }
    ...
  ],
  value: 69,
  x: 800,
  y: 800,
  r: 800,
}

完整代码如下,实现逻辑注释标明:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <canvas width="1600" height="1600"></canvas>
    <script src="https://d3js.org/d3-hierarchy.v1.min.js"></script>
  </body>
  <script type="module">
   import dataSource from "./data.js";
    // 获取节点数量
    const regions = d3
      .hierarchy(dataSource)
      .sum((d) => 1)
      .sort((a, b) => b.value - a.value);
    // 设置节点大小
    const pack = d3.pack().size([1600, 1600]).padding(3);
    // 获取根节点
    const root = pack(regions);
    console.log(root);

   const canvas = document.querySelector("canvas");
    const ctx = canvas.getContext("2d");

    // 绘制节点
    const draw = (
      ctx,
      node,
      { fillStyle = "rgba(0, 0, 0, 0.2)", textColor = "white" } = {}
    ) => {
      // 获取子节点
      const children = node.children;
      // 获取节点的位置和半径
      const { x, y, r } = node;
      // 设置填充颜色
      ctx.fillStyle = fillStyle;
      // 开始绘制圆
      ctx.beginPath();
      // 绘制圆弧
      ctx.arc(x, y, r, 0, 2 * Math.PI);
      // 填充圆弧
      ctx.fill();
      // 如果有子节点,则绘制子节点
      if (children) {
        for (let i = 0; i < children.length; i++) {
          draw(ctx, children[i]);
        }
      // 如果没有子节点,则绘制文本
      } else {
        // 设置文本颜色
        ctx.fillStyle = textColor;
        // 设置字体
        ctx.font = "12px Arial";
        // 设置文本对齐方式
        ctx.textAlign = "center";
        // 绘制文本
        ctx.fillText(node.data.name, x, y);
      }
    };
    // 绘制根节点
    draw(ctx, root);
  </script>
</html>
相关推荐
前端小巷子3 分钟前
跨标签页通信(三):Web Storage
前端·面试·浏览器
工呈士3 分钟前
TCP 三次握手与四次挥手详解
前端·后端·面试
BillKu5 分钟前
Vue3 + TypeScript + Element Plus + el-input 输入框列表按回车聚焦到下一行
前端·javascript·typescript
复苏季风5 分钟前
前端程序员unity学习笔记01: 从c#开始的入门,using命名空间,MonoBehaviour,static,public
前端
阿古达木8 分钟前
沉浸式改 bug,步步深入
前端·javascript·github
stoneSkySpace17 分钟前
react 自定义状态管理库
前端·react.js·前端框架
堕落年代30 分钟前
SpringAI1.0的MCPServer自动暴露Tool
前端
南囝coding1 小时前
一篇文章带你了解清楚,Google Cloud 引发全球互联网服务大面积故障问题
前端·后端
Humbunklung1 小时前
DeepSeek辅助写一个Vue3页面
前端·javascript·vue.js
摸鱼仙人~1 小时前
ESLint从入门到实战
前端