你不知道的 canvas(一)

一、前言

hello 大家好~ 。今天给大家介绍一个在前端被广泛运用的东西——canvas,canvas 在动画、游戏画面、数据可视化、图片编辑、实时视频处理等方面都扮演着一个非常重要的角色。常见的 canvas 制作 H5 小游戏,还有应用比较广泛的 echarts 图表也是由 canvas 绘制而成的,诸如此类的图表有很多很多。所以说 canvas 是一个有趣、运用广泛、功能强大的一个东西。

二、什么是 canvas?

canvas 是 html 的一个标签,它可以结合 JavaScript 提供的 canvasApi 来绘制各种各样的图形。canvas 主要用于绘制 2D 图形。注意:当你不设置 canvas 的宽高时,它的默认宽高是 300px、150px,而且当通过 CSS 样式设置 canvas 宽高而不是直接通过 width、height 属性设置宽高时图形会发生伸缩

canvas 的简单使用(绘制一个红色的长方形)

<canvas id="canvas"></canvas>
<script>
    // 获取 canvas 元素的引用
    let canvas = document.getElementById('canvas')
    // 获取 canvas 元素的 context,图形将绘制在元素的 context 上。
    let context = canvas.getContext('2d')
    // 填充颜色
    context.fillStyle = 'red'
    // 绘制长方形 参数:相对于浏览器左上角的坐标位置 x,y 以及矩形的宽高四个参数
    context.fillRect(0, 0, 200, 100)
</script>

三、canvas 的预留内容

当在不支持 canvas 标签的浏览器中,canvas 开始标签和结束标签之间的内容会被显现出来。注意:canvas 必须含有结束标签,没有结束标签虽然不会报错但是文档的其他内容会被当做预留内容。

<!-- 不支持 canvas 的浏览器将会显示标签内的内容 -->
<canvas>当前浏览器不支持canvas</canvas>

四、绘制形状

通过以上的简单例子我们已经学会了如何开始绘制一个图形,接下来我们来了解其他形状的绘制。

4.1位置坐标概念

在以上的例子中我们用到了坐标位置 x,y 注意该坐标位置相对于 canvas 的左上角来设定的(水平方向为 x 垂直方向为 y),而不是整个文档的左上角。

4.2绘制矩形

<body>
    <canvas id="canvas"></canvas>
    <script>
        let canvas = document.getElementById('canvas');
        let ctx = canvas.getContext('2d')
        // 绘制相对于画布左上角坐标为(x, y)->(50px, 50px) 且宽高都为 100px 的填充矩形。
        ctx.fillRect(50,50,100,100)
        // 清除一个指定的矩形区域,将该区域变为透明。清除的区域起点为(60px, 60px),宽高为 80px 的区域。
        ctx.clearRect(60, 60, 80, 80)
        // 绘制相对于画布左上角坐标为(x, y)->(75px, 75px) 且宽高都为 50px 的矩形边框。
        ctx.strokeRect(75 ,75 , 50, 50)
        // 绘制相对于画布左上角坐标为(x, y)->(160px, 50px) 且宽高都分别为 90px、100px 的矩形边框。
        ctx.rect(160, 50, 90 , 100)
        ctx.stroke()
    </script>
</body>

image

4.3绘制路径

路径是图形的基本元素,路径是由不同颜色、宽度的线段或曲线相连形成不同形状的点的集合。

绘制路径的步骤:

  • 创建路径的起点 -> 使用 beginPath() 新建一条路径,结束前一条路径的绘制。
  • 使用画图方法画出路径
  • 封闭路径 -> 使用 closePath() 封闭路径。fill() 也会自动封闭路径,但 stroke() 不会
  • 封闭路径后就可以通过描边或填充路径区域来进行图形渲染。-> stroke() 绘制图形轮廓,fill() 填充路径形成的区域。
<!-- 绘制一个三角形 -->
<body>
    <canvas id="canvas"></canvas>
    <script>
        let canvas = document.getElementById('canvas')
        let ctx = canvas.getContext('2d')
        // 创建一条路径
        ctx.beginPath()
        ctx.moveTo(50, 50)
        ctx.lineTo(80, 80)
        ctx.lineTo(20, 80)
        ctx.fill()
    </script>
</body>

image

以上案例涉及到了两个很有用的方法:moveTo() 、 lineTo():

  • moveTo(x, y): 它不会绘制任何图形,它的作用是改变画笔笔尖的落点,将笔尖从当前位置移到指定的(x,y)坐标点。
  • lineTo(x, y): 用以绘制一条从当前位置到(x, y)点的直线。

stroke() 与 fill() 区别,再来看个例子。

以下案例绘制了两个三角形来测试 fill() 和 stroke() 的区别,第一前面已经提到过 fill() 用来填充路径区域,stroke() 用来描边。第二 fill() 会自动关闭路径所以会形成以下填充好颜色的三角形,然而 stroke() 不会自动关闭路径,所以形成了一个不完整的三角形,需要手动 closePath() 后才会绘制一个完整的三角形。注意:关闭路径不是结束一条路径的绘制,结束一条路径的绘制需要开启一条新的路径(beginPath)。

let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d')
// 填充三角形
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(80, 80);
ctx.lineTo(20, 80);
ctx.fill();
// 不完整描边三角形
ctx.beginPath();
ctx.moveTo(120, 50);
ctx.lineTo(90, 80);
ctx.lineTo(150, 80);
// ctx.closePath();
ctx.stroke();

image

4.4 绘制圆弧

使用 arc() 绘制圆弧或者圆,它接受 6 个参数, arc(x, y, radius, startAngle, endAngle, anticlockwise) 表示以 x, y 为圆心 radius 为半径,起始弧度 startAngle,结束弧度为 endAngle 按照给定的方向 anticlockwise (anticlockwise 值为布尔值 true 则按逆时针绘制, false 则按顺时针绘制,默认为 false) 绘制一个圆弧或者圆。一个圆的弧度为 2π(js表示为:2*Math.PI),相信大家都知道,那么 startAngle、endAngle 的取值就大家的需求而定了。

let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d')
// 绘制弧度为 π/2 的圆弧
ctx.arc(50, 50, 30, 0, Math.PI/2, false)
ctx.stroke();
ctx.beginPath()
// 绘制一个圆
ctx.arc(150, 50, 30, 0, Math.PI*2, false)
ctx.stroke();

4.5 贝塞尔曲线绘制

我们这里主要介绍二次贝塞尔曲线和三次贝塞尔曲线,它们都是用于绘制曲线,以实现绘制复杂的图形。二次贝塞尔曲线由三个点控制绘制一条曲线。三次贝塞尔曲线由四个点控制绘制一条曲线。

更加深入了解贝塞尔曲线戳这里

  • 二次贝塞尔曲线
    简单的二次贝塞尔曲线绘制
let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d')
ctx.moveTo(50, 150)
ctx.quadraticCurveTo(200, 25, 350, 150)
ctx.stroke()

image

二次贝塞尔曲线是由三个点控制的,如下图,两个红色的点分别代表曲线的起点和终点,绿色的点是控制点,在起点终点不变的情况下控制点不同曲线的弯曲程度不同。曲线的绘制路径原理请参考上面我推荐的关于贝塞尔曲线的文章。
image

  • 三次贝塞尔曲线

三次贝塞尔曲线在二次贝塞尔曲线的基础上另增加了一个控制点,控制点越多绘制会越复杂,最终效果是比较难预测的,所以需要一次次尝试才会达到预期的效果。

let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d')
ctx.moveTo(50, 150)
ctx.bezierCurveTo(100, 25, 300, 25, 350, 150)
ctx.stroke()

image

下图两个红点代表两个控制点

image

4.6 Path2D 对象

为了简化代码和提高性能,在比较新的浏览器可以使用 Path2D 创建的路径在 canvas 上来绘制各种各样的图形 ,Path2D 创建的对象可以缓存绘制命令。Path2D() 会返回一个新的初始化的 Path2D 对象,也可以接受一个路径作为参数或 接受一个 SVG path 字符串。

以上提到绘制方法在 Path2D 中大多都有实现,打印 Path2D 对象结果如下:
image

  • 使用 Path2D 绘制矩形
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d')
// 绘制矩形
let pathObj = new Path2D()
pathObj.rect(10, 10, 100, 50)
ctx.stroke(pathObj)

image

  • Path2D 参数接受一个路径,在 pathObj 路径上接着画一个圆。而且 Path2D 缓存绘制路径的功能也体现出来了,pathObj 可以随时被拿来使用。
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d')
let pathObj = new Path2D()
pathObj.rect(10, 10, 100, 50)
let pathObj2 = new Path2D(pathObj)
pathObj2.arc(140, 25, 20, 0, 2*Math.PI)
ctx.stroke(pathObj2)

image

  • Path2D 参数接受一个矢量路径字符串, 例:"M20 20 h 100 v 100 h -100 Z", (M20 20)表示的是将路径的起点移至(20, 20),(h 100) 表示水平向右移动 100 像素,(v 100) 表示垂直向下移动 100 像素,(h -100) 表示水平向左移动 100 像素, (Z) 表示回到起点处。

矢量路径绘制正方形

let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d')
let pathObj = new Path2D('M20 20 h 100 v 100 h -100 Z')
ctx.stroke(pathObj)

image

五、样式和颜色

5.1 颜色样式

到此为止,以上内容我们主要讲了内容的绘制方法,如果我们想要给绘制的内容添加样式该通过什么方法呢,主要使用 fillStyle(设置填充颜色) 和 strokeStyle(设置路径颜色) 两个属性设置绘制颜色。它们的默认值都为 #000,所以以上绘制的图形填充色和轮廓颜色都为黑色。

绘制一个填充色为红色的矩形,在调用 fill() 之前设置 fillStyle()。

let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d')
ctx.rect(10, 10, 100, 50)
ctx.fillStyle = 'red'
ctx.fill()

image

绘制一个边框轮廓为蓝色的矩形

let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d')
ctx.rect(10, 10, 100, 50)
ctx.strokeStyle = 'blue'
ctx.stroke()

image

5.2 透明度

通过 RGBA 和 globalAlpha 两种方式可以设置颜色的透明度。RGBA 需要符合 CSS3 颜色规范的值。globalAlpha 的值为 0 ~ 1 之间。

  • 使用 RGBA 绘制背景色为红色透明度 50% 的矩形。
ctx.rect(10, 10, 100, 50)
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'
ctx.fill()

image

  • 使用 globalAlpha 设置透明度.
ctx.rect(10, 10, 100, 50)
ctx.fillStyle = 'red'
ctx.globalAlpha = 0.5
ctx.fill()

image

5.3 线条样式

  • 设置线的宽度 lineWidth, 值必须为正数,默认值为 1。
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d')
ctx.moveTo(10, 10)
ctx.lineWidth = 10
ctx.lineTo(10, 80)
ctx.stroke()

image

  • 设置线条端点的样式 lineGap, lineGap 有三个取值:butt、round、square,默认值为 butt

根据 lineGap 的三个不同取值绘制三条线条,如下图,中间的 lineGap 值为 round 左边的为 butt 右边的为 square。其中两条蓝色的两条线为辅助线,让三条垂直线条的区别更加明显。它们的区别主要体现在两个方面:

  • butt 和 square 的端点是直角拐点,round 的端点是一个圆弧
  • 在起点、终点都在辅助线上的情况下,round 和 square 会在原来的起点和终点的基础上分别延伸半个线条宽度的距离,round 延伸的是直径为线宽的半圆,square 延伸的是一个宽为线宽高为线宽一半的矩形。
let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d');
ctx.beginPath()
ctx.strokeStyle = 'blue';
ctx.moveTo(10, 10)
ctx.lineTo(150, 10)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(10, 120)
ctx.lineTo(150, 120)
ctx.stroke()
function drawLine(){
    let tyleList = ['butt','round','square'];
    ctx.strokeStyle = '#000';
    for (var i=0;i<3;i++){
        ctx.lineWidth = 15;
        ctx.lineCap = tyleList[i];
        ctx.beginPath();
        ctx.moveTo(25 + i*50, 10);
        ctx.lineTo(25 + i*50, 120);
        ctx.stroke();
    }
}
drawLine()

!image

  • lineJoin 设置线段拐点处的样式。它有三个取值:round、bevel、miter(默认值)。

根据 lineJoin 的不同取值绘制三条折线线段。如下图,最上面的路径 lineJoin 值为 round,中间的为 bevel,最下面的为 miter。红色的圆为辅助圆,直观的表现出 round 的连接点外侧为一个圆弧。

let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d');
function drawLine(){
    var lineJoin = ['round', 'bevel', 'miter'];
    ctx.lineWidth = 10;
    for (var i = 0; i < lineJoin.length; i++) {
        ctx.lineJoin = lineJoin[i];
        ctx.beginPath();
        ctx.moveTo(-5, 5 + i * 40);
        ctx.lineTo(35, 45 + i * 40);
        ctx.lineTo(75, 5 + i * 40);
        ctx.lineTo(115, 45 + i * 40);
        ctx.lineTo(155, 5 + i * 40);
        ctx.stroke();
    }
}
drawLine()
ctx.beginPath()
ctx.arc(35, 45, 5, 0, Math.PI*2)
ctx.fillStyle = 'red'
ctx.fill()

image

  • 虚线绘制,涉及到 setLineDash 方法和 lineDashOffset 属性。setLinedash 接受一个数组用于设置虚线线段和间隙的宽度,lineDashOffset 用于设置相对于虚线初始位置的偏移量。

绘制边框为虚线的矩形

let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d');
function drawDash(){
    ctx.setLineDash([4, 2])
    ctx.lineDashOffset = 0
    ctx.strokeRect(10, 10, 100, 40)
    ctx.setLineDash([8, 4])
    ctx.lineDashOffset = 0 
    ctx.strokeRect(10, 60, 100, 40)
}
drawDash()

image

5.4 颜色渐变

我们可以使用线性渐变或径向渐变来进行填充或描边。通过新建 canvasGradient 对象,并将该对象赋值给 fillStyle 和 strokeStyle 属性。

  • 线性渐变

通过 createLinearGradient(x1, y1, x2, y2) 创建 canvasGradient 线性渐变对象,(x1, y1) 为渐变的起点,(x2, y2) 为渐变的终点。创建了 canvasGradient 对象后,可以通过 addColorStop(position, color) 指定在渐变区域某个相对位置(position)的渐变颜色(color), position 的值为 0 ~ 1 之间(例如:当值为0.5时表示在渐变区域的中间位置),color 值为 CSS 中颜色的有效值。

绘制一个从上到下填充色由红变粉色的矩形

let lineargradient = ctx.createLinearGradient(10, 10 , 10, 100)
lineargradient.addColorStop(0, 'red')
lineargradient.addColorStop(1, 'pink')
ctx.rect(10, 10, 200, 100)
ctx.fillStyle = lineargradient
ctx.fill()

image

  • 径向渐变

通过 createRadialGradient(x1, y1, r1, x2, y2, r2),创建 canvasGradient 径向渐变对象,前三个参数定义了一个 (x1, y1) 为原点的圆, r1 为半径,后三个参数 定义了另一个(x2, y2) 为原点, r2 为半径的圆。

绘制由内向外填充色渐变的圆

let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d');
let radialGradient = ctx.createRadialGradient(50, 50, 10, 50, 50, 30)
radialGradient.addColorStop(0, 'red');
radialGradient.addColorStop(0.9, 'blue');
radialGradient.addColorStop(1, 'rgba(255, 0, 0, 0)');
ctx.rect(10, 10, 200, 100)
ctx.fillStyle = radialGradient
ctx.fill()

image

5.5 图案样式

可以通过 createPattern() 实现图案的效果,它接受两个参数第一个参数可以是 Image 对象或 canvas 对象,第二个参数的取值可以是:repeat、repeat-x、repeat-y、no-repeat。

通过一个图片绘制一个图案

let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d');
let img = new Image()
img.src = './atom.png'
img.onload = function(){
    let pat = ctx.createPattern(img, 'repeat')
    ctx.fillStyle = pat;
    ctx.rect(10, 10, 200, 100)
    ctx.fill()
}

image

参数为 canvas 对象

let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d');
ctx.arc(5, 5, 5, 0, 2*Math.PI)
ctx.fill()
// 以一个 canvas 对象绘制图案
let canvas2 = document.getElementById('canvas2')
let ctx2 = canvas2.getContext('2d');
ctx2.fillStyle = ctx2.createPattern(canvas, 'repeat')
ctx2.fillRect(10, 10, 100, 50)

image

5.6 阴影

通过设置 shadowOffsetX、shadowOffsetY 属性的值设置阴影的延伸距离,默认都为 0,为正数时分别表示设置向右和向下的阴影距离,负数表示向左和向上的阴影距离。shadowBlur 属性用于设置阴影的模糊程度,默认值为 0,值越大越模糊。shadowColor 属性用于设置阴影的颜色,默认值为全透明黑色。

设置一个圆的阴影

let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d');
ctx.shadowOffsetX = 10
ctx.shadowBlur = 10
ctx.shadowColor = 'red'
ctx.arc(50, 50, 30, 0, 2*Math.PI)
ctx.fill()

image

5.7 canvas 填充规则

当使用 fill() 进行填充时,有两个填充规则可选 nonzero(非零规则)、evenodd(偶数奇数规则)。默认值为 nonzero。

关于两种规则的判定方法:戳这里

两个重叠矩形的 evenodd 的填充规则

let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d');
ctx.fillStyle = 'red'
ctx.rect(10, 10, 100, 50)
ctx.rect(20, 20, 80, 30)
ctx.fill('evenodd')

image

六、绘制文本

6.1 绘制文本

  • fillText(text, x, y [,maxWidth])

它可以接受四个参数,表示将文本(text)绘制在位置坐标为(x, y)的位置上,文本的最大宽度(maxWidth)可选

let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d');
ctx.font = '30px SimSun'
ctx.fillText('hello canvas!', 40, 40)

image

  • strokeText(text, x, y [,maxWidth])

参数的含义和 fillText 一致,区别在于 strokeText 绘制的文本是带有边框的

let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d');
ctx.font = '30px SimSun'
ctx.strokeText('hello canvas!', 40, 40)

image

6.2 文本样式属性

  • font

默认字体样式为 '10px sans-serif',可用于设置字体的大小、种类等

  • textAlign, 可选值:start(默认值) | end | left | right | center, 设置文本的对齐方式。

  • textBaseLine,可选值:top | hanging | middle | alphabetic(默认值) | ideographic | bottom,文字基准对齐方式。

  • direction,可选值:ltr(文本方向从左到右) | rtl(文本方向从右到左) | inherit(默认值),设置文本的方向

6.3 测量文本的宽度

measureText(text)

let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d');
let text = ctx.measureText('hello canvas!')
console.dir(text);

image

七、最后

由于 canvas 涉及到的内容比较多,写在一篇文章里的话文章的篇幅会比较长,故 canvas 的剩余知识点我打算放在第二篇文章,也有可能还会有第三篇。后面会持续更新,未完待续~

posted @ 2022-11-08 11:54  zjl_712  阅读(115)  评论(0编辑  收藏  举报