​canvas 高级功能(上)

简介: ​canvas 高级功能(上)

canvas 高级功能(上)

在本文中,你将学习到 Canvas 提供的一些更高级的功能。你将看到在使用多种绘图样式时如何节省时间,以及如何转换和操作绘图来使其更激动人心。本文内容非常精彩,我希望这些内容能够拓宽你的眼界,帮助你学会画布的高级功能。

1. 保存和恢复绘图状态

有时我们经常在各种样式之间切换,甚至有时候会在不同颜色之间反复切换。这种重复是很麻烦的,它意味着如果你想要返回之前使用的一些样式,必须重写大量的代码。幸好,画布能够记住一些样式和属性,这样将来你就可以再次使用。这就是所谓的「保存」「恢复」画布绘图状态。然而,问题是,如果要记住多个状态,操作起来可能令人困惑,因为你必须跟踪所有发生的变化。但是不用担心,听完我的讲解你就会完全清楚其中的奥妙。

1.1 画布绘图状态

无论是在现实世界还是画布中,“状态”这个词都是用来描述事物在特定时刻所处的状况。重要的是要抓住与所描述时间直接关联的对象状态。例如,如 果我要描述你昨天及今天的状态,那么它们一定是两个完全不同的状态——你今天的状态可能不如昨天。简而言之,状态总在变化。

在画布中,绘图状态指的是描述某一时刻2D渲染上下文外观的整套属性,从简单的颜色值到复杂的变换矩阵(transformation matrix)及其他特性。我们将在本文后面介绍变换矩阵,所以请不必担心我刚刚提到的这些专业词汇。

用于描述画布绘图状态的全部属性为:变换矩阵、裁剪区域(clipping region)、global-Alpha、globalCompositeOperation、strokeStyle、fillStyle、lineWidth、lineCap、lineJoin、miterLimit、shadowOffsetX、shadowOffsetY、shadowBlur、shadowColor、font、textAlign和 textBaseline。在本文中,你将学习到大多数我们尚未接触过的属性。

有一点很重要,画布上的当前路径和当前位图(正在显示的内容)并不属于状态。我们更应该将状态看做2D渲染上下文属性的描述,而不是画布上显示的所有内容的副本。

1.2 保存绘图状态

保存画布状态非常简单。你需要做的就是调用 2D 渲染上下文的save方法。仅此而已。不需要怀疑,我们马上就来看看简单的save方法:

const canvas = document.getElementById("myCanvas");
const context = canvas.getContext("2d");
context.fillStyle = "rgb(255, 0, 0)";
context.save(); // 保存画布状态
context.fillRect(50, 50, 100, 100); // 红色正方形

那么,当你保存绘图状态时,实际上发生了什么呢?可以肯定的是,它必须保存在某个地方。2D渲染上下文会保存一个绘图状态栈,实际上它是一组之前保存的状态,其中最近保存的状态位于顶部——就像一叠纸。绘图状态的默认栈是空的,调用save方法,就会有一个新状态被放入(添加到)这个栈。这意味着,你完全可以多次调用save方法,将多个绘图状态逐一保存到栈中,其中最早的状态在底部。然而,这其中有一点不易理解,那就是你无法将任何绘图状态后移,因为这个过程是有严格顺序的。但是现在请不要担心一我们很快就会介绍多 状态。现在,我们先来了解一下如何访问刚刚保存的状态。

1.3 恢复绘图状态

访问一个已有绘图状态与保存它一样简单,唯一的区别是这次调用的是restore,方法。现在,如果你绘制另一个正方形,并且这次将fillSty1e设置为蓝色,那么很快会看到画布绘图状态的好处:

context.fillStyle = "rgb(0, 0, 255)";
context.fillRect(200, 50, 100, 100); // 蓝色正方形

这里并没有执行任何特殊操作,唯一修改的是填充颜色。

96fad41df953f5964dad6b3f35553ddc.png

但是,如果你想换回之前使用的红色填充颜色,该怎么做呢?我希望你不会考虑再次重写fillSty1e属性并将它设置为红色!哦,你没这样想?太聪明了!没错,你将颜色设置为红色之后保存了绘图状态,所以它已经存在于栈中了,你只需要在现有代码之前调用restore,就可以恢复原先的状态:

context.restore(); // 恢复画布状态
context.fillRect(350, 50, 100, 100); // 红色正方形

通过调用restore方法,你能够自动取出最后添加到栈中的绘图状态,并将它应用于2D渲染上下文,用所保存的状态覆盖全部现有的样式。这意味着,虽然你没有在代码中直接修改fillSty1e属性,但是它将取得所保存的绘图状态的值——它会变成红色。

670d52e050ef702af754c62579d41d78.png

如果只是修改颜色,效果可能还不够明显,但这个概念适用于所有能够保存到绘图状态中的画布属性。

1.4 保持和恢复多个绘图状态

在本文开头,我曾提到过一次处理多个状态有一些复杂。但是,在学完前面的内容之后,我希望现在你已经理解该如何处理它了。实话说,如果理解了栈的概念,并且明白新增的项被添加到栈的顶部,并且它们是从栈顶部取回的,那么你就不会觉得它复杂了。栈实际上采用一种后进先出的机制,最近保存到栈的绘图状态将是后来第一个恢复的状态。

如果你修改前面的例子,在将fillStyle设置为蓝色后保存绘图状态,就会明白我的意思:

context.fillStyle = "rgb(255, 0, 0)";
context.save();
context.fillRect(50, 50, 100, 100); //红色正方形
context.fillStyle = "rgb(0, 0, 255)";
context.save();
context.fillRect(200, 50, 100, 100); //蓝色正方形
context.restore();
context.fillRect(350, 50, 100, 100); //蓝色正方形

第三个正方形现在不是红色,而是蓝色。这是因为最后保存到栈的绘图状态是蓝色的fillSty1e,所以它最先恢复。

fa82448ac66f9cbe02d6bdada4bdd70c.png

另一个状态是红色的fillSty1e,它仍然在栈中等待,你只需要再调用一次restore就能够恢复这个状态:

context.restore();
context.fillRect(50, 200, 100, 100);//红色正方形

这会从栈返回最后一个状态,并将它删除,使栈变成空的。

d436e4484a0e825988d7fdeeb76f32bc.png

关于绘图状态的保存和恢复还有很多其他内容,本文的目的只是介绍一些基础知识。从现在开始,你就能够理解后续文章关于绘图状态的使用方法了。

2. 变形

到现在为止,你在画布中绘制的所有元素都是按照它应该出现的样子绘制的。例如,矩形是按照fillRect方法定义的位置和尺寸绘制的,并且它是用水平和垂直的线条绘制的,平淡无奇。但是,如果你想要画一些奇特的图形呢?如果想要旋转一个矩形呢?如果想要缩放图形呢?2D 渲染上下文的变形功能能够帮助你实现所有这样的操作。它们支持的功能是非常强大的。

2.1 平移

最基本的操作就是平移,即将2D渲染上下文的原点从一个位置移动到另一个位置。在画布中进行平移使用的是translate方法时,实际上它移动的是2D渲染上下文的坐标原点,而不是所绘制的对象。

translate方法的调用方式如下:

context.translate(150, 150);

两个参数是(x, y)坐标值,表示把2D渲染上下文的原点移动多远。一定要注意,将来你所指定的(x, y)坐标值会加上原点的平移,原点最初的默认值是(0, 0)。例如,如果执行两次与上面例子完全相同的平移,那么实际上是将原点在 x 轴方向移动300个单位(0+150+150),在y轴方向也移动300个单位(0+150+150)。

通过移动2D渲染上下文的原点,画布中的所有对象都将移动相应的距离:

context.fillRect(150, 150, 100, 100);
context.translate(150, 150);
context.fillStyle = "rgb(255, 0, 0)";
context.fillRect(150, 150, 100, 100);

一般情况下,第二次调用fillRect时,所绘制的正方形的原点坐标是(150, 150),但是由于执行了一次平移,这个正方形的原点现在变成(300, 300)

e8e1a59af545f95e5897a9564aedd263.png

一定要理解这其中的原理。红色正方形的原点仍然为(150, 150),它只是看上去又平移了150像素,这是因为在黑色正方形绘制之后,2D渲染上下文的原点已经平移了150像素。如果你希望红色正方形仍然出现在点(150, 150)原来的位置(即黑色正方形所在位置),那么可以直接将它的原点设为(0, 0)

context.translate(150, 150);
context.fillStyle = "rgb(255, 0, 0)";
context.fillRect(0, 0, 100, 100);

这是因为你已经将2D渲染上下文移动到位置(150, 150),所以从现在开始,所有在点(0, 0)绘制的图形实际上都显示在点(150, 150)上。

f38908171f5a3031380ef4737e1a5612.png

注意:每一种变形方法,包括平移,都会影响方法执行后所绘制的所有元素。这是因为它们都是直接在2D渲染上下文上操作的,而不是只针对所绘制的图形,这与你修改了fillSty1e等属性的效果一样,新的颜色会影响后来绘制的所有元素。

2.2 缩放

另一个变形方法就是缩放(scale),顾名思义,它是调整 2D渲染上下文的尺寸。它与平移的区别在于(x, y)参数是缩放倍数,而不是像素值。

context.scale(2, 2);
context.fillRect(150, 150, 100, 100);

这个例子将2D渲染上下文的xy方向都乘以2。通俗地说,2D渲染上下文及其绘制的所有对象现在都变成2倍尺寸。

单独使用scale将使所有绘图内容变大,而且它也会使一些对象被画在一些不恰当的位置上。例如,放大2倍实际上意味着现在1个像素变成2个像素,所以如果你绘制了一个x为150像素的图形,现在它看起来像是变成x为300像素了。如果这不符合你的要求,或者你只想要缩放一个图形,可以组合使用scaletranslate方法。

context.save();
context.translate(150, 150);
context.scale(2, 2);
context.fillRect(0, 0, 100, 100);

在这个例子中,首先保存画布的状态,再将原点平移到(150, 150)。然后,将画布放大两倍,在位置(0, 0)绘制一个正方形。因为已经将2D渲染上下文平移到(150, 150),所以这个正方形会被绘制在正确的位置,并同时放大两倍。

598e373cb8bcaac73dbbb6be84e9391d.png

问题是,从现在开始绘制的其他图形都将平移150像素并在两个方向同时放大两倍。幸好, 你已经完成了前面一半的工作:在执行变形之前保存了绘图状态。剩下一半工作是恢复之前保存的绘图状态。

context.restore();
context.fillRect(0, 0, 100, 100);

在恢复绘图状态之后,后面绘制的所有图形都不会出现变形效果。没错!我说过,保存和恢复绘图状态使你能够画出漂亮的图形。

726063ee27d88ca68a92877da8bd7797.png

2.3 旋转

如果要我选择一个最喜欢的变形功能,我肯定会选择rotate方法。通过旋转角度来打破正方形像素的概念。到现在为止,我们介绍的变形方法的共同特点是它们都很容易调用。rotate方法也不例外,你只需要传入以弧度为单位的2D渲染上下文旋转角度值即可:

context.rotate(0.7854); // 旋转45度(Math.PI/4)
context.fillRect(150, 150, 100, 100);

然而,这个旋转的结果可能并不是你所期望的。为什么正方形会旋转到浏览器边界以外呢?f42a82d7478f08e656e4a18196599a15.png

出现这种结果,是因为rotate方法是把2D渲染上下文绕其原点(0, 0)进行旋转的,在前面这个例子中,原点是屏幕的左上角。因此,你所绘制的正方形本身是不会旋转的,它现在实际上是以45度角绘制到画布中。

当然,如果你只想旋转所要绘制的图形,那么这样肯定不行。这时,仍然还需要使用translate方法。要实现所期望的效果,需要将2D渲染上下文的原点平移到正在绘制的图形的中 心。然后,再对画布执行一次旋转,接着在当前位置绘制图形。这个过程描述起来有些复杂,所以让我们用示例代码来演示这个过程:

context.translate(200, 200); // 平移到正方形中心
context.rotate(0.7854); // 旋转45度角
context.fillRect(-50, -50, 100, 100); // 以旋转点为中心绘制一个正方形

这样你会得到一个旋转 45 度角的正方形,它正位于你想要的位置。

1fda8206fa1305ed3015da3e2e74b8d4.png

注意:执行变形的顺序是极为重要的。例如,如果在执行平移之前将画布旋转45度,那么你会在45度角上进行平移。所以如果绘图时出现错误,那么请先检查顺序!

2.4 变换矩阵

现目前为止,你所使用的所有变形方法都会影响一个东西,那就是「变换矩阵」。我们不讨论一些非必要的细节(这些细节信息并不重要),变换矩阵就是一组数字,它们各自描述一个稍后将会介绍的特定变形类型。矩阵分成多个列和行,在画布中,你使用的是一个3×3矩阵——3列和3行。

轴缩放轴倾斜轴平移轴倾斜轴缩放轴平移

你可以忽略最后一行,因为你不需要也不能修改它的值。最重要的是第一行和第二行,其中包含的数字值对应画布中使用的至f。你可以看到,每一个数字值都对应一种特定的变形。例如,表示在 x 轴的缩放倍数,表示在 y 轴的平移。

现在,在学习如何手动处理变换矩阵之前,我先说明一下这个矩阵的默认值。一个新的 2D 渲染上下文将包含一个全新的变换矩阵,即单位矩阵(identity matrix)。

除了左上角至右下角的主对角线以外,这个特殊矩阵的每个值都设置为0。这样设置的唯一原因是它更适合进行计算,但是可以确定的是,单位矩阵表示完全未执行过变形。全面理解单位矩阵的含义并不是很重要,重要的是要知道变换矩阵中的默认值是什么。

「操作变换矩阵」

这里要介绍的最后两个方法是transformsetTransform。它们能够帮助我们操作 2D 渲染上下文的变换矩阵。我们已经了解了足够多的基本概念,所以现在让我们使用transform执行一个平移和缩放,然后再绘制一个正方形,以此说明它的作用:

context.transform(2, 0, 0, 2, 150, 150);
context.fillRect(0, 0, 100, 100);

transform方法有 6 个参数,分别对应变换矩阵的每一个值,第一个表示 a ,最后一个表示 f 。在这个例子中,你想将画布的尺寸放大 2 倍,所以将第 1 个和第 4 个参数设置为2,即 a 和 d 一分别对应 x 轴缩放和 y 轴缩放。可以理解。而如果要平移画布原点呢?没错:你需要设置第 5 个和第 6 个参数,即 e 和 f ——分别对应 x 轴平移和 y 轴平移。

9fabaded8172a81f4bda58f5d98f292a.png

希望你现在已经理解了它的使用方法,手动操作变换矩阵其实并不复杂。只要理解每一个值的意义,就能够执行正确的操作。现在让我们用变换矩阵执行一些更高级的变形——旋转!

不使用rotate方法执行旋转变形似乎有些复杂,但是如果你听我讲下去,很快就能明白这样做的意义:

context.setTransform(1, 0, 0, 1, 0, 0); // 单位矩阵
const xScale = Math.cos(0.7854);
const ySkew = -Math.sin(0.7854);
const xSkew = Math.sin(0.7854);
const yScale = Math.cos(0.7854);
const xTrans = 200;
const yTrans = 200;
context.transform(xScale, ySkew, xSkew, yScale, xTrans, yTrans);
context.fillRect(-50, -50, 100, 100);

首先,你需要调用setTransform方法。这是第二个操作变换矩阵的方法,它的作用是将矩阵重置为单位矩阵,然后按照 6 个参数执行变形。在这个例子中,使用它来重置变换矩阵,从而保证你操作的是一个原始状态的变换矩阵。然后,为一些变量赋值,它们是调用transform方法所使用的参数。有了这些作为参数的变量,就能够使整个过程变得更加简洁和清晰,而且更容易理解。

需要指出的是,transform方法实际上是将现有的变换矩阵乘以你所指定的值,而不是直接设置变换矩阵的值。这意味着其中会有一个累积效应。如果你多次调用transform,那么每一次变形都是应用到前一个变形所得到的变换矩阵。

使用变换矩阵进行旋转是倾斜和缩放的组合效果。为此,你需要给三角函数cos(余弦)和sin(正弦)传入以弧度为单位的角度值。

最后,将所有代码编写出来,你会得到下面的结果一一个漂亮的旋转后的正方形。

9b8c7684dce4d649f0765f856c9d00b4.png


相关文章
|
2月前
|
缓存 JavaScript 前端开发
vue2基础组件通信案例练习:把案例Todo-list新增编辑按钮
vue2基础组件通信案例练习:把案例Todo-list新增编辑按钮
26 4
|
4月前
|
前端开发 JavaScript
Cesium案例解析(八)——CesiumWidget简化窗体
Cesium案例解析(八)——CesiumWidget简化窗体
115 0
|
5月前
Element UI 源码改造 —— 自定义数字输入框的实现
Element UI 源码改造 —— 自定义数字输入框的实现
199 1
|
7月前
|
移动开发 小程序 API
uniapp组件库Line 线条 的适用方法
uniapp组件库Line 线条 的适用方法
368 0
|
7月前
|
移动开发 小程序 API
uniapp中组件库Mask 遮罩层 的使用方法
uniapp中组件库Mask 遮罩层 的使用方法
627 1
|
7月前
|
移动开发 JavaScript 小程序
uniapp中组件库丰富的Switch 开关选择器使用方法
uniapp中组件库丰富的Switch 开关选择器使用方法
460 1
|
7月前
|
iOS开发
iOS设备功能和框架: 如何使用 Core Animation 创建动画效果?
iOS设备功能和框架: 如何使用 Core Animation 创建动画效果?
143 0
Echarts实战案例代码(41):自定义map背景图片
Echarts实战案例代码(41):自定义map背景图片
576 0
|
XML 程序员 Android开发
高级UI系列(一): 自定义UI理论篇(1)
自定义view是区分中级开发和初级开发的分水岭,虽说今年校招,工作三四年的老程序员一直在劝退客户端,作为职场打拼多年的老菜鸟,对android还是挺有信心的,虽说对view的知识也只是停留在纸上,很少真正落地做一些复杂高性能的ui控件,之前在akulaku确实见识了一群技术大牛,高级ui控件伸手就来,让我羡慕不已,这一次我也从基础到源码再到实战开始写几篇自定义view教程。大家有什么好的见解也欢迎到评论区多多交流。
168 0
高级UI系列(一): 自定义UI理论篇(1)
|
XML 存储 前端开发
高级UI系列(一): 自定义UI理论篇(2)
简介: 自定义view是区分中级开发和初级开发的分水岭,虽说今年校招,工作三四年的老程序员一直在劝退客户端,作为职场打拼多年的老菜鸟,对android还是挺有信心的,虽说对view的知识也只是停留在纸上,很少真正落地做一些复杂高性能的ui控件,之前在akulaku确实见识了一群技术大牛,高级ui控件伸手就来,让我羡慕不已,这一次我也从基础到源码再到实战开始写几篇自定义view教程。大家有什么好的见解也欢迎到评论区多多交流。
163 0
高级UI系列(一): 自定义UI理论篇(2)