Canvas 从进阶到退学

news2024/11/18 19:48:50

本文简介

点赞 + 关注 + 收藏 = 学会了


接着 《Canvas 从入门到劝朋友放弃(图解版)》 ,本文继续补充 canvas 基础知识点。

这次我不手绘了!


本文会涉及到 canvas 的知识包括:变形、像素控制、渐变、阴影、路径



变形

这里说的变形是基于画布,全局进行变形。

变形主要包括:平移 translate缩放 scale旋转操作 rotate

除了对应的方法外,还可以使用 transformsetTransform 对上面三种操作进行配置,这称为“变换矩阵”。


在学习“变形”之前,需要了解 W3C坐标系

file

箭头所指是各轴自己的正方向,x轴越往右(正方向)值越大,y轴越往下(正方向)值越大。


平移

使用 translate() 方法可以实现平移效果(位移)。

translate(x, y) 接收2个参数,第一个参数代表x轴方向位移距离,第二个参数代表y轴方向位移距离。

正数代表向正方向位移,负数代表向反方向位移。


演示平移效果之前,我先创建一个矩形,长宽都是100,位置在画布的原点 (0, 0) ,也就是紧贴画布的左上角。

file

<canvas id="c" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 紧贴原点的矩形,默认是黑色[]
  ctx.fillRect(0, 0, 100, 100)
</script>

如果此时在 fillRect 之前设置 translate 就可以实现整个画布位移的效果。

file

// 省略部分代码

// 平移,往右平移10,往下平移20
ctx.translate(10, 20)

// 渲染矩形
ctx.fillRect(0, 0, 100, 100)

从上图可以看出,矩形距离画布顶部的距离是20,距离画布左侧的距离是10。


注意:平移 translate() 要写在 “绘制操作(本例是 fillRect)” 之前才有效。


如果在使用 translate 的前后都有渲染操作,画布会多次渲染,并不会自动清屏。

比如这样

file

<canvas id="c" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.fillRect(0, 0, 100, 100)

  ctx.translate(10, 20)

  ctx.fillRect(0, 0, 100, 100)
</script>

再做个明显点的效果,每秒平移一次

file

<canvas id="c" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.fillRect(0, 0, 100, 100)

  setInterval(() => {
    ctx.translate(10, 20)
    ctx.fillRect(0, 0, 100, 100)
  }, 1000)

</script>

可以看出,每次使用 translate() 平移画布,都会基于上一次画布所在的位置进行平移。


上图效果是 canvas 的默认效果,所以在执行 translate 之前可以执行 “清屏操作”。


清屏

file

<canvas id="c" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.fillRect(0, 0, 100, 100)

  setInterval(() => {
    ctx.clearRect(0, 0, context.width, context.height)
    ctx.translate(10, 20)
    ctx.fillRect(0, 0, 100, 100)
  }, 1000)

</script>

缩放

缩放画布用到的方法是 scale(x, y) ,接收2个参数,第一个参数是x轴方向的缩放,第二个参数是y轴方向的缩放。

x 或者 y 的值是 0 ~ 1 时代表缩小,比如取值为 0.5 时,表示比原本缩小一半;值为2时,比原本放大一倍。

file

<canvas id="c" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.font = '60px Arial'
  ctx.strokeStyle = 'hotpink'
  ctx.strokeText('雷猴', 40, 100)

  // 缩小
  ctx.scale(0.5, 0.5)

  // 重新渲染
  ctx.strokeText('雷猴', 40, 100)
</script>

scale() 方法同样会保留原本已经渲染的内容。

如果不需要保留原本内容,可以使用 “清屏操作”。

注意:scale() 会以上一次缩放为基准进行下一次缩放。


副作用:

其实从上面的例子就可以看出 scale() 存在一点副作用的,从图中可以看出,缩放后文本的左上角坐标发生了“位移”,文本描边粗细也发生了变化。

虽然说是副作用,但也很容易理解,整块画布缩放了,对应的坐标比例其实也跟着缩放嘛。


旋转

使用 rotate(angle) 方法可以旋转画布,但默认的旋转原点是画布的左上角,也就是 (0, 0) 坐标。

我计算旋转角度通常是用 角度 * Math.PI / 180 的方式表示。

虽然这样书写代码看上去很长,但习惯后就比较直观的看出要旋转多少度。

rotate(angle) 中的参数 angle 代表角度,angle 的取值范围是 -Math.PI * 2 ~ Math.pi * 2

当旋转角度小于 0 时,画布逆时针旋转;反之顺时针旋转。

file

<canvas id="c" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.font = '60px Arial'
  ctx.strokeStyle = 'pink'
  ctx.strokeText('雷猴', 40, 100)

  // 旋转 45°
  ctx.rotate(45 * Math.PI / 180)

  // 重新渲染
  ctx.strokeText('雷猴', 40, 100)
</script>

修改原点

如果需要修改旋转中心,可以使用 translate() 方法平移画布,通过计算移动到指定位置。

file

<canvas id="c" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.font = '60px Arial'
  ctx.strokeStyle = 'pink'
  ctx.strokeText('雷猴', 40, 100)

  // 设置旋转中心
  ctx.translate(90, -50)

  // 旋转
  ctx.rotate(45 * Math.PI / 180)

  // 重新渲染
  ctx.strokeText('雷猴', 40, 100)
</script>

变换矩阵

变换矩阵常用方法有 transform()setTransform() 两个方法。

变换矩阵是一个稍微进阶一点的知识了,别怕!

前面的 平移 translate缩放 scale旋转操作 rotate 可以说都是 transform() 的 “语法糖”。

变换矩阵已经涉及到一点数学知识了,但本文不会讲到这些知识,只会讲讲 transform() 是怎么用的。


transform

transform() 一个方法就可以实现 平移、缩放、旋转 三种功能,它接收6个参数。

transform(a, b, c, d, e, f)

  • a: 水平缩放(x轴方向),默认值是 1;
  • b: 水平倾斜(x轴方向),默认值是 0;
  • c: 垂直倾斜(y轴方向),默认值是 0;
  • d: 垂直缩放(y轴方向),默认值是 1;
  • e: 水平移动(x轴方向),默认值是 0;
  • f: 垂直移动(y轴方向),默认值是 0;

这默认值看上去很乱,但如果这样排列一下是不是就比较容易理解了: $$ \begin{pmatrix}a & c & e \\ b & d & f \\ 0 & 0 & 1 \end{pmatrix} $$


随便修改几个值试试效果:

file

<canvas id="c" width="400" height="400" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 变换矩阵
  ctx.transform(1, 1, 1, 2, 30, 40)

  // 绘制矩形
  ctx.fillRect(10, 10, 100, 100)
</script>

setTransform

setTransform(a, b, c, d, e, f) 同样接收6个参数,和 transform() 一样

file

<canvas id="c" width="400" height="400" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 变换矩阵
  ctx.setTransform(2, 1, 1, 2, 20, 10)

  // 绘制矩形
  ctx.fillRect(10, 10, 100, 100)
</script>

transform 和 setTransform 的区别

transform() 每次执行都会参考上一次变换后的结果

比如下面这个多次执行的情况:

file

<canvas id="c" width="400" height="400" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.fillStyle = 'rgba(10, 10, 10, 0.2)'

  ctx.fillRect(10, 10, 100, 100)

  ctx.transform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

  ctx.transform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

  ctx.transform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

  ctx.transform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

</script>

setTransform() 每次调用都会基于最原始是状态进行变换。

file

<canvas id="c" width="400" height="400" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.fillStyle = 'rgba(10, 10, 10, 0.2)'

  ctx.fillRect(10, 10, 100, 100)

  ctx.setTransform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

  ctx.setTransform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

  ctx.setTransform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

  ctx.setTransform(1, 0, 0, 1, 10, 10)
  ctx.fillRect(10, 10, 100, 100)

</script>

不管改变多少次,setTransform() 都会参考原始状态进行变换。



控制像素

位图是由像素点组成的,canvas 提供了几个 api 可以操作图片中的像素。

很多工具网站也常用接下来说到的几个 api 做图片滤镜。


需要注意的是,canvas 提供的操作像素的方法,必须使用服务器才能运行起来,不然没有效果的。

可以搭建本地服务器运行本文案例,方法有很多种。

比如你使用 Vue 或者 React 的脚手架搭建的项目,运行后就能跑起本文所有案例。

又或者使用 http-server 启动本地服务。


getImageData()

首先要介绍的是 getImageData() 方法,这个方法可以获取指定区域内的所有像素。


getImageData(x, y, width, height) 接收4个参数,这4个参数表示选区范围。

xy 代表选区的左上角坐标,width 表示选区宽度,height 表示选区高度。


还是举例说明比较清楚。下图渲染到画布上的是我的猫Bubble

file

<canvas id="c" width="400" height="400" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  const img = new Image() // 创建图片对象
  img.src = './bubble.jpg' // 加载本地图片

  // 图片加载完成后在执行其他操作
  img.onload = () => {
    // 渲染图片
    ctx.drawImage(img, 0, 0)
    // 获取图片信息
    const imageData = ctx.getImageData(0, 0, img.width, img.height)
    console.log(imageData)
  }

</script>

打印出来的信息可以点开大图看看

  • data: 图片像素数据集,以数组的形式存放,这是本文要讲的重点,需要关注!
  • colorSpace: 图片使用的色彩标准,这个属性在 Chrome 里有打印出来,Firefox 里没打印。不重要~
  • height: 图片高度
  • width: 图片宽度

通过 getImageData() 获取到的信息中,需要重点关注的是 data ,它是一个一维数组,仔细观察发现里面的值没一个是大于255的,也不会小于0。

file

其实 data 属性里记录了图片每个像素的 rgba 值分别是多少。

  • r 代表红色
  • g 代表绿色
  • b 代表蓝色
  • a 透明度

这个和 CSS 里的 rgba 是同一个意思。

data 里,4个元素记录1个像素的信息。也就是说,1个像素是由 rgba 4个元素组成。而且每个元素的取值范围是 0 - 255 的整数。

 data: **[r1, g1, b1, a1, r2, g2, b2, a2, ......]** 
像素点颜色通道
imgData.data[0]49红色 r
imgData.data[1]47绿色 g
imgData.data[2]51蓝色 b
imgData.data[3]255透明度 a
………………
imgData.data[n-4]206红色 r
imgData.data[n-2]200绿色 g
imgData.data[n-3]200蓝色 b
imgData.data[n-1]255透明度 a

如果一张图只有10个像素,通过 getImageData() 获取到的 data 信息中就有40个元素。


putImageData()

putImageData(imageData, x, y) 可以将 ImageData 对象的数据(图片像素数据)绘制到画布上。

putImageData(imgData, x, y, dirtyX, dirtyY, dirtyWidth, dirtyHeight) 也可以接收更多参数。

  • imageData: 规定要放回画布的 ImageData 对象
  • x: ImageData 对象左上角的 x 坐标,以像素计
  • y: ImageData 对象左上角的 y 坐标,以像素计
  • dirtyX: 可选。水平值(x),以像素计,在画布上放置图像的位置
  • dirtyY: 可选。水平值(y),以像素计,在画布上放置图像的位置
  • dirtyWidth: 可选。在画布上绘制图像所使用的宽度
  • dirtyHeight: 可选。在画布上绘制图像所使用的高度

比如,我要将图片复制到另一个位置

file

<canvas id="c" width="500" height="500" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  const img = new Image() // 创建图片对象
  img.src = './bubble.jpg' // 加载本地图片

  // 图片加载完成后在执行其他操作
  img.onload = () => {
    // 渲染图片
    ctx.drawImage(img, 0, 0)
    // 获取图片信息
    const imageData = ctx.getImageData(0, 0, img.width, img.height)

    // 将图片对象输出到 (100, 100) 的位置上
    ctx.putImageData(imageData, 100, 100)
  }

</script>

可以实现复制的效果。


透明

知道前面两个 api 就可以实现透明效果了。

前面讲到,通过 getImageData() 获取的是一个数组类型的数据,每4个元素代表1个像素,就是rgba,而 a 表示透明通道,所以只需修改每组像素的最后1个元素的值,就能修改图片的不透明度。

file

<canvas id="c" width="500" height="500" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  const img = new Image() // 创建图片对象
  img.src = './bubble.jpg' // 加载本地图片

  // 图片加载完成后在执行其他操作
  img.onload = () => {
    // 渲染图片
    ctx.drawImage(img, 0, 0)
    // 获取图片信息
    const imageData = ctx.getImageData(0, 0, img.width, img.height)

    for (let i = 0; i < imageData.data.length; i += 4) {
      imageData.data[i + 3] = imageData.data[i + 3] * 0.5
    }

    // 将图片对象输出到 (100, 100) 的位置上
    ctx.putImageData(imageData, 100, 100)
  }

</script>

滤镜

要做不同的滤镜效果,其实就是通过不同的算法去操作每个像素的值,我在 《Canvas 10款基础滤镜(原理篇)》 讲到相关知识,有兴趣的工友可以点进去看看



渐变

csssvg 里都有渐变,canvas 肯定也不会缺失这个能力啦。

canvas 提供了 线性渐变 createLinearGradient径向渐变 createRadialGradient


线性渐变 createLinearGradient

canvas 中使用线性渐变步骤如下:

  1. 创建线性渐变对象: createLinearGradient(x1, y1, x2, y2)
  2. 添加渐变颜色: addColorStop(stop, color)
  3. 设置填充色或描边颜色: fillStylestrokeStyle

createLinearGradient(x1, y1, x2, y2)

createLinearGradient(x1, y1, x2, y2) 中,x1, y1 表示渐变的起始位置,x2, y2 表示渐变的结束位置。

比如水平方向的从左往右的线性渐变,此时的 y1y2 的值是一样的。

file

两个点就可以确定一个渐变方向。


addColorStop(stop, color)

addColorStop(stop, color) 方法可以添加渐变色。

第一个参数 stop 表示渐变色位置的偏移量,取值范围是 0 ~ 1。

第二个参数 color 表示颜色。


填充渐变

实际编码演示一下

file

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 1. 创建线性渐变对象
  const lgrd = ctx.createLinearGradient(10, 10, 200, 10)

  // 2. 添加渐变颜色
  lgrd.addColorStop(0, 'pink')
  lgrd.addColorStop(1, 'yellow')

  // 设置填充色
  ctx.fillStyle = lgrd

  // 创建矩形,填充
  ctx.fillRect(10, 10, 200, 200)
</script>

如果想修改渐变的方向,只需在使用 createLinearGradient() 时设置好起点和终点坐标即可。


除了填充色,描边渐变和文本渐变同样可以做到。

描边渐变

file

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  const lgrd = ctx.createLinearGradient(10, 10, 200, 10)

  lgrd.addColorStop(0, 'pink')
  lgrd.addColorStop(1, 'yellow')

  ctx.strokeStyle  = lgrd
  ctx.lineWidth = 10
  ctx.strokeRect(10, 10, 200, 200)

</script>

文本渐变

file

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  const lgrd = ctx.createLinearGradient(10, 10, 200, 10)

  lgrd.addColorStop(0, 'pink')
  lgrd.addColorStop(1, 'yellow')

  const text = '雷猴'
  ctx.font = 'bold 100px 黑体'
  ctx.fillStyle = lgrd
  ctx.fillText(text, 10, 100)
</script>

多色线性渐变

在 0 ~ 1 的范围内,addColorStop 可以设置多个颜色在不同的位置上。

file

// 省略部分代码
lgrd.addColorStop(0, '#2a9d8f') // 绿色
lgrd.addColorStop(0.5, '#e9c46a') // 黄色
lgrd.addColorStop(1, '#f4a261') // 橙色

径向渐变 createRadialGradient

径向渐变是从一个点到另一个点扩散出去的渐变,是圆形(椭圆也可以)渐变。

直接看效果

file

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  const rgrd = ctx.createRadialGradient(70, 70, 0, 70, 70, 60)
  rgrd.addColorStop(0, 'yellow')
  rgrd.addColorStop(1, 'pink')
  ctx.fillStyle = rgrd

  ctx.fillRect(10, 10, 120, 120)
</script>

createRadialGradient 可以创建一个径向渐变的对象。使用步骤和 createLinearGradient 一样,但参数不同。

createRadialGradient(x1, y1, r1, x2, y2, r2)

  • x1, y1: 渐变开始的圆心坐标
  • r1: 渐变开始的圆心半径
  • x2, y2: 渐变结束的圆心坐标
  • r2: 渐变结束的圆心半径

同样使用 addColorStop 设置渐变颜色,同样支持多色渐变。


渐变的注意事项

渐变的定位坐标是参照画布的,超出定位的部分会使用最临近的那个颜色。

我用线性渐变举例。

file

<canvas id="c" width="600" height="600" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  const lgrd = ctx.createLinearGradient(200, 0, 400, 400)

  lgrd.addColorStop(0, 'pink')
  lgrd.addColorStop(1, 'yellow')

  ctx.fillStyle = lgrd

  ctx.fillRect(10, 10, 160, 160)

  ctx.fillRect(220, 10, 160, 160)

  ctx.fillRect(430, 10, 160, 160)

  ctx.fillRect(10, 210, 160, 160)

  ctx.fillRect(220, 210, 160, 160)

  ctx.fillRect(430, 210, 160, 160)

  ctx.fillRect(10, 430, 160, 160)

  ctx.fillRect(220, 430, 160, 160)

  ctx.fillRect(430, 430, 160, 160)

</script>

上面的例子中,我只创建了一个渐变,然后创建了9个正方形。

此时正方形的填充色取决于出现在画布中的位置。

可以修改一下 createLinearGradient() 的定位数据对照理解。

file

// 省略部分代码
const lgrd = ctx.createLinearGradient(200, 0, 400, 400)

如果想每个图形都有自己的渐变色,这需要定制化配置,每个创建每个图形之前都单独创建一个渐变色。

file

<canvas id="c" width="600" height="600" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 粉 - 黄 渐变
  const lgrd1 = ctx.createLinearGradient(10, 10, 160, 160)
  lgrd1.addColorStop(0, 'pink')
  lgrd1.addColorStop(1, 'yellow')
  ctx.fillStyle = lgrd1
  ctx.fillRect(10, 10, 160, 160)

  // 橘黄 - 蓝紫 渐变
  const lgrd2 = ctx.createLinearGradient(210, 10, 380, 160)
  lgrd2.addColorStop(0, 'bisque')
  lgrd2.addColorStop(1, 'blueviolet')
  ctx.fillStyle = lgrd2
  ctx.fillRect(220, 10, 160, 160)
</script>

所以不管是填充色还是秒变颜色,每个元素最好都自己重新设定一下。不然可能会出现意想不到的效果~



阴影

阴影在前端也是很常用的特效。 依稀记得当年还用 png 做阴影效果

canvas 中,和阴影相关的属性主要有以下4个:

  • shadowOffsetX: 设置或返回阴影与形状的水平距离。默认值是0。大于0时向正方向偏移。
  • shadowOffsetY: 设置或返回阴影与形状的垂直距离。默认值是0。大于0时向正方向偏移。
  • shadowColor: 设置或返回用于阴影的颜色。 默认黑色。
  • shadowBlur: 设置或返回用于阴影的模糊级别。 默认值是0,数值越大模糊度越强。

相信使用过 css 阴影属性的工友,理解起 canvas 阴影也会非常轻松。

file

<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.shadowOffsetX = 10 // x轴偏移量
  ctx.shadowOffsetY = 10 // y轴偏移量
  ctx.shadowColor = '#f38181' // 阴影颜色
  ctx.shadowBlur = 10 // 阴影模糊度,默认0

  ctx.fillStyle = '#fce38a' // 填充色
  ctx.fillRect(30, 30, 200, 100)

  console.log(ctx.shadowOffsetX) // 输出阴影x轴方向的偏移量:10
</script>

除了图形外,文本和图片都可以设置阴影效果。

file

<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.shadowOffsetX = 10 // x轴偏移量
  ctx.shadowOffsetY = 10 // y轴偏移量
  ctx.shadowColor = '#b83b5e' // 阴影颜色
  ctx.shadowBlur = 10 // 阴影模糊度,默认0

  const text = '雷猴'
  ctx.font = 'bold 100px 黑体'
  ctx.fillStyle = '#fce38a'
  ctx.fillText(text, 10, 100)
</script>


路径

在 Canvas 从入门到劝朋友放弃(图解版) —— 新开路径 中我讲到 新开路径关闭路径 的用法,本节会在上篇的基础上丰富更多使用细节。

本节要讲的是

  • beginPath(): 新开路径
  • closePath(): 关闭路径
  • isPointInPath(): 判断某个点是否在当前路径内

beginPath()

beginPath() 方法是用来开辟一条新的路径,这个方法会将当前路径之中的所有子路径都清除掉,以此来重置当前路径。


如果你的画布上有几个基础图形(直线、多边形、圆形、弧、贝塞尔曲线),如果样式相互之间受到影响,那你可以立刻想想在绘制新图形之前是不是忘了使用 beginPath()

先举几个例子说明一下。


污染:颜色、线条粗细受到污染

后面的样式覆盖了前面的样式。

file

<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 第一条线,粉色
  ctx.moveTo(50, 40)
  ctx.lineTo(150, 40)
  ctx.strokeStyle = 'pink' // 粉色描边
  ctx.stroke()

  // 第二条线,红色
  ctx.moveTo(50, 80)
  ctx.lineTo(150, 80)
  ctx.strokeStyle = 'red' // 红色描边
  ctx.lineWidth = 10 // 表面粗细
  ctx.stroke()
</script>

污染:图形路径污染

比如画布上有一条直线和一个圆形,不使用 beginPath() 开辟新路径的话,它们可能会“打架”。

file

<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 第一条线,粉色
  ctx.moveTo(50, 40)
  ctx.lineTo(150, 40)
  ctx.strokeStyle = 'pink' // 粉色描边
  ctx.stroke()

  // 圆形
  ctx.arc(150, 120, 40, 0, 360 * Math.PI / 180)
  ctx.lineWidth = 4
  ctx.stroke()
</script>

明明直线和圆形是没有交集的,为什么会有一条倾斜的线把两个元素连接起来?


解决办法

除了上面两种情况外,可能还有其他更加奇怪的情况(像极喝醉了假酒),都可以先考虑是不是要使用 beginPath()

比如这样做。

file

<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 第一条线,粉色
  ctx.moveTo(50, 40)
  ctx.lineTo(150, 40)
  ctx.strokeStyle = 'pink' // 粉色描边
  ctx.lineWidth = 10
  ctx.stroke()

  // 圆形
  ctx.beginPath() // 开辟新的路径
  ctx.arc(150, 120, 40, 0, 360 * Math.PI / 180)
  ctx.strokeStyle = 'skyblue' // 蓝色描边
  ctx.lineWidth = 4
  ctx.stroke()
</script>

在使用 arc 或者 moveTo 方法之前加上一句 ctx.beginPath() 就可以有效解决以上问题。

这个例子中,如果没用 ctx.beginPath()canvas 就会以为 线 和 圆形 都属于同一个路径,所以在画圆形时,下笔的时候就会把线的“结束点”和圆的“起点”相连起来。


stroke()fill() 都是以最近的 beginPath() 后面所定义的状态样式为基础进行绘制的。


注意事项

前面的样式会覆盖后面元素的默认样式,即使使用了 beginPath()

file

<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  // 第一条线,粉色
  ctx.moveTo(50, 40)
  ctx.lineTo(150, 40)
  ctx.strokeStyle = 'pink' // 粉色描边
  ctx.lineWidth = 10 // 表面粗细
  ctx.stroke()

  // 第二条线,红色
  ctx.beginPath()
  ctx.moveTo(50, 80)
  ctx.lineTo(150, 80)
  ctx.stroke()
</script>

第一条先设置了 strokeStylelineWidth ,第二条线并没有设置这两个属性,即使在绘制第二条线的开始时使用了 ctx.beginPath() ,第二条线也会使用第一条线的 strokeStylelineWidth 。除非第二条线自己也有设置这两个属性,不然就会沿用之前的配置项。


"特殊情况"

还要补充一个 “特殊情况”。

file

<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>

<script>
  const cnv = document.getElementById('c')
  const ctx = cnv.getContext('2d')

  // 第一条线,粉色
  ctx.moveTo(50, 30)
  ctx.lineTo(150, 30)
  ctx.strokeStyle = 'pink' // 粉色描边
  ctx.lineWidth = 10 // 描边粗细
  ctx.stroke()

  // 矩形
  ctx.strokeStyle = 'red' // 红色描边
  ctx.strokeRect(50, 50, 200, 100)
</script>

这个例子中,绘制矩形 rect 前并没有用 beginPath() ,但矩形的红色描边并没有影响直线的粉色描边。

其实还不止 strokeRect() ,还有 fillRect()strokeText()fillText() 都不会影响其他图形,这些方法都只会绘制图形,不会影响原本路径。


closePath()

closePath() 方法可以关闭当前路径,它可以显示封闭某段开放的路径。这个方法常用于关闭圆弧路径或者由圆弧、线段创建出来的开放路径。

closePath() 是关闭路径,并不是结束路径。

关闭路径,指的是连接起点与终点,也就是能够自动封闭图形。

结束路径,指的是开始新的路径。


基础用法

举个例子会更直观

file

<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.moveTo(50, 40)
  ctx.lineTo(150, 40)
  ctx.lineTo(150, 140)
  ctx.stroke()
</script>

上面的代码通过 moveTolineTo 画了3个点,使用 stroke() 方法把这3个点连起来,就形成了上图效果。

但如果此时在 stroke() 前使用 closePath() 方法,最终出来的路径将自动闭合(将起点和终点连接起来)。

file

<canvas id="c" width="300" height="200" style="border: 1px solid #ccc;"></canvas>

<script>
  const context = document.getElementById('c')
  const ctx = context.getContext('2d')

  ctx.moveTo(50, 40)
  ctx.lineTo(150, 40)
  ctx.lineTo(150, 140)
  ctx.closePath() // 关闭路径
  ctx.stroke()
</script>

注意事项

看到上面的例子后,可能有些工友会觉得使用 ctx.lineTo(50, 40) 连接回起点也有同样效果。

// 省略部分代码
ctx.moveTo(50, 40)
ctx.lineTo(150, 40)
ctx.lineTo(150, 140)
ctx.lineTo(50, 40)
ctx.stroke()

确实在描边为1像素时看上去效果是差不多的,但如果此时将 lineWidth 的值设置得大一点,就能看到明显区别。

file

// 省略部分代码
ctx.lineWidth = 10
ctx.moveTo(50, 40)
ctx.lineTo(150, 40)
ctx.lineTo(150, 140)
ctx.lineTo(50, 40) // 连接回起点
ctx.stroke()

如果用 closePath() 自动闭合路径的话,会出现以下效果

file

// 省略部分代码
ctx.lineWidth = 10
ctx.moveTo(50, 40)
ctx.lineTo(150, 40)
ctx.lineTo(150, 140)
ctx.closePath() // 关闭路径
ctx.stroke()


本文到此就完结了,但 canvas 的知识点还没完,还有很多很多,根本学不完的那种。

接下来 本专栏 的文章会偏向于 知识点 + 案例 的方式讲解 canvas



代码仓库

⭐雷猴 Canvas



推荐阅读

👍《Canvas 从入门到劝朋友放弃(图解版)》

👍《Canvas 10款基础滤镜(原理篇)》

👍《Fabric.js 从入门到膨胀》

👍《『Three.js』起飞!》

👍《p5.js 光速入门》

👍《SVG 从入门到后悔,怎么不早点学起来(图解版)》


点赞 + 关注 + 收藏 = 学会了 代码仓库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/111114.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

向量杂记(python)

以 O 点为圆心&#xff0c; 绘制如上图&#xff0c; 则有向量 vOA&#xff0c; vOB, vOC, vOD 再给定一个X轴方向的单位向量 vX [1, 0] 则 向量 vOA&#xff0c; vOB, vOC, vOD 分别与 vX 进行叉乘&#xff1a; # python 代码 # X Y 轴上的单位向量 vX [1, 0] vY [0, 1]vA …

Matter理论介绍-通用-1-05:桥接设备-发现与配置流程

【源码、文档、软件、硬件、技术交流、技术支持&#xff0c;入口见文末】 【所有相关IDE、SDK和例程源码均可从群文件免费获取&#xff0c;免安装&#xff0c;解压即用】 持续更新中&#xff0c;欢迎关注&#xff01; 一、桥接设备的发现 桥接设备如何被其他Matter终端发现呢…

论文阅读《FSCE: Few-Shot Object Detection via Contrastive Proposal Encoding》

论文地址&#xff1a;https://arxiv.org/pdf/2103.05950.pdf 代码地址&#xff1a;https://github.com/megvii-research/FSCE 对比学习https://zhuanlan.zhihu.com/p/346686467 目录1、存在的问题2、算法简介3、算法细节3.1、new-baseline3.2、对比建议编码3.3、对比建议编码的…

(第二章)OpGL超级宝典学习:创建我们第一个OpenGL程序,绘制一个点和三角形

目录前言创建简单的应用创建脚本渲染底色渲染底色plus使用着色器&#xff08;shader&#xff09;什么是着色器&#xff08;shader&#xff09;GLSLOpenGL的着色阶段编写着色器顶点着色器片段着色器shader对象的创建执行程序的创建删除无用的shader顶点数组对象VAO清理VAO和Prog…

跨孔CT交叉梯度联合反演算法

跨孔CT交叉梯度联合反演算法 利用交叉梯度函数的结构约束作用实现多种跨孔CT的联合反演。 文章目录跨孔CT交叉梯度联合反演算法1、跨孔CT原理2、跨孔CT正演算法2.1 多模块快速推进算法2.2 最短路径法弯曲射线追踪2.3 胖射线追踪2.4 直射线追踪3、跨孔CT正演模型4、交叉梯度联合…

Matlab渐变色生成/编辑器专业版—ColorMapPro

渐变配色在科研绘图时十分常用&#xff0c;比如三维柱状图、特征渲染散点图、热图、曲面图、等高线填充图等等&#xff0c;都需要使用渐变配色。 虽说现成的渐变配色有很多&#xff0c;比如&#xff0c;在TheColor配色工具的渐变颜色库中便集成了近千种的渐变色&#xff1a; 然…

Lambda Python 创建和使用 Layer

目标 由于 Python 代码中需要用到第三方的库, 在 Lambda 中可以通过创建自定义层(Layer)的方式来将这些依赖提供给 Lambda 运行环境. 下面演示创建一个包含第三方库 requests 的层, 以及如何在 Lambda 中调用 准备 zip 文件 首先在本地电脑上将相关依赖下载到名为 python 的…

32.多输入多输出通道

通道数是一个重要的超参数&#xff0c;通常是会仔细设计的。 在之前&#xff0c;我们仅展示了单个输入和单个输出通道的简化例子。 这使得我们可以将输入、卷积核和输出看作二维张量。 当我们添加通道时&#xff0c;我们的输入和隐藏的表示都变成了三维张量。例如&#xff0c…

OpenPCDet详细安装指南+单/多GPU训练演示

OpenPCDet详细安装指南单/多GPU训练演示前言个人环境一览安装环境安装的各版本1. 创建conda虚拟环境2. 安装PyTorch3. 安装spconv4. 安装PCDet5. 训练单GPU训练多GPU训练前言 本人总共花了两个晚上一个下午安装&#xff0c;中间因为各种版本错误、版本不兼容报各种奇奇怪怪的错…

Windows 下使用 Docker + MySQL 安装 Wiki.js

Windows 下使用 Docker MySQL 安装 Wiki.jsIntroductionChapter 1 配置数据库Part 1 数据库安装Part 2 创建 Wiki 相关配置并修改权限Chapter 2 配置 Wiki.jsPart 1 安装Part 2 配置 WikiIntroduction Wiki 是一种在网络上开放且可供多人协同创作的系统&#xff0c;比较著名的…

【触摸屏功能测试】昆仑通态MCGS——测试通过HJ212_TCPIP驱动4G功能发送212报文

1、触摸屏功能测试 型号&#xff1a;TPC7022Ni 测试内容&#xff1a; 实现4G触摸屏&#xff0c;通过自带的4G卡向平台发送HJ212报文 2、HJ212_TCPIP 驱动说明 2.1、驱动介绍 本驱动构件用于MCGS软件,通过污染物在线监控(监测)系统数据传输标准协议向下位机数采仪发送各类指…

MCU-51:静态数码管显示

目录一、数码管介绍2.1 介绍2.2 数码管引脚定义2.3 原理图二、要用到的C语言知识2.1 数组2.2 子函数三、数码管显示3.1 单个数码管显示3.2 通过定位函数实现数码管显示四、动态数码管显示一、数码管介绍 2.1 介绍 LED数码管&#xff1a;数码管是一种简单、廉价的显示器&#…

Word表格常见设置汇总

Word表格常见设置汇总 总结一些Word中常用的关于表格的设置问题。 文章目录1.居中设置2.隔页显示表格首行设置3.边框边线粗细及显示设置4.表格斜线设置5.单元格合并与拆分设置1.居中设置 全选要居中显示部分→布局→选择居中类型。 2.隔页显示表格首行设置 选择表格→表格属性→…

idea插件之自动生成时序图(Sequence Diagram)

前言 小伙们&#xff0c;你们有没有发现&#xff0c;身边经常有这样一些人&#xff0c;看着平时没怎么干活&#xff0c;到关键时候需要展示工作成果&#xff0c;会发现这些人也有工作成果&#xff0c;甚至比辛苦在干的人可能还要多一些。按常理推测&#xff0c;这绝对不是一件科…

【小程序】案例 - 本地生活(首页)

1. 首页效果以及实现步骤 新建项目并梳理项目结构 配置导航栏效果 配置 tabBar 效果 实现轮播图效果 实现九宫格效果 实现图片布局 2. 接口地址 获取轮播图数据列表的接口 【GET】 https://www.escook.cn/slides 获取九宫格数据列表的接口 【GET】 https://www.esco…

XXE工具——XXEinjection安装与使用

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是XXE工具——XXEinjection安装与使用。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#xff1…

直播推流神器 Kplayer 手把手教你在B站7*24h全天直播

开始前的准备工作 Linux服务器 (1)KPlayer目前仅支持Linux环境并需要满足x86_64(amd64)与aarch64(arm64)CPU架构的硬件环境上运行&#xff0c;我们已经将相关依赖库静态链接至主程序中&#xff0c;这意味着你不需要额外的安装任何的第三方库来支持KPlayer的运行。 在后续的迭代…

【数据预处理】基于Pandas的数据预处理技术【前七个任务】

文章目录一.需求分析二.需求解决2.1 本次实验以california_housing加州房价数据集为例&#xff0c;下载数据集2.2 查看数据集的描述、特征及目标数据名称、数据条数、特征数量2.3 将数据读入pandas的DataFrame并转存到csv文件2.4 查看数据集各个特征的类型以及是否有空值2.5 对…

五年经验的前端社招被问:CPU 和 GPU 有什么区别?

CPU 和 GPU 的设计目标和整体架构的区别分析&#xff0c;并在全文最后使用通俗的例子做比喻帮助理解。 首先来看 CPU 和 GPU 的百科解释&#xff1a; CPU&#xff08;Central ProcessingUnit&#xff0c;中央处理器&#xff09;&#xff1a;功能主要是解释计算机指令以及处理计…

【工作流Activiti7】2、Activiti7 与 Spring Boot 及 Spring Security 整合

1. 前言 其实&#xff0c;选择用Activiti7没别的原因&#xff0c;就是因为穷。但凡是有钱&#xff0c;谁还用开源版的啊&#xff0c;当然是用商业版啦。国外的工作流引擎没有考虑中国的实际情况&#xff0c;很多像回退、委派、撤销等等功能都没有&#xff0c;所以最省事的还是…