likes
comments
collection
share

canvas核心速通

作者站长头像
站长
· 阅读数 45

前言

用canvas做过需求的朋友多少应该写过ctx.beginPath方法,一旦某个位置少了这个方法的调用,canvas绘制就可能出现预期之外的效果。依稀记得一年前初学canvas的时候有一个对canvas的描述——“基于状态的绘图”,当时似懂非懂,直到最近写业务彻底搞清楚然后来简单分享一下。

如果

  1. 你想入门canvas,宏观上对canvas的绘制流程有一个清晰的把握
  2. 你不是很清楚究竟beginPath()的作用以及何为“canvas是基于状态的绘图”

可以看下文章

canvas绘图入门与理解

咱们不去细说api,从宏观上,我谈一下我对canvas绘图的理解。

给不太熟悉canvas的朋友们多说一些:我们在绘制canvas时需要先获取canvas元素对应的上下文对象,然后通过上下文对象去绘制对应的canvas,如下面的栗子:

<canvas id="demo"></canvas>
<script>
const canvasElm = document.getElementById('demo');
canvasElm.width = 300;
canvasElm.height = 300;
canvasElm.style.border = `1px solid red`;

// 创建canvas上下文
const ctx = canvasElm.getContext('2d');
</script>

既然代码写出来了,我还是多说两句,把代码解释清楚:在js中拿到<canvas/>标签的对应domcanvasElm后,通过canvasElm.width/height来设置的宽高其实并不是真正意义上的设置dom的宽高样式,这里类似于申请canvas画布的像素点,只是说在不显式设置canvas的css样式时canvas像素点大小等于一个css像素大小。也就是说经过赋值canvasElm.width/height后我们的canvas是一张300 * 300的画布(用于绘制定位)。并且我们没有显式设置canvas的css宽高,所以canvas的宽高就默认也等于300px * 300px了。从这里可以延伸到关于canvas画质优化的一个手段,无关主题咱就不多说了。然后canvasElm.getContext('2d')返回了一个canvas上下文对象,调用这个对象上的api就可以完成对<canvas id="demo">的绘制。

canvas绘制的过程是先构思再动笔的过程。 这里的构思并不是指我们大脑里的构思,而是说canvas上下文上提供的绘制相关的api大体可以分为两种,一种是“构思api”,另一类就是“动笔api”,看下面的demo秒懂:

const ctx = canvasElm.getContext('2d');
ctx.moveTo(0, 0); // 这里的0, 0和下面的150, 150都是针对上面我们所说的canvas画布大小,canvas画布左上角为(0, 0)点,向右为x轴,向下为y轴
ctx.lineTo(150, 150);

moveTolineTo就是“构思api”,顾名思义moveTolineTo就是移动的意思,他们在描述一个路径,moveTo指移动到某个具体的点,lineTo就是基于现在的位置与目标位置画一条线。但是他们并不会真正的进行绘制,也就是说经过上面的操作,canvas上并没有真正的出现绘制的线条,我们把moveTolineTo产生的痕迹理解为一种构思。有了“构思”,现在就需要“动笔api”来将构思“变现”。

const ctx = canvasElm.getContext('2d');
ctx.moveTo(0, 0);
ctx.lineTo(150, 150);
// 动笔api——stroke
ctx.stroke();

所谓stroke,虽然我我不知道为啥叫stroke(中风),但作用就是连线,将“构思”产生的线条真正的画在canvas上。经过上面“构思”+“动笔”的操作,我们的canvas上就有了一条从左上角到中心的黑线。

理解基于状态的绘图

先看一段代码结合绘制效果感受一下:

const ctx = canvasElm.getContext('2d');
ctx.moveTo(0, 0);
ctx.lineTo(150, 150);
ctx.stroke(); // 默认连线的颜色是黑色,我们可以通过ctx.strokeStyle = color来修改stroke时连线的颜色
ctx.strokeStyle = "blue"; // 修改连线颜色
ctx.lineTo(300, 300);
ctx.stroke();

基于代码的逻辑我们分析最后三行:

  1. 修改strokeStyle,那么以后stroke方法绘制出来的线条为蓝色
  2. ctx.lineTo(300, 300);即从(150, 150)(300, 300)之间“构思”一条线
  3. 调用stroke方法绘制这条线,那么我们的canvas画布上应该从(0, 0)(150, 150)之间为黑线,(150, 150)(300, 300)之间为蓝线

但是实际上绘制出来的结果是从(0, 0)(300, 300)一整条线都是蓝色的。

先给结论:

结论

canvas中的绘制方法(“动笔api”)都以上一次beginPath之后的所有“构思”的路径为基础进行绘制

基于结论解释一下上面的代码:我们创建上下文之后默认就调用了一次ctx.beginPath来开启一个新路径,所以上面没写,但我们明确上面的所有canvas代码是处于同一个beginPath中的,上面代码中第二次stroke方法调用时,我们是基于上面的所有“构思api”产生的路径的。之所以我们看到一整条蓝色的线是因为第一个stroke方法绘制的黑色线条被第二次stroke方法绘制的蓝色线条覆盖了(第二次stroke方法绘制时,路径中已经有了从(0, 0)到(300, 300)的线)。

如果我们想绘制(0, 0)(150, 150)的线条为黑色,(150, 150)(300, 300)为蓝色,可以修改代码如下:

const ctx = canvasElm.getContext('2d');
ctx.moveTo(0, 0);
ctx.lineTo(150, 150);
ctx.stroke();
ctx.strokeStyle = "blue";

ctx.beginPath(); // 上面第一次stroke绘制完成,为了让下面的stroke不受上面的“构思”的影响,调用beginPath来开启一个新的路径
ctx.moveTo(150, 150); // 在一个新的Path中,相当于清空了曾经Path中的“构思”(路径),所以我们需要先调用moveTo先移动到一个点,换句话说lineTo方法需要前面有一个点为基础,不然无效
ctx.lineTo(300, 300);
ctx.stroke(); // 绘制出来了从(150, 150)到(300, 300)的蓝色线段 且 (0, 0)到(150, 150)的黑色线段安然无恙

closePath与beginPath的关系

答:没有关系,ctx.closePath相当于一个“构思api” ,基于上面我们对于canvas绘制的理解举个例子:

ctx.moveTo(200, 0);
ctx.lineTo(200, 300);
ctx.lineTo(150, 150); // 当下已经“构思”了两条线
ctx.closePath(); // closePath即让当前的构思线条起点与终点相连,形成闭合区域
ctx.stroke(); // 绘制出一个封闭的三角形

beginPath作用是保证我们两个不同的绘制(“动笔api”)不相互影响。

理解何为“基于状态”

状态的概念我们并不陌生,vue与react这些前端框架的核心思想都是“状态驱动视图”,这里的状态可以理解为数据;那么canvas中,每一个beginPath都相当于开启一个新的路径,canvas中的状态完全可以理解为路径中我们“构思”的线条,最终调用stroke\fill等绘制方法时绘制出来的效果是基于lineTo等方法构造出来的“状态”的。

转载自:https://juejin.cn/post/7244089396567769144
评论
请登录