canvas掌握这些"或许"就够了

284 阅读5分钟

1.描述

首先混个脸熟,canvas?干啥的,为啥要用。打开疑问:canvas画图的。问:有图片,有我们漂亮的UI设计师,为什么要自己画?答:canvas可以动态生成图片,虽然你可以拿到各式各样的图片,但是你能拿到我想要的文字加你的图片吗?显然不能。比如面对不同的用户,都显示xxx,早上好。普通的照片是做不到,需要我们自己手动去画,常见的是文字配二维码。后面有实例不过多详细说明。本篇文章主要基于对canvas的初体验,对于小白前端而言“或许”就够了,如果需要深入学习请前往官或者网书籍等详细学习,谢谢。

2.常见的2d画图工具canvas与svg比较

比较canvassvg
维度2d2d
类型位图矢量图
图形纯代码(js)标签
操作麻烦简便
性能性能极高一般:跟html标签
用途高性能重交互
应用场景游戏、大型图表、图片处理地图、小型图表、图标

解释:

canvas:位图(存的是像素——放大模糊),所有代码路径操作全部是由js完成,在画30000个路径以上时就会有一点点卡顿。

svg:矢量图(存的是路径——放大依然清晰),所有代码由html标签完成,在html中会渲染很长时间,一般画2000个标签时就会卡顿。

总结:各有优缺点,canvas可以说是svg的十多倍性能,以下以canvas为主,想学习svg请自行学习。

3.绘图样式、操作顺序

  1. canvas标签

#c1 {       border: 1px dashed black;       display: block;       margin: 5px auto 0;     }

<canvas id="c1" width="800" height="600">     你的浏览器不支持canvas,请升级或更换 <a href="chrome.google.com/">下载 </canvas>

内容:只有浏览器不识别canvas才会显示canvas标签中的内容。width、height必须是属性 决定了canvas的绘图范围viewbox,style加给canvas——拉伸canvas,这句话什么意思呢?在canvas中,只能在标签内定义宽高,不能再style中定义宽高,就算定义了,也只是放大或者缩小,没有意义。

  1. 图形上下文

const gd=canvas.getContext('type') type: 2d, webgl

  1. 图形操作-canvas中一切是路径

  路径——选定范围(不会真的画东西),后续绘图操作(stroke、fill)

  绘制——直接画出东西 stroke

  1. 线(一切都是点位操作)

  moveTo(x,y)    起点

  lineTo(x,y)    终点(多次划线也是下一笔的起点)

  closePath() 手动闭合路径

  1. 绘图操作

  stroke()    边框

  fill()      填充:自动闭合(最后一个路径点,到起点)

4.Line操作、路径操作

  1. 路径

  moveTo

  lineTo

  closePath

2.绘图

  stroke

  Fill

边线颜色    strokeStyle

边线宽度    lineWidth

填充颜色    fillStyle

strokeStyle=任何css颜色

名称:red、blue、green...

16进制:#C00、#CC0000

rgba:rgb(x,x,x)、rgba(x,x,x,1)

eg: gd.strokeStyle = "rgba(255, 0, 255, 0.1)";

【!!!顺序的重要性】

strokeStyle需要在stroke()的前面

strokeStyle只会影响后续的操作,对于已经完成的操作完全没影响

样式:

1.strokeStyle // 描边样式

2.fillStyle // 填充样式

3.lineWidth // 线宽

!!!重要:顺序

上代码:

    let canvas = document.getElementById('c1')
    let gd = canvas.getContext('2d')
    gd.moveTo(100, 100);
    gd.lineTo(300, 200);
    gd.lineTo(100, 200)
    gd.strokeStyle = "rgb(255, 0, 255)";   // 线条颜色
    gd.fillStyle = 'red';  // 填充颜色
    gd.lineWidth = 5;   // 线宽
    gd.closePath()     // 闭合
    gd.fill()        // 填充
    gd.stroke();

线路操作.jpg

5.渐变

  1. 创建线性渐变对象

gd.createLinearGradient(x1, y1, x2, y2) // 点位代表起点、终点的渐变过程

    let canvas = document.getElementById('c1')
    let gd = canvas.getContext('2d')
    gd.moveTo(100, 100);
    gd.lineTo(300, 200);
    gd.lineTo(100, 300);
    // 创建线渐变对象
    const gradient = gd.createLinearGradient(
      100, 100,
      300, 200
    );
    // 添加渐变点
    gradient.addColorStop(0, '#fff');
    gradient.addColorStop(0.24, '#f00');
    gradient.addColorStop(1, '#0f0');
    gd.fillStyle = gradient;
    gd.fill();
    gd.stroke();
    

linear.jpg

  1. 创建圆形渐变 gd.createRadialGradient(x1, y1, r1,x2, y2, r2)

        let canvas = document.getElementById('c1') 
        let gd = canvas.getContext('2d')
        gd.rect(50,50,600,400)  // 画了一个矩形描边
        const gradient = gd.createRadialGradient(
          100, 100, 100,
          300, 200, 50
        );
        //添加渐变点
        gradient.addColorStop(0, '#CCC');
        gradient.addColorStop(0.24, '#f00');
        gradient.addColorStop(1, '#0f0');
        gd.fillStyle = gradient;
        gd.fill();
        gd.stroke();
    

gradient.jpg

嗯?很懵,咋做到的。来看一下原理

gradientP.jpg

原理: 两个点位圆相切,两个圆心之间的连线为0.5 一看就懂了,不多说。

6. 文字

1.绘制文字

  gd.fillText(txt, x, y) // 真实的写文字

  gd.strokeText(txt, x, y) // 这个只是描边文字

2.font属性

  gd.font='20px 字体';

  gd.font='bold italic 20px 字体';

和css一样,一看就懂

3.默认原点(左上角)

  但是中文文字在左下角

        gd.font='bold italic 20px 字体';
        gd.fillText('你好',0,20) // 真实的写文字 20为字体大小
        gd.strokeText('你好', 0, 40) // 这个只是描边文字

  txt.jpg

 

4.文字原点

  默认:left alphabetic

  gd.textAlign = 'left'||'center'||'right'; // 文字对齐方式

注意这里的对齐方式,不是文字居中画布,而是中心点位于文字的左中右。

  gd.textBaseline = 'alphabetic'||'bottom'||'top'||'middle'; 文字基线

alphabetic:在英文的时候y、g等会超出下面一部分,这个基线就是针对英文的正常基线。

  text.jpg

  1. 多行文字 描述: 当有多行文本的时候怎么办呢?canvas不会自动换行,不可能一个像素一个像素取算吧?

measureText(str)为canvas的一个方法可以计算文字的长度,想一想,如果有文字长度加上画布长度,是不是就可以每一行放多少了呢?

上代码:

        const fontSize = 20;
        const lineHeight = fontSize \* 2;
        gd.font = `${fontSize}px 宋体`;
        gd.textAlign = 'center';
        gd.textBaseline = 'top';

        const str = '声明显示,深圳市智信新信息技术有限公司,由深圳市智慧城市科技发展集团与30余家荣耀代理商、经销商共同投资设立,包括天音通信有限公司、苏宁易购集团股份有限公司、北京松联科技有限公司、深圳市顺电实业有限公司、山东怡华通信科技有限公司、深圳冀顺通投资有限公司、河南象之音健康科技有限公司、福建瑞联优信科技有限公司、内蒙古英孚特通讯技术有限公司、科技发展有限公司等。';
        const lines = [];
        let line = '';
        for (let i = 0; i < str.length; i++) {
          if (gd.measureText(line + str[i]).width > canvas.width) {
            lines.push(line);
            line = '';
          }
          line += str[i];
        }
        lines.push(line);
        lines.forEach((line, index) => {
          gd.fillText(line, canvas.width / 2, lineHeight * index + (lineHeight - fontSize) / 2);
        });
    

lineText.jpg

  1. 线

lineCap (划线的时候头部样式)

  'butt'  默认    没有

  'round'        圆

  'square'       正方形

lineJoin (链接样式)

  'miter' 默认

  'bevel' 折叠

  'round'

eg: 以bevel折叠为例

            gd.beginPath();
            gd.moveTo(100, 100);
            gd.lineTo(300, 200);
            gd.lineTo(100, 300);
            gd.lineWidth=10
            gd.lineCap='square'
            gd.lineJoin = 'bevel'
            gd.stroke()
            gd.beginPath()
            gd.moveTo(300, 300);
            gd.lineTo(400, 200);
            gd.lineTo(600, 300);
            gd.lineWidth=10
            gd.lineCap='square'
            gd.lineJoin = 'round'
            gd.stroke()

join.jpg

理解路径

路径——操作范围

        gd.moveTo(100, 100);
        gd.lineTo(300, 200);
        gd.lineWidth=10
        gd.strokeStyle='red'
        gd.stroke()
        // gd.beginPath()
        gd.strokeStyle='yellow'
        gd.moveTo(300, 300);
        gd.lineTo(400, 400);
        gd.lineWidth=10
        gd.stroke()

linea.jpg

放开注释 为什么需要这一步? 你那一支笔,不换一个颜色就是上面的,如果画好了,在拿黄笔画就是第二张图 lineb.jpg

beginPath——清除已有的路径(开始一个全新的)

习惯:一定先beginPath,然后再开始操作路径

canvas绝大多数的属性,都是“全局” 比如: gd.globalAlpha=0.2 设置全局透明度,干啥?用来设置后续操作的透明度(绘制完要恢复回来)

7.阴影

1.颜色  shadowColor

2.范围  shadowBlur

3.偏移  shadowOffsetX,shadowOffsetY

        let canvas = document.getElementById('c1')
        let gd = canvas.getContext('2d')
        gd.shadowColor = 'rgba(0,0,0,1)';
        gd.shadowBlur = 4;
        gd.shadowOffsetX = 4;
        gd.shadowOffsetY = 4;
        drawRect(100, 100, 400, 300,'red');
        drawRect(20, 200, 30, 500,'yellow');
        drawRect(150, 200, 10, 300,'green');
        // 自己写的方法,可以用gd.fillRect()直接画矩形
        function drawRect(x, y, w, h,color) {
          gd.beginPath();
          gd.fillStyle=color
          gd.moveTo(x, y);
          gd.lineTo(x + w, y);
          gd.lineTo(x + w, y + h);
          gd.lineTo(x, y + h);
          gd.closePath();
          gd.fill();
        }

shallow.jpg

8.画椭圆、圆、饼图

  1. 圆/弧 注意(平常我们画圆起始点在顶上为(0,0),在canvas中正右方为0,0) 如果需要调整为肉眼的正上方需要回转90°,后面例子可见

ellipse     椭圆弧 (如果会画椭圆,name就会画圆,x=y=r)

gd.ellipse(

  //圆心   cx, cy,

  //半径   rx, ry,

  //顺时针旋转   rotation,

  //角度   startAng, endAng,

  //是否逆时针   anticlock(可选参数) )

arc         正圆弧

圆没有旋转概念,怎么旋转都一样

gd.arc(   cx, cy,   r,   startAng, endAng,   [anticlock](可选参数) )

    gd.beginPath()
    gd.ellipse(
        300,300,200,100,degree2arc(20),0,degree2arc(360) //??? 什么鬼
    )
    gd.stroke()
    gd.beginPath()
    gd.arc(
        500,100,100,0,degree2arc(360) // ?? 什么鬼
    )
    gd.stroke()
    
    // 度->弧度
    function degree2arc(n) {
      return n * Math.PI / 180;
    }
    // 弧度->度
    function arc2degree(n) {
      return n * 180 / Math.PI;
    }
    

round.jpg

在上面看到代码有??了吗?现在回忆一下学数学那会儿,圆都是弧度,所以计算机需要转一下

度    0~360

弧度  0~2*PI

换算:

360度=2*PI弧度

1度=2*PI/360弧度=PI/180弧度

n度 = n*PI/180弧度

2*PI弧度=360度

1弧度=180/PI度

n弧度=n*180/PI度

度->弧度    n*PI/180

代码写了两个函数就是这个意思,如果不套用函数gd.ellipse( 300,300,200,100,degree2arc(20),0,360) )其实有很多个圆,请自行尝试

  1. 饼图

思考,怎么画饼图呢?既然能画弧,那必然有参数可以画半圆弧。没问题,尝试一下

    gd.beginPath()
    gd.moveTo(200,300)
    gd.arc(
        300,300,100,degree2arc(0-90),degree2arc(90-90)
    )
    gd.stroke()
    // 第二个
    gd.beginPath()
    gd.moveTo(400,500)
    gd.arc(
        500,500,100,degree2arc(0-90),degree2arc(90-90)  // 上面注意说了,canvas原点在最右边,-90就是直观的顶点为起点
    )
    gd.closePath()  闭合
    gd.stroke()
    

bing.jpg

既然扇形出来了,那饼图不就简单啦。举个例子

饼图

100, 50, 200    350

//占比

28.57%

14.28%

57.14%

//角度

102.85°

51.4°

205.7°

//start, end

0, 102.85

102.85, 154.25

154.25, 360

上面大家都懂,不说了 上代码

    const cx = canvas.width / 2,
      cy = canvas.height / 2,
      r = 200;

    //数据
    const data = [
      { value: 50, color: 'red' },
      { value: 150, color: 'green' },
      { value: 200, color: 'pink' },
      { value: 300, color: '#CCC' },
      { value: 500, color: 'yellow' },
    ];

    let total = 0;
    data.forEach(item => {
      total += item.value;
    });
    
    let base = 0;
    data.forEach(item => {
      let ang = 360 \* item.value / total;
      drawPie(base, base + ang, item.color);
      base += ang;
    });
    function drawPie(startAng, endAng, color) {
      gd.beginPath();
      gd.moveTo(cx, cy);
      gd.arc(
        cx, cy, r,
        degree2arc(startAng - 90), degree2arc(endAng - 90),
        false,
      );
      gd.closePath();
      gd.fillStyle = color;
      gd.fill();
    }

bingtu.jpg

如果再想配文字,就自己去配了,当然这只是原理,如果使用饼图,可以看一下echarts,这里就不描述了。

9.曲线

1.贝塞尔曲线

bezierCurveTo(   x1, y1,   x2, y2,   x, y ) 相信大家都了解ps钢笔绘图吧,那个就是贝塞尔曲线,起点到终点画图,两个控制点。

    gd.beginPath();
    gd.moveTo(100, 100); 起点
    gd.bezierCurveTo(
      300, 100,  // 控制点1
      100, 300,  // /控制点2
      300, 300   // 终点
    );
    gd.stroke();

besiz.jpg

原理如下(感兴趣可以去研究一下)这里就不说了

bezierCurve.gif

2.曲线

quadraticCurveTo(   x1, y1,   x, y )一个结束点,一个控制点

    gd.beginPath();
    gd.moveTo(100, 100); // 起点
    gd.quadraticCurveTo(
      280, 100, // 控制点,可以想象这里是一个磁铁,在吸引
      300, 300 // 结束点
    );

    gd.stroke();

quadratic.jpg

总结:这里不是几何学,如果喜欢曲线可以自行去学习,这里了解即可。

10.开发中canvas实战,加上画图片

在h5中二维码保存:

// 这个是文字加二维码

5711a842fc962dbf8d81f39e74d4905d.jpg

// 这张图是由图片加画二维码加文字

2b45004ddd5c40155f8390e4af6bcf46.jpg

在手机上请把二维码保存到相册,带上这条数据的动态标题(最开始就说了,图片做不到吧。需要手动画)

直接上代码:(这里以第二张图为例)

    // 这里有两种方法,第一是直接用canvas画图片,第二是创建canvas然后转换为地址,在放到img中去。这里我使用第二种,为什么呢?因为在手机上,canvas没法保存,只能长按图片保存,所以采用第二种,转换canvas为图片src塞到img中
    <van-overlay :show="showQrCode" z-index="100" @click="showQrCode = false">  
          <div id="container" class="image-code">  
                <div>  
                 <img id="qrcode" class="qr" :src="temUrl" alt >  
                  <div class=" flex justify-center margin-top">  
                    <span style="color: white;">  
                      请长按保存图片  
                    </span>  
                  </div>  
                </div>  
          </div>  
        </van-overlay>
    async checkQr () {  
          this.showQrCode = true  
          let url = 'xxx,写自己的地址及参数'
          const res = await createQrCode({   // 这里和后端配合拿到二维码,带logo的,如果不带logo可以使用三方组件qrCode包,生成一个二维码地址
            contents: url,  
            width240,  
            height240  
          })  
          this.createCanvas(res)  
        },  
        async createCanvas () { 
          let canvas = document.createElement('canvas')  
          // 获取到屏幕倒是是几倍屏。  这里做适配
          let getPixelRatio = function (context) {  
            let backingStore = context.backingStorePixelRatio ||  
            context.webkitBackingStorePixelRatio ||  
            context.mozBackingStorePixelRatio ||  
            context.msBackingStorePixelRatio ||  
            context.oBackingStorePixelRatio ||  
            context.backingStorePixelRatio || 1  
            return (window.devicePixelRatio || 1) / backingStore  
          }  
          // iphone6下得到是2  
          const pixelRatio = getPixelRatio(canvas)  
          // // 设置canvas的真实宽高  
          let canvasCtx = canvas.getContext('2d')  
          let img = document.createElement('img')  
          // 这里看个人,根据ui去量
          canvas.width = pixelRatio * 120  
          canvas.height = pixelRatio * 147  
          // 获取二维码code,和后端配合
          const res = await getMiniCode({  
            scenethis.$route?.query?.orgId,  
            page'pagesSecondHouse/shop/shop/index',  
            appId'wx0e3787020d00b980'  
          })  
          img.src = res  
          let image = new Image()  
          // 插入底图
          image.src = 'xxx.png'  
          // 再画网络url图片时,请加上下面这句话,不然canvas告诉你跨域了,下面代码意思是允许跨域
          image.setAttribute('crossOrigin''Anonymous')  
          setTimeout(async () => {  
            canvasCtx.beginPath()  
           //画底图
           canvasCtx.drawImage(image, pixelRatio * 6, pixelRatio * 0, pixelRatio * 110, pixelRatio * 137)  
            canvasCtx.beginPath()  
            canvasCtx.font = '15px 宋体'  
            canvasCtx.fillStyle = '#fff'  
            canvasCtx.textAlign = 'center'  
            canvasCtx.fillText(this.$route.query?.orgName, (canvas.width / 2), pixelRatio * 33)  
            canvasCtx.beginPath()  
           // 画二维码
           canvasCtx.drawImage(img, pixelRatio * 30, pixelRatio * 40, pixelRatio * 60, pixelRatio * 60)  
            // 将画好的canvas装换成src用img展示temUrl
            this.temUrl = canvas  
              .toDataURL('image/png')  
              .replace('image/png''image/octet-stream')  

          }, 1000)  
        },
        
        
        
        // 到此就已经画出来了

        !!! 如果实在网页端,可以点击保存canvas图片就不用转img了,直接上canvas
        
        // 网页端保存图片调用方法
        // url 请使用let url=canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream') 生成
        this.saveImgFile(  url,  filename || `file_${new Date().getTime()}.png`  )  
        saveImgFile (data, filename) {  
        let eleLink = document.createElement('a'// 下面就是保存的步骤了,很简单 自行看看  
        eleLink.href = data // 转换后的图片地址  
        eleLink.download = 'xx.jpg' // 自己取个名字
        document.body.appendChild(eleLink)  
        // 触发点击  
        eleLink.click()  
        // 然后移除  
        document.body.removeChild(eleLink)
        
        

全文总结:如果不涉及复杂业务,你掌握这篇canvas对你“或许”够了,如果还需要深华,请自行学习。