> For the complete documentation index, see [llms.txt](https://lizh.gitbook.io/knowledge/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lizh.gitbook.io/knowledge/javascript-xi-lie/05canvas-ji-chu-ru-men-pian.md).

# Canvas基础入门篇

Canvas API 提供了一条用 JavaScript 和 HTML 的 `<canvas>` 元素绘制图形的路子。常见用途包括动画、游戏画面、数据可视化、图片编辑以及实时视频处理。

Canvas API 主要做 **2D** 图形。同一个 canvas 元素还可以交给 WebGL API，用硬件加速画 2D 和 3D。

## 获取渲染上下文（2D 环境）

现行主流浏览器都支持 `<canvas>`。画布默认尺寸是 300×150 CSS 像素。要用 HTML 的 `width`、`height` **属性**（不是 CSS）设定绘图缓冲区的像素尺寸。

**注意：** CSS 的 `width`、`height` 只控制元素在页面上占多大。若和 canvas 属性上的宽高比不一致，会把缓冲区里的图拉伸变形。

```html
<div class="flex_box">
    <canvas id="canvas_base" class="flex_item" width="500" height="500"></canvas>
</div>
```

```javascript
const canvas = document.getElementById("canvas_base")
const context = canvas.getContext("2d")
```

```javascript
function getContext2D () {
    return new Promise((resolve, reject) => {
        const canvas = document.getElementById("canvas_base")
        const context = canvas.getContext("2d")
        if (context) {
            resolve(context)
        } else {
            reject()
        }
    })
}
```

## 状态的保存和恢复

Canvas 是基于状态的绘制，包括：当前应用的变形、当前的裁切路径、以及以下属性：

```
strokeStyle
fillStyle
globalAlpha
lineWidth
lineCap
lineJoin
miterLimit
lineDashOffset
shadowOffsetX
shadowOffsetY
shadowBlur
shadowColor
globalCompositeOperation
font
textAlign
textBaseline
direction
imageSmoothingEnabled
filter
imageSmoothingQuality
```

Canvas 可通过以下方法管理状态：

* `save()`：保存画布的所有状态
* `restore()`：`save` 和 `restore` 用来保存和恢复 canvas 状态，都没有参数。Canvas 的状态就是当前画面应用的所有样式和变形的一个快照。

Canvas 状态存储在栈中，每当 `save()` 被调用后，当前的状态就被推送到栈中保存。

## 从线条开始

**移动画笔：**

```javascript
context.moveTo(x, y)
```

> 这句代码的意思是**移动画笔至 (x, y) 这个点（单位是 px）**。记住，这里是以 **canvas 画布的左上角为笛卡尔坐标系的原点，且 y 轴的正方向向下，x 轴的正方向向右。**

**笔画停点：**

```javascript
context.lineTo(x, y)
```

> 这句的意思是从**上一笔的停止点**绘制到 (x, y) 这里。不过要清楚，这里的 `moveTo()`、`lineTo()` 都只是状态而已，是规划，是我准备要画，还没有开始画，只是一个计划而已！

**选择画笔：**

```javascript
context.lineWidth = 2        // 设置画笔(线条)的粗细为2px
context.fillStyle = "#0dd"   // 设置或返回用于填充绘画的颜色、渐变或模式
context.strokeStyle = "#0dd" // 设置或返回用于笔触的颜色、渐变或模式
```

> Canvas 是基于状态的绘制，所以我们在选择画笔粗细和颜色的同时，其实也是选择了线条的粗细和颜色。

**确定绘制：**

**确定绘制只有两种方法：`fill()` 和 `stroke()`**，前者是指填充，后者是指描边。

![image-20200802152344385](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802152344385.png)

```javascript
// 绘制线条：青色
function drawLine (context) {
    context.moveTo(100, 100)
    context.lineTo(300, 200)
    context.lineWidth = 2
    context.strokeStyle = "#0dd"
    context.stroke()
}

// 绘制折线：粉红 -> 深蓝
function drawBrokenLine (context) {
    context.lineWidth = 2

    context.moveTo(100, 100)
    context.lineTo(300, 300)
    context.strokeStyle = "#d0d"
    context.stroke()

    context.moveTo(300, 300)
    context.lineTo(100, 500)
    context.strokeStyle = "#00d"
    context.stroke()
}
getContext2D().then(context => {
    drawLine(context) 
    drawBrokenLine(context) 
})
```

[查看 DEMO](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=1)

期待绘制结果：第一条线是青色，第二条线是粉红色，第三条线是深蓝。然而最终三条线都是深蓝。原因还是 **Canvas 基于当前路径和当前样式绘制**。

`stroke()` 不会改已经画到画布上的像素，而是用**此刻**的 `strokeStyle` 把**当前路径**再描一遍。没有 `beginPath()` 时，路径会一直累积。第一次 `stroke()` 描青色；第二次路径里已有前两段，用粉红再描一遍；第三次三段都在路径里，用深蓝再描一遍。看起来像三条深蓝折线，实际是同一条累积路径被描了多次。

**创建新路径：**

`beginPath()` 会清空当前路径的子路径列表，开始一条新路径。样式（颜色、线宽等）不受它影响，一直有效到你改掉为止。`stroke()`、`fill()` 只是把当前路径画出来，并不会结束「beginPath 的作用域」。`closePath()` 只是把路径连回起点。换一条线、换一种颜色时，应先 `beginPath()`。

![image-20200802152715047](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802152715047.png)

```javascript
function drawMultBrokenLine (context) {
    context.lineWidth = 2

    context.beginPath()
    context.moveTo(100, 100)
    context.lineTo(300, 200)
    context.strokeStyle = "#0dd"
    context.stroke()

    context.beginPath()
    context.moveTo(100, 100)
    context.lineTo(300, 300)
    context.strokeStyle = "#d0d"
    context.stroke()

    context.beginPath()
    context.moveTo(300, 300)
    context.lineTo(100, 500)
    context.strokeStyle = "#00d"
    context.stroke()
}
```

[查看 DEMO](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=2)

**线条属性概述：**

`lineCap` 属性定义上下文中线的端点，可以有以下 3 个值：

* `butt`：默认值，端点是垂直于线段边缘的平直边缘。
* `round`：端点是在线段边缘处以线宽为直径的半圆。
* `square`：端点是在线段边缘处以线宽为长、以一半线宽为宽的矩形。

`lineJoin` 属性定义两条线相交产生的拐角，可将其称为连接。在连接处创建一个填充三角形，可以使用 `lineJoin` 设置它的基本属性。

* `miter`：默认值，在连接处边缘延长相接。`miterLimit` 是角长和线宽所允许的最大比例（默认是 10）。
* `bevel`：连接处是一个对角线斜角。
* `round`：连接处是一个圆。

`lineWidth` 属性定义线的宽度（默认值为 1.0）。

`strokeStyle` 属性定义线和形状边框的颜色和样式。

## 填充颜色

**基本颜色：**

```javascript
context.fillStyle = "red"
context.fillStyle = "#FF0000"
context.fillStyle = "#F00"
context.fillStyle = "rgb(255,0,0)"
context.fillStyle = "rgba(255,0,0,1)"
context.fillStyle = "hsl(0,100%,50%)"
context.fillStyle = "hsla(0,100%,50%,1)"
```

**线性渐变：**

```javascript
const gradient = context.createLinearGradient(x0,y0,x1,y1)
// x0 渐变开始点的 x 坐标
// y0 渐变开始点的 y 坐标
// x1 渐变结束点的 x 坐标
// y1 渐变结束点的 y 坐标

gradient.addColorStop(stop, color)
// stop  介于 0.0 与 1.0 之间的值，表示渐变中开始与结束之间的位置。
// color 在结束位置显示的 CSS 颜色值
```

![image-20200802153043369](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802153043369.png)

```javascript
function createLinearGradient (context) {
    context.rect(100, 100, 300, 300);
    let grd = context.createLinearGradient(100, 100, 300, 300)
    grd.addColorStop(0, "#dd0")
    grd.addColorStop(0.5, "#fd0")
    grd.addColorStop(1, "#0dd")
    context.fillStyle = grd
    context.fill()
}
```

[查看 DEMO](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=3)

**径向渐变：**

```javascript
const gradient = context.createRadialGradient(x0,y0,r0,x1,y1,r1)
// x0 渐变的开始圆的 x 坐标
// y0 渐变的开始圆的 y 坐标
// r0 开始圆的半径
// x1 渐变的结束圆的 x 坐标
// y1 渐变的结束圆的 y 坐标
// r1 结束圆的半径

gradient.addColorStop(stop, color)
// stop  介于 0.0 与 1.0 之间的值，表示渐变中开始与结束之间的位置。
// color 在结束位置显示的 CSS 颜色值
```

![image-20200802153527937](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802153527937.png)

```javascript
function createRadialGradient (context) {
    let grd = context.createRadialGradient(75, 50, 5, 90, 60, 100)
    grd.addColorStop(0, "#0aa")
    grd.addColorStop(1, "white")
    context.fillStyle = grd
    context.fillRect(10, 10, 300, 300)
}
```

[查看 DEMO](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=4)

**填充纹理：** 纹理其实就是图案的重复，填充图案通过 createPattern() 函数进行初始化。

```javascript
context.createPattern(image, "repeat|repeat-x|repeat-y|no-repeat")
// 第一个参数：CanvasImageSource（Image、canvas、video、ImageBitmap 等）
// 平面上重复：repeat;
// x 轴上重复：repeat-x;
// y 轴上重复：repeat-y;
// 不使用重复：no-repeat;
```

![image-20200802154344739](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802154344739.png)

```javascript
function createPattern (context) {
    let canvas = document.createElement("canvas")
    let context2 = canvas.getContext('2d')
    canvas.width = 200
    canvas.height = 200
    let img = new Image()
    img.src = "https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/人世间.png"
    img.onload = function() {
        context2.drawImage(img, 0, 0, 200, 200)
        let pattern = context.createPattern(canvas, 'no-repeat')
        context.fillStyle = pattern
        context.fillRect(0, 0, 500, 500)
        context.lineWidth = 1
        context.strokeStyle = '#0dd'
        context.strokeRect(0, 0, 500, 500)
    }
}
```

[查看 DEMO](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=5)

## 绘制矩形

```javascript
context.rect(x,y,width,height)
context.fillRect(x,y,width,height)
context.strokeRect(x,y,width,height)
context.clearRect(x,y,width,height)
// x 矩形左上角的 x 坐标
// y 矩形左上角的 y 坐标
// width  矩形的宽度，以像素计
// height 矩形的高度，以像素计
```

## 绘制标准圆弧

```javascript
context.arc(x,y,r,sAngle,eAngle,counterclockwise);
// x 圆的中心的 x 坐标。
// y 圆的中心的 y 坐标。
// r 圆的半径。
// sAngle 起始角，以弧度计。（弧的圆形的三点钟位置是 0 度）。
// eAngle 结束角，以弧度计。
// counterclockwise	可选。规定应该逆时针还是顺时针绘图。false = 顺时针（默认），true = 逆时针。
```

```javascript
context.arcTo(x1, y1, x2, y2, r);
// x1、y1 第一个控制点（拐角）的坐标，不是弧的起点
// x2、y2 第二个控制点的坐标
// r  弧的半径
// 弧从当前点出发，与两条切线相切：当前点→(x1,y1)、(x1,y1)→(x2,y2)
```

![image-20200802154748374](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802154748374.png)

```javascript
function drawArc (context) {
    context.beginPath()
    context.lineWidth = 1
    context.strokeStyle = '#0dd'
    context.arc(100, 75, 100, 0,  (120 / 180) * Math.PI, false)
    context.stroke()
}
```

[查看 DEMO](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=6)

![image-20200802154936529](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802154936529.png)

```javascript
function drawArcTo (context) {
    context.beginPath()
    context.lineWidth = 1
    context.strokeStyle = '#0dd'
    context.moveTo(20, 20)
    context.lineTo(100, 20)
    context.arcTo(150, 20, 150, 70,50)
    context.lineTo(150, 100)
    context.stroke()
}
```

[查看 DEMO](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=7)

## 贝塞尔曲线

### 二次贝塞尔曲线

```javascript
context.quadraticCurveTo(cpx,cpy,x,y);
// cpx 贝塞尔控制点的 x 坐标
// cpy 贝塞尔控制点的 y 坐标
// x   结束点的 x 坐标
// y   结束点的 y 坐标
```

> 二次贝塞尔曲线需要两个点。第一个点是用于二次贝塞尔计算中的控制点，第二个点是曲线的结束点。曲线的开始点是当前路径中最后一个点。如果路径不存在，那么请使用 `beginPath()` 和 `moveTo()` 方法来定义开始点。

![image-20200802160540181](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802160540181.png)

```javascript
function drawQuadraticCurveTo (context) {
    context.lineWidth = 1
    context.strokeStyle = '#0dd'
    context.beginPath()
    context.moveTo(20, 20)
    context.quadraticCurveTo(20, 100, 200, 20)
    context.stroke()
}
```

[查看 DEMO](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=8)

### 三次贝塞尔曲线

```javascript
context.bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y);
// cp1x	第一个贝塞尔控制点的 x 坐标
// cp1y	第一个贝塞尔控制点的 y 坐标
// cp2x	第二个贝塞尔控制点的 x 坐标
// cp2y	第二个贝塞尔控制点的 y 坐标
// x	结束点的 x 坐标
// y	结束点的 y 坐标
```

> 三次贝塞尔曲线需要三个点。前两个点是用于三次贝塞尔计算中的控制点，第三个点是曲线的结束点。曲线的开始点是当前路径中最后一个点。如果路径不存在，那么请使用 `beginPath()` 和 `moveTo()` 方法来定义开始点。

## 文本设置

```
font	     // 设置或返回文本内容的当前字体属性
textAlign	 // 设置或返回文本内容的当前对齐方式
textBaseline // 设置或返回在绘制文本时使用的当前文本基线

context.font = "[font-style] [font-variant] [font-weight] [font-size/line-height] [font-family]"
// font-style    // 规定字体样式：normal、italic、oblique
// font-variant  // 规定字体变体：normal、small-caps
// font-weight   // 规定字体的粗细：normal、bold、bolder、lighter、100～900
// font-size/line-height // 规定字号和行高，以像素计。
// font-family   // 规定字体系列。

context.textAlign = "center|end|left|right|start"
// start   默认。文本在指定的位置开始（ltr 下等同 left）。
// end    文本在指定的位置结束。
// center 文本的中心被放置在指定的位置。
// left   文本左对齐。
// right  文本右对齐。

context.textBaseline = "alphabetic|top|hanging|middle|ideographic|bottom"
// alphabetic  默认。文本基线是普通的字母基线。
// top         文本基线是 em 方框的顶端。
// hanging     文本基线是悬挂基线。
// middle      文本基线是 em 方框的正中。
// ideographic 文本基线是表意基线。
// bottom      文本基线是 em 方框的底端。
```

```
fillText()    // 在画布上绘制“被填充的”文本
strokeText()  // 在画布上绘制文本（无填充）
measureText() // 返回包含指定文本宽度的对象

context.fillText(text,x,y,maxWidth)
context.strokeText(text,x,y,maxWidth)
// text     规定在画布上输出的文本。
// x        开始绘制文本的 x 坐标位置（相对于画布）。
// y        开始绘制文本的 y 坐标位置（相对于画布）。
// maxWidth 可选。允许的最大文本宽度，以像素计。
```

![image-20200802160727162](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802160727162.png)

```javascript
function drawText (context) {
    context.fillStyle = "#0aa"
    context.strokeStyle = "#0aa"

    context.font = "normal normal bold 30px/150px Times"
    context.textAlign = "center"
    context.textBaseline = "middle"

    context.fillText("Hello Canvas", 200, 250)
    context.strokeText("width:" + context.measureText("Hello Canvas").width, 200, 300, 200)
}
```

[查看 DEMO](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=9)

## 变换设置

```javascript
context.scale(scalewidth,scaleheight);
// scalewidth  缩放当前绘图的宽度 (1=100%, 0.5=50%, 2=200%, 依次类推)
// scaleheight 缩放当前绘图的高度 (1=100%, 0.5=50%, 2=200%, etc.)

context.rotate(angle);
// angle	旋转角度，以弧度计。
// 如需将角度转换为弧度，请使用 degrees*Math.PI/180 公式进行计算。
// 举例：如需旋转 5 度，可规定下面的公式：5*Math.PI/180。

context.translate(x,y);
// x 添加到水平坐标（x）上的值
// y 添加到垂直坐标（y）上的值
```

> 对于缩放变换有两点问题需要注意：
>
> 1.缩放时，图像左上角坐标的位置也会对应缩放
>
> 2.缩放时，图像线条的粗细也会对应缩放

```javascript
context.transform(a,b,c,d,e,f);
context.setTransform(a,b,c,d,e,f);
// a 水平缩放(1)
// b 水平倾斜(0)
// c 垂直倾斜(0)
// d 垂直缩放(1)
// e 水平位移(0)
// f 垂直位移(0)

// 平移变换： x’ = x + dx， y’ = y + dy。
// 也即是说可以使用 context.transform(1,0,0,1,dx,dy) 代替 context.translate(dx,dy)。

// 同理可以使用 context.transform(sx, 0, 0, sy, 0, 0) 代替 context.scale(sx, sy)。
// 不要用 context.transform(0, sy, sx, 0, 0, 0) 去代替 scale，那会把轴换掉。

// context.transform(Math.cos(θ*Math.PI/180)，Math.sin(θ*Math.PI/180), -Math.sin(θ*Math.PI/180),Math.cos(θ*Math.PI/180)，0，0）可以替代 context.rotate(θ)。

// 也可以使用 context.transform(-Math.sin(θ*Math.PI/180),Math.cos(θ*Math.PI/180)， Math.cos(θ*Math.PI/180)，Math.sin(θ*Math.PI/180)，0，0）替代。

// 区别：transform() 变换是指在上一次变换的状态基础上再次变换；setTransform() 变换是指先重置到最初始的状态再开始变换。
```

![image-20200802162108256](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802162108256.png)

```javascript
function transform (context) {
    context.save()
    context.fillStyle = "#0aa"
    context.scale(1, 0.5)
    context.rotate(10 * Math.PI / 180)
    context.translate(20, 0)
    context.fillRect(0, 0, 150, 150)
    console.log('scale')
    context.restore()

    //transform
    context.save()
    context.translate(100, 0)
    context.fillStyle = "yellow"
    context.fillRect(0, 0, 150, 100)

    context.transform(1, 0.5, -0.5, 1, 30, 10)
    context.fillStyle = "red"
    context.fillRect(0, 0, 150, 100)

    context.transform(1, 0.5, -0.5, 1, 30, 10)
    context.fillStyle = "blue"
    context.fillRect(0, 0, 150, 100)
    context.restore()

    //setTransform
    context.save()
    context.translate(400, 0)
    context.fillStyle = "yellow"
    context.fillRect(0, 0, 150, 100)

    context.setTransform(1, 0.5, -0.5, 1, 100, 0)
    context.fillStyle = "red"
    context.fillRect(0, 0, 150, 100)

    context.setTransform(1, 0.5, -0.5, 1, 100, 0)
    context.fillStyle = "blue"
    context.fillRect(0, 0, 150, 100)
    context.restore()
}
```

[查看 DEMO](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=10)

> 注意保存和恢复 Canvas 状态
>
> 变换只会影响 transform() 等方法调用之后的绘图

## 裁剪区域

`clip()` 用来实现 Canvas 的图像裁剪。它用路径给画布设置一块裁剪区域，因此必须先创建好路径，再调用 `clip()`。

`clip()` 用当前路径在画布上设一块裁剪区域，之后的绘制只出现在区域内。裁剪是画布状态的一部分，配合 `save()`、`restore()` 可以恢复，并不是「切一次就永远变小」。画布是先 clip 再绘制。不一定是图片，路径也可以。现行 `clip()` 还可传入填充规则（`nonzero`、`evenodd`）。

![image-20200802162403917](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802162403917.png)

```javascript
function setClip (context) {
    context.save()
    context.beginPath()
    context.fillStyle = '#0aa'
    context.arc(100, 100, 100, 2 * Math.PI, false)
    context.clip()
    let img = new Image()
    img.src = "https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/人世间.png"
    img.onload = function() {
        context.drawImage(img, 0, 0, 350, 250)
        context.closePath()
        context.restore()

        context.beginPath()
        context.moveTo(100, 100)
        context.lineTo(300, 100)
        context.strokeStyle = '#0aa'
        context.stroke()
        context.closePath()
    }
}
```

[查看 DEMO](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=11)

## 绘制图像

Canvas 更有意思的一项特性就是图像操作能力。可以用于动态的图像合成或者作为图形的背景，以及游戏界面等等。浏览器支持的任意格式的外部图片都可以使用，比如 PNG、GIF、JPEG。你甚至可以将同一个页面中其他 canvas 元素生成的图片作为图片源。

Canvas 的 API 可以使用下面这些类型中的一种作为图片的源：

* `HTMLImageElement`：`Image()` 构造出来的，或者任何 `img` 元素。
* `HTMLVideoElement`：`video` 元素，可抓当前帧。
* `HTMLCanvasElement`：另一个 canvas。
* `ImageBitmap`：高性能位图。
* `OffscreenCanvas`、`SVGImageElement`、`VideoFrame` 也可作为源。

这些源统一由 `CanvasImageSource` 类型来引用。

```javascript
context.drawImage(img,x,y)
context.drawImage(img,x,y,width,height)
context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height)

// img     规定要使用的图像、画布或视频。
// sx      可选。开始剪切的 x 坐标位置。
// sy      可选。开始剪切的 y 坐标位置。
// swidth  可选。被剪切图像的宽度。
// sheight 可选。被剪切图像的高度。
// x       在画布上放置图像的 x 坐标位置。
// y       在画布上放置图像的 y 坐标位置。
// width   可选。要使用的图像的宽度。（伸展或缩小图像）
// height  可选。要使用的图像的高度。（伸展或缩小图像）
```

![image-20200802162553067](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802162553067.png)

```javascript
function drawImage (context) {
    let img = new Image()
    img.src = "https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/人世间.png"
    img.onload = function() {
        // context.drawImage(img, 0, 0, 500, 500, 0, 0, window.innerWidth, window.innerHeight)
        context.drawImage(img, 0, 0, 350, 250)
    }
}
```

[查看 DEMO](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=12)

## 非零环绕原则与奇偶原则

我们一般使用 Canvas 的 `fill` 方法时直接填充路径。`fill` 可以传入填充规则：`nonzero`（非零环绕，默认）或 `evenodd`（奇偶）。

### 非零环绕原则 nonzero

```
用来判断哪些区域属于路径内（计算结果非 0，即为路径内）。
* 在路径包围的区域中，随便找一点，向外发射一条射线，
* 和所有围绕它的边相交，
* 然后开启一个计数器，从 0 计数，
* 如果这条射线遇到顺时针围绕，那么 +1，
* 如果遇到逆时针围绕，那么 -1，
* 如果最终值非 0，则这块区域在路径内。结果为 0，代表这块区域不要填充，否则必须填充
```

### 奇偶原则 evenodd

```
* 在路径包围的区域中，随便找一点，向外发射一条射线，
* 和所有围绕它的边相交，
* 查看相交线的个数，如果为奇数，就填充，如果是偶数，就不填充
```

![image-20200802162859005](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802162859005.png)

```javascript
function drawCircular (context) {
    context.shadowColor = "#545454";
    context.shadowOffsetX = 5;
    context.shadowOffsetY = 5;
    context.shadowBlur = 2;
    context.globalCompositeOperation = 'source-over'

    context.fillStyle = "#00AAAA"
    context.arc(200, 200, 100, 0, Math.PI * 2 ,false)
    context.arc(200, 200, 115, 0, Math.PI * 2 ,true)
    context.fill()
}
```

[查看 DEMO](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=13)

## 其他属性

**阴影设置：**

```
shadowColor   // 设置或返回用于阴影的颜色
shadowBlur    // 设置或返回用于阴影的模糊级别
shadowOffsetX // 设置或返回阴影距形状的水平距离
shadowOffsetY // 设置或返回阴影距形状的垂直距离
```

> 需要注意的是，这里的阴影同其他属性设置一样，都是基于状态的设置。因此，如果只想为某一个对象应用阴影而不是全局阴影，需要在下次绘制前重置阴影的这四个属性。

**全局透明：**

```
globalAlpha
// 默认值为 1.0，代表完全不透明，取值范围是 0.0（完全透明）～1.0。这个属性与阴影设置是一样的，如果不想针对全局设置不透明度，就得在下次绘制前重置 globalAlpha。
```

**橡皮擦：**

```javascript
context.clearRect(x,y,w,h)
// 清空指定矩形上的画布上的像素。它接受四个参数，和其他绘制矩形的方法一样
```

**检测点：**

`isPointInPath` 用于判断当前路径中是否包含检测点。

```javascript
isPointInPath(x, y)
isPointInPath(x, y, fillRule)
isPointInPath(path, x, y)
isPointInPath(path, x, y, fillRule)

/*
x：检测点的 X 坐标
y：检测点的 Y 坐标
fillRule：用来决定点在路径内还是在路径外的算法，允许的值：
	nonzero - 非零环绕规则，默认的规则
	evenodd - 奇偶环绕原则
path：Path2D 应用的路径
*/
```

## 绘制圆角矩形

现行环境也可直接用 `ctx.roundRect(x, y, w, h, radii)`（Canvas 2D 已标准化）。下面是用 `arcTo` 手写的等价实现：

![image-20200802163558011](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802163558011.png)

```javascript
function roundRect (ctx, x, y, w, h, r) {
    var min_size = Math.min(w, h);
    if (r > min_size / 2){
        r = min_size / 2
    }
    ctx.beginPath()
    ctx.moveTo(x + r, y)
    ctx.arcTo(x + w, y, x + w, y + h, r)
    ctx.arcTo(x + w, y + h, x, y + h, r)
    ctx.arcTo(x, y + h, x, y, r)
    ctx.arcTo(x, y, x + w, y, r)
    ctx.closePath()
}
```

[查看 DEMO](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=14)

## 绘制圆角图片

![image-20200802173138214](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802173138214.png)

![image-20200802173158563](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20200802173158563.png)

```javascript
function drawRoundImg (ctx, x, y, w, h, r) {
    ctx.save();
    let img = new Image()
    img.src = "https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/人世间.png"
    img.onload = function() {
        roundRect(ctx, x, y, w, h, r)
        ctx.clip();
        ctx.drawImage(img, x, y, w, h)
        ctx.restore()
    }
}

function drawRoundImg2 (ctx, x, y, w, h, r) {
    ctx.save();
    let img = new Image()
    img.src = "https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/人世间.png"
    img.onload = function() {
        let pattern = ctx.createPattern(img, "no-repeat");
        roundRect(ctx, x, y, w, h, r)
        ctx.fillStyle = pattern
        ctx.fill()
    }
}
```

[查看 DEMO 圆角图片1](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=15) [查看 DEMO 圆角图片2](https://github.com/1927344728/fed-knowledge/tree/master/demo/10-canvas.html?type=16)

## 参考链接

[Canvas API 中文文档首页地图](https://www.canvasapi.cn/)

[MDN - canvas](https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API)
