【Web】HTML5 Canvas 2D绘图的封装

06-02 1034阅读

概述

(原文写自2024年10月9日,整理笔记所以现在发出)

【Web】HTML5 Canvas 2D绘图的封装

HTML5 Canvas并不是一项很新的技术了,但是作为一名业余程序员,我是不需要考虑新旧技术和投入实际生产的问题,相反,我只需要考虑有趣,什么有趣搞什么。在Godot中玩味了一圈CanvasItem的绘图函数,也慢慢补齐了一点三角函数、向量和线性变换的基础之后,发现绘图才是程序中最有意思的内容。

关于Canvas的2D绘图指令,确实没有必要重复讲述,有很好的文章和在线文档讲述这些内容。相反,一些除绘图指令之外的用法是非常值得进阶学习的,因为我有Godot中的一些经验,理解和运用这些内容也变得非常容易,这大概就叫做“触类旁通”吧。

本文部分参考《HTML5 Canvas核心技术——图形、动画与游戏开发》(下文简称《HC开发》)一书,MDN文档、菜鸟教程和其他各处博文等。

本文的主要目标是试图精炼Canvas 2D绘图的一些高级和核心内容,并将其封装为一个自定义类的方法,从而简化原来的大量基础绘图代码,并且作为后续高级应用开发的基础。你可以看到我糅合了很多Godot中的思路和做法,之前封装JS版本的Vector2类就是我无法忘掉从Godot中学习到的内容。

另外推荐渡一教育的几个Canvas视频,我觉得是目前讲的最牛最清晰的一个,而且也是最接近《HC开发》一书内容的,可以作为速通和辅助学习视频。渡一教育的视频在B站、小红书等社交账号都可以找到。

【Web】HTML5 Canvas 2D绘图的封装

本文的最大特点,一个是业余,一个就是会讲述整个自定义类逐渐添加和封装Canvas核心功能的过程和思路。并且会以小tip的形式补充大量JavaScript的基础知识点(毕竟我的JavaScript基础也不是很牢固)。

动态创建canvas

在2D绘制方面,最重要的是下面两句:

var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");

封装为ES6类


JavaScript中的类

ECMAScript(简称ES)是一种由Ecma International通过ECMA-262标准定义的脚本语言规范。ES5和ES6是这个规范的两个版本,分别代表JavaScript语言的两个不同的发展阶段。

在ES5中,JavaScript并没有原生的类(class)概念,但是可以通过构造函数和原型链(prototype)来模拟面向对象编程中的类。而在ES6中,JavaScript正式引入了类(class)的概念,提供了一种更简洁和直观的方式来实现面向对象编程,但是其本质还是通过构造函数和原型链(prototype)实现。


可以看到,很多脚本和语言都有趋同化的设计,如果你将JavaScript和Python以及GDSCript放在一起,会发现很多相似的东西,毕竟思路和用途都大差不差。

以下是一个简单的ES6风格的类定义形式和实例化用法:

class 类名{
    constructor(参数列表){
       this.属性A = 参数1
       this.属性B = 参数2
      //...
    }
    方法(参数){
      //...
    }
}
let 实例 = new 类名(参数列表);
  • 类名一般首字母大写
  • constructor()是类的构造函数,可以传入一些参数,为属性进行初始的赋值
  • 构造函数和一般的方法都不需要带function关键字
  • new 类名(参数列表)而不是类名.new(),习惯了GDSCript,很容易写错

    我们依照ES6风格的类定义形式,定义一个初步的·Canvas2D类型如下:

    // Canvas2D.js
    // 2D canvas 辅助类
    class Canvas2D{
        constructor(width,height,p_node = document.body){
            this.canvas = document.createElement("canvas");
            this.ctx = this.canvas.getContext("2d");
            this.canvas.width = width;
            this.canvas.height = height;
            p_node.append(this.canvas);
        }
    }
    

    其中:

    • Canvas2D的构造函数,有三个参数:
      • width和height分别指定的画布宽度和高度
      • p_node指定标签的父元素,默认为document.body

        如上定义后,我们只需要在测试代码中new一个Canvas2D的实例,并传入宽高和父元素,就可以自定在测试页面的标签或其他元素中添加一个标签,Canvas2D实例会在其canvas属性中存储对标签实例的引用。

        // draw.js
        var canvas = new Canvas2D(200,200);
        

        因为需要再浏览器中使用和测试,所以我们搭建如下的测试页面:

        
        
        
            
            
            Canvas2D测试
            
            
                canvas{
                    box-shadow: 1px 1px 5px #ccc;
                }
            
        
        
        
        
        
        

        其中:

        • 在部分引入Canvas2D.js,并且用直接定义所有canvas标签的统一样式,为其添加一个box-shadow,用于在HTML页面中与白色背景区分
        • 在外,引入draw.js,作为测试代码

          测试效果:

          【Web】HTML5 Canvas 2D绘图的封装

          使用Getter和Setter

          使用set和get关键字,可以定义属性的Getter和Setter方法,用于更细节的控制属性的读写操作。

          这里我们将Canvas2D的width和height属性设定为读写Canvas2D.canvas的width和height属性,以简化代码:

          // Canvas2D.js
          // 2D canvas 辅助类
          class Canvas2D{
              constructor(width,height,p_node = document.body){...}
              set width(val){
                  this.canvas.width = val;
              }
              get width(){
                  return this.canvas.width;
              }
              set height(val){
                  this.canvas.height = val;
              }
              get height(){
                  return this.canvas.height;
              }
          }
          

          这样我们就可以直接像下面这样重新定义和读取canvas的尺寸:

          // draw.js
          var canvas = new Canvas2D(200,200);
          // 通过height和width属性重新设定canvas的尺寸
          canvas.height = 400;
          canvas.width = 300;
          // 读取canvas的尺寸
          console.log(canvas.height);
          console.log(canvas.width);
          

          【Web】HTML5 Canvas 2D绘图的封装

          编写绘图方法

          在搞定canvas的实例化之后,我们开始正式封装一些绘图方法。

          draw_circle()

          以绘制圆为例,封装一个方法如下:

          // Canvas2D.js
          // 2D canvas 辅助类
          class Canvas2D{
              //...
              // ================= 方法 =================
              draw_circle(cx,cy,radius,border = "#000",fill = "#fff",border_width = 1,dash = null){
                  let ctx =  this.ctx;
                  ctx.beginPath();
                  // 样式设定
                  ctx.strokeStyle = border;      // 轮廓样式
                  ctx.lineWidth = border_width;  // 轮廓线宽
                  ctx.fillStyle = fill;          // 填充样式
                  if(dash != null && dash.length>0){   //虚线样式
                      ctx.setLineDash(dash);
                  }
                  // 主体路径
                  ctx.arc(cx,cy,radius,0,Math.PI * 2);
                  // 填充和轮廓绘制
                  ctx.fill();
                  ctx.stroke();
              }
          }
          

          测试:

          // draw.js
          var canvas = new Canvas2D(200,200);
          canvas.draw_circle(100,100,50);
          

          【Web】HTML5 Canvas 2D绘图的封装

          // draw.js
          var canvas = new Canvas2D(200,200);
          canvas.draw_circle(100,100,50,"#444","#eee",2);
          

          【Web】HTML5 Canvas 2D绘图的封装

          // draw.js
          var canvas = new Canvas2D(200,200);
          canvas.draw_circle(100,100,50,"#FF5722","#F0F4C3",2,[5,10]);
          

          【Web】HTML5 Canvas 2D绘图的封装

          对draw_circle()的改进

          可以看到对轮廓和填充样式的设定,以及调用fill()和stroke()进行绘制,对于每个绘图函数封装都是必须且重复的,所以可以将代码提炼出来,作为单独的方法。

          // Canvas2D.js
          // 2D canvas 辅助类
          class Canvas2D{
              // ...
              // ================= 方法 =================
              // 设定绘图样式
              set_draw_style(border = "#000",fill = "#fff",border_width = 1,dash = null){
                  let ctx =  this.ctx;
                  ctx.beginPath();
                  // 样式设定
                  ctx.strokeStyle = border;      // 轮廓样式
                  ctx.lineWidth = border_width;  // 轮廓线宽
                  ctx.fillStyle = fill;          // 填充样式
                  if(dash != null && dash.length>0){   //虚线样式
                      ctx.setLineDash(dash);
                  }
              }
              // 填充和轮廓绘制
              stroke_and_fill(border = "#000",fill = "#fff",border_width = 1){
                  let ctx =  this.ctx;
                  if(border != null && border_width > 0){
                      ctx.stroke();
                  }
                  if(fill != null){
                      ctx.fill();
                  }
              }
              draw_circle(cx,cy,radius,border = "#000",fill = "#fff",border_width = 1,dash = null){
                  let ctx =  this.ctx;
                  // 设定绘图样式
                  this.set_draw_style(border,fill,border_width,dash);
                  // 主体路径
                  ctx.arc(cx,cy,radius,0,Math.PI * 2);
                  // 填充和轮廓绘制
                  this.stroke_and_fill(border,fill,border_width)
              }
          }
          

          进一步的还可以改进为:

          // Canvas2D.js
          // 2D canvas 辅助类
          class Canvas2D{
              // ...
              // ================= 方法 =================
              // 设定绘图样式
              set_draw_style(border = "#000",fill = "#fff",border_width = 1,dash = null){
                  let ctx =  this.ctx;
                  ctx.beginPath();
                  // 样式设定
                  ctx.strokeStyle = border;      // 轮廓样式
                  ctx.lineWidth = border_width;  // 轮廓线宽
                  ctx.fillStyle = fill;          // 填充样式
                  if(dash != null && dash.length>0){   //虚线样式
                      ctx.setLineDash(dash);
                  }
              }
              // 填充和轮廓绘制
              stroke_and_fill(border = "#000",fill = "#fff",border_width = 1){
                  let ctx =  this.ctx;
                  if(border != null && border_width > 0){
                      ctx.stroke();
                  }
                  if(fill != null){
                      ctx.fill();
                  }
              }
              draw_circle(cx,cy,radius){
                  let ctx =  this.ctx;
                  // 主体路径
                  ctx.arc(cx,cy,radius,0,Math.PI * 2);
              }
          }
          

          两种方式各有利弊吧,第二种形式更像是将原来的设定样式工作和填充和轮廓绘制工作整体封装起来,绘图函数的参数也可以更加简化。

          我更倾向于第一种设计,因为每绘制一个图形只需要调用一个方法,而不是两个。

          绘制折线和多边形


          Javascript不定参数函数设计

          在ES6之前,JavaScript中处理不定参数的常用方法是使用arguments对象,ES6引入了Rest参数,用来创建更清晰和简洁的代码。Rest参数通过在参数名前加上…来表示,它将所有剩余的参数收集到一个数组中。


          // Canvas2D.js
          // 2D canvas 辅助类
          class Canvas2D{
               // 直线和折线
              draw_polyline(points,close = false,border = "#000",fill = "#fff",border_width = 1,dash = null){
                  let ctx =  this.ctx;
                  if(points.length>3 && points.length % 2 == 0){  // 至少有2个点,而且的数量是2的整数倍
                      // 设定绘图样式
                      this.set_draw_style(border,fill,border_width,dash);
                      // 绘图线段
                      for(let i=0;i
                          if(i % 2 == 0){ //偶数项
                              let x = points[i];
                              let y = points[i+1];
                              this.ctx.lineTo(x,y);
                          }
                      }
                      // 设定路径是否闭合
                      if (close == true){this.ctx.closePath();}
                       // 填充和轮廓绘制
                      this.stroke_and_fill(border,fill,border_width)
                  }
              }
              // 多边形
              draw_polygon(points,border = "#000",fill = "#fff",border_width = 1,dash = null){
                  this.draw_polyline(points,true,border,fill,border_width,dash);
              }
          }
          
              // 事件处理代码
          }
          canvas.canvas.addEventListener("mousedown",function(e){
              // 事件处理代码
          })
          
              console.log(e.clientX,e.clientY);
          })
          
              var rect = canvas.canvas.getBoundingClientRect();
              console.log(e.clientX - rect.x,e.clientY - rect.y);
          })
          
              constructor(width,height,p_node = document.body){...}
              // ================= 方法 =================
              get_rect(){ // 获取矩形边界框
                  return this.canvas.getBoundingClientRect();
              }
              to_local(x,y){ // 将全局坐标转换为canvas局部坐标
                  var rect = this.get_rect();
                  return [x - rect.x,y - rect.y];
              }
              draw_circle(cx,cy,radius,border = "#000",fill = "#fff"){...}
              draw_polyline(border,fill,close = false,...positions){...}
              draw_polygon(border,fill,...positions){...}
          }
          
              console.log(canvas.to_local(e.clientX,e.clientY));
          })
          
              constructor(width,height,p_node = document.body){...}
              // ================= 方法 =================
              get_rect(){...}
              to_local(x,y){...}
              clear(){ // 清空画布
                  this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)
              }
              draw_circle(cx,cy,radius,border = "#000",fill = "#fff"){...}
              draw_polyline(border,fill,close = false,...positions){...}
              draw_polygon(border,fill,...positions){...}
          }
          
              canvas.clear();
          })
           //主体绘制部分
              canvas.draw_hlines(30,"#ccc");
              canvas.draw_vlines(30,"#ccc");
              canvas.draw_hlines(6,"#444");
              canvas.draw_vlines(6,"#444");
          }
          // 鼠标移动
          canvas.canvas.addEventListener("mousemove",function(e){
              canvas.clear();
              draw();
              canvas.draw_help_lines(e.clientX,e.clientY);
          })
          
              const img = new Image();
              
              img.onload = function(){
                  ctx.drawImage(img,20,20,300,300)
              }
              img.src = "godot.png";
          }
          draw();
          
          pimg src="https://i-blog.csdnimg.cn/img_convert/2ecf5105e0ca7594b3e3310f1c663627.png" alt="" width="300" //p h3全屏/h3 h3requestAnimationFrame/h3 p前端 - 浅析requestAnimationFrame的用法与优化 - 个人文章 - SegmentFault 思否/p pre class="brush:python;toolbar:false"const animation = () => { // 绘制代码 requestAnimationFrame(animation); //结束后调用 } requestAnimationFrame(animation); // 第一次调用

          基于requestAnimationFrame的动画

          // draw.js
          // 创建并添加一个canvas
          let canvas = document.createElement("canvas");
          canvas.width = 200;
          canvas.height = 200;
          document.body.appendChild(canvas);
          let ctx = canvas.getContext("2d");
          // 矩形的起点和宽高
          let x= 0;
          let y= 0;
          let w= 50;
          let h= 50;
          var deltaX = 1;
          // 绘制函数
          function draw(){
              // 绘制逻辑
              ctx.clearRect(0,0,canvas.width,canvas.height); // 清除上一帧绘制内容
              if(x > canvas.width - w || x  
          

          实现了动画:

          【Web】HTML5 Canvas 2D绘图的封装

          save()和restore()

          canvas理解:一看就懂的save和restore_canvas save restore-CSDN博客

          • save()保存上下文的边线、填充以及线性变换状态,每次保存状态压入栈内
          • restore()弹出并恢复栈顶的上下文状态
            // draw.js
            // 创建并添加一个canvas
            let canvas = document.createElement("canvas");
            canvas.width = 200;
            canvas.height = 200;
            document.body.appendChild(canvas);
            let ctx = canvas.getContext("2d");
            ctx.fillStyle = "red"
            ctx.fillRect(0,0,100,100);
            ctx.save(); // 保存状态
            ctx.translate(50,50);
            ctx.fillStyle = "yellow"
            ctx.fillRect(0,0,100,100);
            ctx.restore() //恢复状态
            ctx.fillStyle = "green"
            ctx.fillRect(0,0,50,50);
            
            • ctx.translate(50,50);是对画布的绘制位置进行了偏移,类似于GDScript中CanvasItem的set_tramsform()用法。

              完整代码

              // Canvas2D.js
              // 2D canvas 辅助类
              class Canvas2D{
                  constructor(width,height,p_node = document.body){
                      this.canvas = document.createElement("canvas");
                      this.ctx = this.canvas.getContext("2d");
                      this.canvas.width = width;
                      this.canvas.height = height;
                      p_node.append(this.canvas);
                  }
                  set width(val){
                      this.canvas.width = val;
                  }
                  get width(){
                      return this.canvas.width;
                  }
                  set height(val){
                      this.canvas.height = val;
                  }
                  get height(){
                      return this.canvas.height;
                  }
                  // ================= 绘图样式 =================
                  // 设定绘图样式
                  set_draw_style(border = "#000",fill = "#fff",border_width = 1,dash = null){
                      let ctx =  this.ctx;
                      ctx.beginPath();
                      // 样式设定
                      ctx.strokeStyle = border;      // 轮廓样式
                      ctx.lineWidth = border_width;  // 轮廓线宽
                      ctx.fillStyle = fill;          // 填充样式
                      if(dash != null && dash.length>0){   //虚线样式
                          ctx.setLineDash(dash);
                      }else{
                          ctx.setLineDash([]);
                      }
                  }
                  // 填充和轮廓绘制
                  stroke_and_fill(border = "#000",fill = "#fff",border_width = 1){
                      let ctx =  this.ctx;
                      if(border != null && border_width > 0){
                          ctx.stroke();
                      }
                      if(fill != null){
                          ctx.fill();
                      }
                  }
                  // ================= 基础图形绘制 =================
                  // 圆
                  draw_circle(cx,cy,radius,border = "#000",fill = "#fff",border_width = 1,dash = null){
                      let ctx =  this.ctx;
                      // 设定绘图样式
                      this.set_draw_style(border,fill,border_width,dash);
                      // 主体路径
                      ctx.arc(cx,cy,radius,0,Math.PI * 2);
                      // 填充和轮廓绘制
                      this.stroke_and_fill(border,fill,border_width)
                  }
                  // 直线和折线
                  draw_polyline(points,close = false,border = "#000",fill = "#fff",border_width = 1,dash = null){
                      let ctx =  this.ctx;
                      if(points.length>3 && points.length % 2 == 0){  // 至少有2个点,而且的数量是2的整数倍
                          // 设定绘图样式
                          this.set_draw_style(border,fill,border_width,dash);
                          // 绘图线段
                          for(let i=0;i
                              if(i % 2 == 0){ //偶数项
                                  let x = points[i];
                                  let y = points[i+1];
                                  this.ctx.lineTo(x,y);
                              }
                          }
                          // 设定路径是否闭合
                          if (close == true){this.ctx.closePath();}
                           // 填充和轮廓绘制
                          this.stroke_and_fill(border,fill,border_width)
                      }
                  }
                  // 多边形
                  draw_polygon(points,border = "#000",fill = "#fff",border_width = 1,dash = null){
                      this.draw_polyline(points,true,border,fill,border_width,dash);
                  }
                  // ================= 矩形与坐标 =================
                  get_rect(){ // 获取矩形边界框
                      return this.canvas.getBoundingClientRect();
                  }
                  full_screen(){ // 全屏 - 设置canvas的尺寸为页面尺寸
                     this.width =  window.innerWidth;
                     this.height =  window.innerHeight;
                  }
                  to_local(x,y){ // 将全局坐标转换为canvas局部坐标
                      var rect = this.get_rect();
                      return [x - rect.x,y - rect.y];
                  }
                   // ================= 矩形与坐标 =================
                  clear(){ // 清空画布
                      this.ctx.clearRect(0,0,this.width,this.height)
                  }
                  // ================= 网格线 =================
                  draw_hlines(num,border = "#000",border_width = 1,dash = null){  // 绘制水平间隔线
                      const dh = this.height / num;
                      const w = this.width;
                      for(let i =0;i
                          this.draw_polyline([0,dh * i,w,dh * i],false,border,null,border_width,dash)
                      }
                  }
                  draw_vlines(num,border = "#000",border_width = 1,dash = null){  // 绘制垂直间隔线
                      const dw = this.width / num;
                      const h = this.height;
                      for(let i =0;i
                          this.draw_polyline([dw * i,0,dw * i,h],false,border,null,border_width,dash)
                      }
                  }
                  // ================= 辅助线 =================
                  draw_help_lines(x,y,border = "orange",border_width = 1,dash = null){ // 绘制水平和垂直辅助线
                      const h = this.height;
                      const w = this.width;
                      const local = this.to_local(x,y);
                      this.draw_polyline([0,local[1],w,local[1]],false,border,null,border_width,dash)
                      this.draw_polyline([local[0],0,local[0],h],false,border,null,border_width,dash)
                  }
              }
              
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

相关阅读

目录[+]

取消
微信二维码
微信二维码
支付宝二维码