当前位置: 首页 > news >正文

canvas学习:如何绘制带孔洞的多边形

在canvas中可以通过路径绘制多边形,但是多边形有一种特殊的情况就是带有孔洞的多边形。这种多边形又该如何绘制呢,今天我就来探究一下这个问题

一、使用通常的方法绘制(失败)

我准备了如下的两组坐标,outer构成了多边形的外轮廓,inner构成了多边形内岛的轮廓。

// 外环
const outer = [
  [100, 100],
  [500, 100],
  [500, 500],
  [100, 500],
  [100, 100],
];

// 内环
const inner = [
  [200, 200],
  [300, 200],
  [300, 300],
  [200, 300],
  [200, 200],
];

之后使用路径进行绘制

  ctx.fillStyle = "red";
  ctx.strokeStyle = "black";
  ctx.lineWidth = 3;

  // 绘制外环
  ctx.beginPath();
  for (let index = 0; index < inner.length; index++) {
    const point = outer[index];
    if (index === 0) {
      ctx.moveTo(point[0], point[1]);
    } else {
      ctx.lineTo(point[0], point[1]);
    }
  }

  // 绘制内环
  for (let index = 0; index < inner.length; index++) {
    const point = inner[index];
    if (index === 0) {
      ctx.moveTo(point[0], point[1]);
    } else {
      ctx.lineTo(point[0], point[1]);
    }
  }

  ctx.closePath();

  ctx.fill();
  ctx.stroke();

绘制出来的成果如下,可以看到中的孔洞还是成为了图形的一部分,并未实现我所想要的效果。

二、使用奇偶环绕规则实现带孔洞的多边形

我查阅了资料后了解到想要绘制带孔洞的多边形,需要用的 CanvasRenderingContext2D.fill() 方法 的参数fillRule

这个参数默认为 nonzero(非零环绕规则),若设置为evenodd(奇偶环绕规则)就可以实现我想要的效果。

 ctx.fillStyle = "red";
  ctx.strokeStyle = "black";
  ctx.lineWidth = 3;

  // 绘制外环
  ctx.beginPath();
  for (let index = 0; index < inner.length; index++) {
    const point = outer[index];
    if (index === 0) {
      ctx.moveTo(point[0], point[1]);
    } else {
      ctx.lineTo(point[0], point[1]);
    }
  }

  // 绘制内环
  for (let index = 0; index < inner.length; index++) {
    const point = inner[index];
    if (index === 0) {
      ctx.moveTo(point[0], point[1]);
    } else {
      ctx.lineTo(point[0], point[1]);
    }
  }

  ctx.closePath();

  ctx.fill();
  ctx.stroke('evenodd');

这个简直是太神奇了,只要稍稍的修改一下fillRule参数就可以实现不同的效果。我对这两个环绕规则产生了浓厚的兴趣,我想了解一下它们究竟是什么。

三、非零环绕规则与奇偶环绕规则

在查阅了一些文章后我对这两个规则有了一定的了解。

首先,非零环绕规则与奇偶环绕规则本质上是两个用于判断点在多边形内部还是外部的算法,在fill()方法中显然就是用来判断哪些区域是属于多边形内部应该被填充。

因此在上面的例子中多边形孔洞里的点在非零规则下被判定为了多边形的内部点。

而在奇偶规则下则被判定为了多边形的外部点,这是导致两种规则下绘图结果差异的原因。

什么是非零环绕规则?

非零环绕规则是: 若环绕数为0表示点在多边形内,非零表示在点多边形外

上面这个规则可能有点不太好理解,但是我们一起来使用一下非零环绕规则就知道了。

首先,我们要统计环绕数,这个环绕数初始为0。

let index = 0; //环绕数

然后要确定多边形每条边的方向,在我这里 多边形外环和内环的方向使用的是绘制路径时的方向,都为顺时针。

然后随机取一个点,并从该点开始向任意方向画一条射线。(这里我就还用之前孔洞中的P点,因为我也是要研究孔洞的问题)

之后对在每个方向上穿过射线的边计数,每当多边形的边从右到左穿过射线时,环绕数加1,从左到右时,环绕数减1。

下面我就统计一下,外环边从左至右穿过了射线,所以环绕数减1。

外环边也从左至右穿过了射线,所以环绕数再减1。

最终统计的环绕数就为-2,非零环绕规则环绕数非零,所以点P就位于多边形的内部。因此最终P点所在的孔洞部分就被视为了多边形的内部被填充了红色。

什么是奇偶环绕规则?

奇-偶规则为:奇数表示点在多边形内,偶数表示点在多边形外

下面我就用奇偶环绕规则再来计算一遍P点与多边形的位置关系。依旧是从P点开始画一条射线:

之后统计多边形的边与射线相交的次数,在我这里就是2次。

由于2是偶数,所以根据奇偶规则,点P就位于多边形的外面。因此在奇偶环绕规则下点P所在的孔洞就被判定为了多边形的外部,最终没有被填充为红色。

四、使用非零环绕规则实现带孔洞的多边形

在深入了解了非零环绕规则与奇偶环绕规则后,我发现使用非零环绕规则似乎也可以实现带孔洞的多边形。

方法很简单,只要让内环和外环的方向相反,就可以令最终的环绕数为0,这样孔洞部分就会被判定为多边形的外侧了。

比如说,我就可以让外环还是保持顺时针方向,但将内环改为逆时针方向。

此时外环会从左至右穿过射线,令环绕数减1。而内环则是从右至左穿过射线,令环绕数加1。最终统计的环绕数就是0,根据非零规则P点位于多边形外。

理清了思路,我们就可以尝试一下了。稍稍修改一下代码,在绘制内环时反向遍历内环坐标数组,便可以将内环路径的方向改为逆时针。

  // 外环
  const outer = [
    [100, 100],
    [500, 100],
    [500, 500],
    [100, 500],
    [100, 100],
  ];
  
  // 内环
  const inner = [
    [200, 200],
    [300, 200],
    [300, 300],
    [200, 300],
    [200, 200],
  ];

  ctx.fillStyle = "red";
  ctx.strokeStyle = "black";
  ctx.lineWidth = 3;

  // 绘制外环
  ctx.beginPath();
  for (let index = 0; index < inner.length; index++) {
    const point = outer[index];
    if (index === 0) {
      ctx.moveTo(point[0], point[1]);
    } else {
      ctx.lineTo(point[0], point[1]);
    }
  }

  // 绘制内环
  for (let index = inner.length - 1; index >= 0; index--) {
    const point = inner[index];
    if (index === inner.length - 1) {
      ctx.moveTo(point[0], point[1]);
    } else {
      ctx.lineTo(point[0], point[1]);
    }
  }

  ctx.closePath();

  ctx.fill();
  ctx.stroke();

最终的结果如我所愿。(*^▽^*)

五、美丽的剪纸效果

之后我有在这篇博客(canvas笔记-非零环绕原则及剪纸实例-CSDN博客)中看到,这位大佬利用 fillRule的规则实现了剪纸的效果,简直是叹为观止。我在这里忍不住复刻一下。

这是我复刻的剪纸效果:

代码如下,其实很简单,但是与那位大佬实现方式不同的是,我在绘制剪纸的时候直接使用了“奇偶环绕规则”,这样就不用像他那样去刻意控制路径的方向了,实现起来更加简单。

  // 剪纸外轮廓
  const paper_outer_rect = {
    x: 350,
    y: 50,
    w: 600,
    h: 600,
  };

  // 剪纸内轮廓

 //内轮廓长方形 
  const paper_inner_rect = {
    x: 450,
    y: 150,
    w: 400,
    h: 200,
  };

 //内轮廓三角形
  const paper_inner_triangle = [
    [550, 400],
    [400, 600],
    [700, 600],
    [550, 400],
  ];

// 内轮廓圆
  const paper_inner_circle = {
    x: 800,
    y: 500,
    r: 100,
  };

  // ctx.fillStyle = "#058";
  ctx.fillStyle = "#ffebcd";
  ctx.shadowColor = "gray";
  ctx.shadowBlur = 10;
  ctx.shadowOffsetX = 10;
  ctx.shadowOffsetY = 10;

  ctx.beginPath();

  // 绘制外轮廓
  ctx.rect(
    paper_outer_rect.x,
    paper_outer_rect.y,
    paper_outer_rect.w,
    paper_outer_rect.h
  );

// 绘制内轮廓
  ctx.rect(
    paper_inner_rect.x,
    paper_inner_rect.y,
    paper_inner_rect.w,
    paper_inner_rect.h
  );

  paper_inner_triangle.forEach((point, index) => {
    if (index === 0) {
      ctx.moveTo(point[0], point[1]);
    } else {
      ctx.lineTo(point[0], point[1]);
    }
  });

  ctx.arc(
    paper_inner_circle.x,
    paper_inner_circle.y,
    paper_inner_circle.r,
    0,
    2 * Math.PI,
    false
  );

  ctx.fill("evenodd");

参考资料

  1. CanvasRenderingContext2D:fill() 方法 - Web API | MDN
  2. 非零缠绕规则和奇偶规则_非零环绕数规则-CSDN博客
  3. canvas笔记-非零环绕原则及剪纸实例-CSDN博客

相关文章:

  • 详细存储与相关接口协议?
  • Vue项目的 Sass 全局基础样式格式化方案,包含常见元素的样式重置
  • 头歌实践教学平台--【数据库概论】--SQL
  • VUE3 路由配置
  • Apifox下载安装
  • 【C++】C++中的动态内存分配(new和delete)
  • 2025前端面试题(vue、react、uniapp、微信小程序、JS、CSS、其他)
  • 从零构建大语言模型全栈开发指南:第二部分:模型架构设计与实现-2.2.1从零编写类GPT-2模型架构(规划模块与代码组织)
  • 详细介绍RECT结构体
  • 09_从经典论文入手Seq2Seq架构
  • spring-security原理与应用系列:核心过滤器
  • 设置 Ollama 模型下载位置
  • Spring 线程
  • 微信小程序如何接入直播功能
  • [leetcode]map的用法
  • SpringBoot-配置文件中敏感信息的加密保姆级教程
  • Solr-搜索引擎-入门到精通
  • Ubuntu与Windows之间相互复制粘贴的方法
  • Spring MVC 请求与响应
  • Node.js下载安装配置指南(精简)
  • 大足网站建设公司/今日桂林头条新闻
  • 阿里云服务的官方网站/广州网站快速优化排名
  • 公司网站建设考核/环球网今日疫情消息
  • 网站魔板大全/百度seo有用吗
  • 阳江网雨大医院/网站seo快速优化技巧
  • 做网站卖什么发财/北京互联网公司