![微信小游戏开发:前端篇](https://wfqqreader-1252317822.image.myqcloud.com/cover/316/46670316/b_46670316.jpg)
第4课 绘制挡板
第3课我们学习了如何通过改变font、fillStyle等渲染上下文对象属性来实现文本绘制。这节课我们开始练习几何绘制,完成一个基本的游戏元素——挡板的绘制。
怎么绘制挡板呢?挡板是一个矩形,如果有一个Canvas API能绘制直线,连续绘制4次,便可以完成矩形绘制,是不是?
如何在画布上绘制直线
挡板在游戏中将用于接挡运动中的小球,那么如何在画布上画一个挡板呢?
如果我们将挡板看作一个带颜色的几何图形,那么这个问题可以转化为如何绘制一个矩形。在Canvas绘制中,使用moveTo、lineTo可以绘制直线,我们可以沿矩形的四边依次调用lineTo,达到绘制目的。为了简单起见,可以先自上而下绘制一条线作为挡板,只要线条足够宽,看起来也会像挡板。
moveTo是把路径移动到画布中的指定点,不创建线条,lineTo是同时创建线条,它们的调用语法为:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/66_01.jpg?sign=1739665120-TmyZOsSSXvJwnlfxEinagGAGFXfDcJJl-0-68852d323755a23c9a1633aa726a97f3)
将第3课的源码复制到第2章/2.2目录下,删除不必要的注释及已经注释掉的代码,在其基础上修改,最终绘制右挡板的示例代码如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/66_02.jpg?sign=1739665120-SAl1ZW2RyHPMDUwab2jbOUX6xHPh7sYH-0-e1e336ed33d1c8418d1a51b535d8ec63)
上面的代码发生了什么呢?
□在绘制直线时,一般是一个moveTo操作后面跟着多个lineTo操作。第6行使用moveTo将笔触移到画布右边缘、中心向上50px的地方,第7行使用lineTo向下绘制到中心向下50px的地方。moveTo与lineTo方法,以及其他Canvas API方法,默认单位都是px。
□strokeStyle是线条样式属性,类似我们在前面用过的fillStyle属性,它能够接收的值同样有3类:颜色(Color)、颜色渐变对象(CanvasGradient)和填充材质(CanvasPattern)。第5行使用的线条样式是black,black属于CSS定义的颜色名称,代表十六进制颜色值#000。
运行一下,我们发现没有效果,在画布右边什么也没看到。这是为什么呢?
这是因为moveTo与lineTo并不发生真正的绘制,仅是在定义绘制路径,要想完成真正的绘制,必须要使用beginPath和stroke方法,示例如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/66_03.jpg?sign=1739665120-SYR1Xodmp2gm96cyKWBLYX0rgYGDhNx1-0-d8d5022a84f2115a54540985ab2f4f2f)
在上面的代码中,要注意以下方面。
□第6行,beginPath方法开始或重置一条路径绘制,这个方法必须在路径绘制前调用,即在moveTo调用前调用。
□第9行,stroke方法会真正绘制出第7行、第9行定义的路径。beginPath可以没有,但stroke方法不可以没有。
修改后的运行效果如图2-15所示。
在画布右边缘,贴着一条黑色细线(见图2-15中的箭头所指),这就是我们目前的右挡板。
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/67_01.jpg?sign=1739665120-wDfFD5JpM9ytbCYHihcdH0oZ85W0he83-0-08d2be226725605ad52051ecbf5f7950)
图2-15 直线绘制的右挡板
思考与练习2-6:以整张画布作为矩形,尝试绘制它的两条红色对角线看看。
拓展:JS的8个基本数据类型,如何进行类型判断
看一下下面这行在实战中出现过的代码:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/67_02.jpg?sign=1739665120-tc33vOSa2eAw4z1loQlf9O7Ox6VGCEPR-0-fc7cf8ecc7822559233073b82976c1f9)
JS的数据类型是隐性的,赋值时是根据右边的值自动判断的,在声明变量或常量时并不需要显式描述。panelHeight在声明时被赋值为100,100是数值,所以panelHeight是一个数值(number)类型常量。JS不像其他高级语言(例如Go语言)一样,在声明变量时需要显式指定变量类型,示例如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/67_03.jpg?sign=1739665120-t5RYuAYYOKFtN4zi50HL8W1L2O8YLPva-0-dfedc67c67d8f3ded9827a8cea4b19f4)
在Go语言中,var是声明变量的关键字,string是字符串类型。
注意:Go语言的变量,如果声明时有字面量赋值,类型也可以省略,类型是由编译器自动推导的;但如果没有赋值,则不可以省略。详细情况参见《微信小游戏开发:后端篇》第12章第28课的相关内容。
包括前面已经提到的数值类型,JS共有8种基本数据类型。
□null:对象空值,只有一个值null。
□undefined:变量未定义值,只有一个undefined,没有被赋值的变量会有这个默认值。
□boolean:布尔值,只有两个值,即true和false。
□number:数值类值,panelHeight就是一个数值常量。JS没有整型类型,整型和浮点数都是number类型,以64位浮点数存储。NaN是一个特殊的数值常量,表示非数值,与NaN相关的全局函数isNaN用于判断变量是不是非数值。
□bigint:数字类型,可以安全地存储和操作大整数,即使这个数已经超出了number能够表示的范围。
□string:字符串。
□symbol:独一无二的值,是ES6新引入的基本数据类型,用于解决属性名重复的问题。
□object:对象,在内存堆中存储的、可以被标识符引用的一块内存区域,在JS中一切皆为对象。
注意:当我们谈论基本数据类型时,一般用小写字母开头的单词表示,例如string、boolean、number等;而大写字母开头的单词则代表全局构造函数,一般在new关键字后面使用,例如new Number、new Boolean、new String等。对于引用类型,例如Object、Array、Map、Set等,仍然使用大写字母开头。
思考与练习2-7(面试题):JS有哪些数据类型,它们在内存存储上有什么区别?
如何判断一个变量或常量的数量类型是什么呢?有4种方法,下面具体来介绍。
1.所有基本数据类型都可以用typeof判断
看一个示例:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/68_01.jpg?sign=1739665120-iV3sw9kjdy35yeBWc6ue89cnnMxUkNBX-0-d137468e8a9b24d67547d4dda9bc4725)
在上述代码的最后一行中,null的类型是null,但是使用typeof检测,却返回Object,这是为什么呢?此处误判是因为JS的基本数据类型在底层都是以二进制形式表示的。typeof在进行判断时,如果二进制前3位数字都是0,它就认为是Object类型,而null的二进制前3位恰好都是0(事实上它的所有位都是0),所以null被误判为Object类型,这是JS早期的设计缺陷造成的。以下是主要基本数据类型的二进制前缀列表:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/68_02.jpg?sign=1739665120-HEDFjQKW96Wxp4HM1rLgoICoarorMtIn-0-1eab19eb39c0701423089dcc212bde24)
像Array、Map、Set、RegExp、Date等数据类型均是Object的子类,使用typeof检测时,它们均会返回object;所有自定义类的实例变量,使用typeof检测时,也都是返回object。
同是Object类型,那么如何判断一个Object类型具体是哪个对象呢?这就会涉及第2个方法。
2.对于Object类型,可以使用instanceof关键字判断
instanceof可以判断对象是否属于某个具体的Object类型,并返回布尔值。instanceof是一个单词的组合,像一个全局函数,但是它在使用时更像一个操作符。一般情况下,方法在调用时后面要加一对小括号,而操作符是不需要的,所以instanceof是操作符。
来看一个示例:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/69_01.jpg?sign=1739665120-cCY2fMS6lrpKlvmUpKwCQ80G1omXPQ1D-0-d05f8299456978c8dc83a3117edba39b)
arr是一个数组变量,所以第2行用instanceof判断时会返回true。
注意,instanceof仅可以判断对象,不可以使用它代替typeof判断基本数据类型,例如:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/69_02.jpg?sign=1739665120-SR1O8ogqTDK6t7JGBSdZZGC8lABdtdwP-0-e70da2a78df7e997f1e9ea02d95dd514)
上述代码中,n是一个数值类型的变量,但第2行判断结果是false,这显然与实际不符。
有人可能会问,前面不是说“在JS中一切皆为对象”吗?既然一切皆为对象,那么作为数值类型的变量n也是对象,为什么这个变量不能用instanceof判断呢?
实际上,默认状态下基本数据类型的变量并不是对象,当把它们当作对象使用时(例如访问原型上定义的方法),JS才会将其“装箱”为对象;当把“装箱”后的对象当作基本数据类型使用时,JS又会将其“拆箱”为基本数据类型的变量。来看一个示例:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/69_03.jpg?sign=1739665120-mO27oDi6rhH42Gb8JoYwQjYv1dfW6IR4-0-98d79e2a8736931d057d6cdfe02b785b)
第3行使用构造函数实现了“装箱”操作,n变成了对象,第4行返回true。第5行执行递增操作,n又退化为基本类型的变量,第6行还是返回false。可见,“在JS中一切皆为对象”这句话并没有错。
思考与练习2-8(面试题):instanceof操作符的实现原理是什么?尝试实现一个具有同样功能的instanceOf函数(注意Of首字母是大写的)。
3.使用toString方法判断
除了typeof关键字,使用Object.toString方法也可以判断数据的基本类型,例如:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/69_04.jpg?sign=1739665120-tPDnWdxHV7Uwnejjd4JZyIEwI88B1QOG-0-c2b5bc86ffc81d56c12393bfbd7c7207)
将toString方法返回的内容与特定的字符串(例如[object Number])进行比较,便可以知道所测目标的类型了。
4.使用构造函数判断
在JS中,每个类型都有一个构造函数,因此也可以通过构造函数判断变量的类型。例如:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/70_01.jpg?sign=1739665120-1tdLezGdcqXdyo7qKzASi1WGlG9gw0xF-0-92f5222a25f60635821172668efd42cd)
以上这些表达式都返回了true。基于构造函数可判断值类型、引用类型及非空类型。
思考与练习2-9(面试题):列出可判断JS变量数据类型的具体方法。
给画布添加一个浅色背景
现在的绘制效果,画布内与画布外都是白色,不便于区分。那能不能给画布添加一个浅色的背景呢?
答案肯定是可以的,既然现在我们已经学会了使用moveTo与lineTo,那么完全可以使用绘制直线的方法画一个与画布大小相同的浅色矩形,示例代码如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/70_02.jpg?sign=1739665120-2ervBXNe5idZwUOZbpjf46oiKGsPR7Ti-0-0975fca4f75feebba233554cae6955e3)
在上面的代码中,要注意以下几点。
□第5行,在绘制路径之前先调用beginPath方法,这是告诉画布的渲染上下文对象“我要开始绘制了”。第4行通过fillStyle属性设置填充颜色为whitesmoke(浅灰色)。
□第6行,先使用moveTo移动笔触到画布的左上角。画布的坐标系x轴是自左向右的,这点与数学坐标系相同,y轴是自上向下的,这点与数学坐标系相反。
□第7行至第10行,连续调用4次lineTo,分别移动笔触并绘制到画布的右上角、右下角、左下角与左上角。
看一下运行效果,如图2-16所示。
奇怪,在图2-16中,除了边框好像被加深了,画布并没有出现浅灰色背景,这是为什么呢?是因为whitesmoke这个颜色太浅,无法显示吗?
答案是因为路径没有填充。使用渲染上下文对象的fill方法可用于填充当前绘制的路径,修改代码如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/71_01.jpg?sign=1739665120-fQIPhZhed4OrQ6q4WSZbKLk7IaoXw3AP-0-aaedd482937b63023ee7d4656a69e5c4)
路径绘制以stroke结束,填充绘制以fill结束,两者都要以beginPath开始。在调用fill方法之前,如果不设置fillStyle为whitesmoke,效果是黑色,默认的填充颜色是黑色。
最终运行效果如图2-17所示。
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/71_02.jpg?sign=1739665120-mAvpacmGUM01ZRWZCtwZP7dvqrqN3cQp-0-6852596f9929e501dba032fd10398724)
图2-16 用画线法尝试绘制的背景
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/71_03.jpg?sign=1739665120-Odoh2BJdVdhaSnO6zbRgsYmoQc8vash0-0-e234b99bc91371ca3a7b592c9a54ef68)
图2-17 画线法+填充颜色绘制的背景
如何加厚挡板
渲染上下文对象的lineWidth属性可以设置线条宽度,下面尝试使用这个属性改变挡板宽度。
修改代码,使挡板有10个像素宽,如下所示:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/71_04.jpg?sign=1739665120-Y4IQOIbziIUMlee6rXOw868QsXzZuQq7-0-c21c9114d35bf96ac1df6b304623a70e)
上面的代码只在原来代码的基础上添加了第5行代码,设置lineWidth为10,单位默认是px。
运行效果非常好,如图2-18所示。
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/71_05.jpg?sign=1739665120-5z3lxQuNi6gLXxsR8VcuaWo6A5nK3unx-0-232b3422a8b370507ce30c418c37400b)
图2-18 增加了宽度的线绘右挡板
右挡板在画布右边界非常显眼,但其实它的宽度并不是10px,而是只有5px。
思考与练习2-10:在本节示例中,为什么说右挡板的实际宽度只有5px呢?
拓展:JS中的数值类型、布尔类型是如何进行类型转换的
在上一节示例中有这样一行实战代码:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/72_01.jpg?sign=1739665120-O7Djh8SxAYXYADLanWtgwH4MIF005TVl-0-855c1d1bbf3c604256de017e691ebb8b)
如果将这行代码修改为:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/72_02.jpg?sign=1739665120-SMUk1xEVo6O5Kz9iunb6rTXfkO6UBWFL-0-7193c0a236d1ffab085db0cfa0913fde)
并不影响正确设置lineWidth属性。因为前者是一个数值,后者是一个字符串,后者在向lineWidth属性赋值之前,先进行了从字符串向数值的类型转化。
1.字符串向数值转换
下面以字符串10为例,讲解将字符串转换为数值的4种转换方法。
□使用全局的构造函数:Number("10"),这种方法很少使用。
□使用转换方法parseFloat或parseInt:parseInt("10px"),这种方式可以将非数值字符px剔除并成功转换。
□隐式自动转换:在期望是数值类型的地方,非数值类型会自动转换为数值类型。例如渲染上下文对象的lineWidth属性期望右值是一个数值,字符串"10"会自动向数值转换。
□使用加号与一个数字拼接:0+"10",这种方式最简单直接,其实本质上也是隐式转换。
2.数值向字符串转换
上面是将字符串转换为数值,反过来也可以将数值转换为字符串,并且会简单许多,字符串加任何数值(例如""+10)都会直接返回字符串。
思考与练习2-11(面试题):其他类型转换为数值类型具体有哪些规则?如何让一个自定义对象与数字进行四则运算?
3.其他类型转换为布尔值
在JS中,布尔类型转换是最常用的类型转换。以下6种类型将转换为布尔值,结果都是false:空字符串('' ")、整型数字(0)、浮点型(0.0)、特殊值(null)、非数字(NaN)、未定义值(undefined)。
未定义值是一个特殊空值,所有变量在声明而未赋值时,或者在未声明之前,其值都是undefined。undefined将转换为布尔值false。
思考与练习2-12(面试题):undefined作为一个全局的标识符,是可以被重写的吗?在开发中如何获取、使用安全的undefined值呢?
在if条件语句里经常会用到布尔值的自动转换。有时候我们会看到这样的代码:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/72_03.jpg?sign=1739665120-FQCY0FSANFt1NNI62U8Jw5cO0GvNzznj-0-2651b3ac022ebc20d2eebf5df2b65ffe)
一个感叹号(!)代表否定,两个感叹号(!!)代表否定的否定,这不等于白写吗?
其实这是一种更严谨的写法,尤其在期望值是布尔类型的地方。两个感叹号会强制转换右值为布尔类型,if(!!options.user)比if(options.user)更加严谨。
在JS中,空值null、未定义值undefined、字符串空值""、0和NaN,在预期值为布尔类型的地方(例如if条件、while条件、for循环条件处),都会自动转换为布尔值false。
思考与练习2-13:除了隐式自动布尔转换,还可以使用Boolean方法强制转换,例如Boolean(0)将返回布尔假。那么,对于下面这段代码,将输出什么?
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/73_01.jpg?sign=1739665120-jjBwAF3xnmKEUfivf4DDyodvOHuhQ90Q-0-01a2a9405d755aaa6434bfcecc864ec6)
空数组转换为布尔值时,会返回false吗?
如何给挡板添加圆角、阴影效果
目前挡板是方角的,太过生硬,能不能实现圆角效果呢?
1.添加圆角效果
渲染上下文对象的lineCap属性可用于设置线条末端线帽的样式,它有如下3种选项。
□butt:向线条的每个末端添加平直的边缘。
□round:向线条的每个末端添加圆形线帽。
□square:向线条的每个末端添加正方形线帽。
其中第二个选项round可以实现圆角效果,我们尝试修改代码:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/73_02.jpg?sign=1739665120-i71V09EwDtIPdkmVQaqJynqPOQQTDHoK-0-ed05a6e06b3d8b5fc282432b771f5742)
在上面的代码中,第6行代码将lineCap属性设置为了round。
运行效果如图2-19所示。
为什么挡板的两端是半圆形?
这是因为有一半的线条绘制到了画布外,绘制时使用的x坐标点是canvas.width,也就是说线条是“骑”在画布右边界线上绘制的。
当前挡板宽度为10px,现在我们调整挡板位置,向左移动5px,示例代码如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/73_03.jpg?sign=1739665120-zKGhqjOlaR2nZno0L3Ut0TUXnSRjcn2s-0-089cd303d207555edcae778830f33fa7)
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/74_01.jpg?sign=1739665120-XkBG7gFRChFzyUUWagpboxQFqOduPpw8-0-9a344d0e33a0191209a22906627f4790)
注意,第5行、第6行的代码有变化。
运行效果如图2-20所示。
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/74_02.jpg?sign=1739665120-USxfD2gyC9o45lHuYw84ccD3j5R90w8Y-0-70742dfbad6179669352c51bb79207f8)
图2-19 设置了圆角的线绘右挡板
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/74_03.jpg?sign=1739665120-xiuKTWcWDs4KgWLNuZGYqN2I4m9vaFMV-0-e8fadc00c677749b487e5cd0802248d9)
图2-20 修改了起绘位置的线绘右挡板
现在圆角全部显示了。但是二维图形效果稍显呆板,能不能给挡板添加阴影效果呢?
2.添加阴影效果
RenderingContext对象的shadowBlur、shadowColor等属性可以设置阴影效果。
RenderingContext对象有如下4个属性可用于设置阴影。
□shadowBlur:描述模糊效果程度,是一个大于或等于0的数值,可以是小数,负数会被忽略。它既不对应像素值,也不受当前转换矩阵的影响。
□shadowColor:设置或返回阴影的颜色。创建阴影效果时,需将shadowColor属性与shadowBlur属性一起使用。
□shadowOffsetX:设置或返回阴影与形状的水平距离。shadowOffsetX=0指示阴影正好位于目标下方(该方向看不到),shadowOffsetX=20指示阴影位于目标右侧的20像素处,shadowOffsetX=-20指示阴影位于目标左侧的20像素处。
□shadowOffsetY:设置或返回阴影与形状的垂直距离。shadowOffsetY=0指示阴影正好位于目标下方(该方向看不到),shadowOffsetY=20指示阴影位于目标上方20像素处。shadowOffsetY=-20指示阴影位于目标下方的20像素处。
现使用上面4个属性给画布添加阴影效果,示例代码如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/74_04.jpg?sign=1739665120-ZKuKeDDqbvNmQcdT9SYTZTLVLxk0wZuX-0-f7f2898b444d738b9b69df8ffe59fe45)
在上面的代码中,第4行至第7行是阴影的设置代码。阴影效果是对整张画布设置的,如果我们想让画布上绘制的所有图形都产生阴影效果,就必须把这些代码添加到绘制任何对象的代码之前。
运行效果如图2-21所示。
从图2-21中可以看出右挡板与游戏标题都是有阴影效果的,那么为什么画布的浅色背景没有阴影效果呢?因为阴影效果的呈现也需要空间,浅色背景是满画布绘制的,右下角已经没有地方显示阴影了。
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/75_01.jpg?sign=1739665120-tk0FwwrjCftn3OhNiPwjztaNAXURpR9b-0-0806ebf959fc0b69ffd5b547ca428e42)
图2-21 阴影效果
使用路径填充和矩形绘制挡板
目前我们的右挡板是使用绘制直线的方法绘制的,而浅色背景是使用路径填充的,那么我们能不能也使用路径填充的方式绘制右挡板呢?答案是可以的。
1.路径填充绘制
我们可以组合使用moveTo与lineTo,在挡板的4个角坐标依次移动绘制,事实上可以用这种方式绘制任何二维图形,示例代码如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/75_02.jpg?sign=1739665120-i7GMow5PCAe9lE3KEwy1UR3wfguDibeP-0-7c783edf8ae48d1484b75082e0be740a)
上面的代码发生了什么呢?我们一起来看一下:
□第5行至第8行是设置绘制与填充的样式,这些属性前面已经接触过了。不同的是,我们将路径颜色与填充颜色都设置为了棕色(brown)。
□第9行至第12行是路径绘制代码。
运行效果如图2-22所示。
画布全部变成棕色,为什么会这样呢?
在画布绘制中,路径是必须闭合的,但凡带填充的路径绘制,必须起始于beginPath,不然fill方法可能发生填充错误,现在我们加上beginPath方法:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/75_03.jpg?sign=1739665120-XeysisLP8p7dibjwbk3aAs7YpZzEDlBC-0-e1e353219c5b31e924d8e37a32d620dd)
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/76_01.jpg?sign=1739665120-rt8IppzaNpq4hUOXLxtZ3F5uUDXQxAgB-0-ec0b5079edf1249337738aa474558a54)
其中,第5行是新增代码。
运行效果现在正常了,如图2-23所示。
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/76_02.jpg?sign=1739665120-R4VIjqfrDgfFwYOmMxjj2tZVtr3CFbRJ-0-cc2dd42aac2ab8fdb59d911ba64b482e)
图2-22 填充绘制右挡板无效
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/76_03.jpg?sign=1739665120-9RhKzaF1ewg0nht5QdtQzwuRfCFpLVqP-0-9ea072018ca8b66165ffdfbd4f385945)
图2-23 正常的右挡板填充绘制
lineTo、moveTo组合起来虽然适用于任何绘制场景,但使用起来较为麻烦。其实除了线条绘制、路径填充绘制外,还可以使用rect、fillRect直接进行矩形绘制。
2.使用rect进行矩形绘制
调用渲染上下文对象的rect方法创建矩形,其调用语法为:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/76_04.jpg?sign=1739665120-OeR8pB8nC1AXD0UHzinjJqg4eTPfxazf-0-62866133d1d39972fa74e640a02d624b)
参数说明如下。
□x:矩形左上角距离画布左上角的x坐标。
□y:矩形左上角距离画布左上角的y坐标。
□width:矩形宽度,以像素计。
□height:矩形高度,以像素计。
现在直接使用rect绘制,示例代码如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/76_05.jpg?sign=1739665120-EM8ouWUHN5o6UbTNzmCqTst0lJlKRIYL-0-bcf10fb0cbc6bc388d5da31fe665880c)
第7行使用了rect方法,其绘制代码比路径填充的绘制代码简单多了,且绘制效果与图2-23是一样的。
有个问题,既然是使用rect直接绘制的,那么代码中对beginPath、fill方法的调用能不能去掉呢?
答案是不能。rect默认是不填充的,使用rect绘制的是路径,如果需要填充效果,必须同时有beginPath和fill的配合。
3.使用fillRect进行矩形绘制
fillRect方法相当于beginPath、rect和fill三个方法的综合,使用fillRect绘制更简洁,示例代码如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/77_01.jpg?sign=1739665120-fTKMlLa3cKWiuHfK5i0pKoh16Y2y52Lq-0-aa753cbb7ba80fbe2d81ac222b1a2b9b)
其中,第6行使用了fillRect方法,是不是比之前使用fill绘制还要简洁呢?
之前使用路径填充绘制的浅色不透明背景也可以使用fillRect改写,示例代码如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/77_02.jpg?sign=1739665120-tnUVak1WUlvbll6Aq6KK9OEsbbTIcBY2-0-a271d8c1cb85cfde45cb019cb19aef9f)
其中,第4行至第12行是注释掉的旧代码,第15行至第16行是新的背景绘制代码。
运行效果不变。
如何使用颜色渐变对象和图像填充材质绘制挡板
前面在绘制文本时使用过颜色渐变对象,绘制挡板也能使用这个对象吗?答案肯定是可以的。
1.使用线性颜色渐变对象绘制
我们只需要再创建一个LinearGradient对象,赋值给渲染上下文对象的fillStyle属性即可实现线性颜色渐变绘制,示例代码如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/77_03.jpg?sign=1739665120-RFvYu05cuzkw5Vkm7XjNJlMJd6vain64-0-3ddfe821e5211ba29623fe59fe5879b6)
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/78_01.jpg?sign=1739665120-UNyyGRcnsllDAm1WSS9N07POXGsr9vbf-0-456c26ff3f0f7a8ea5e3d143ac096332)
在上面的代码中:
□第5行至第9行为挡板创建了一个自上而下的渐变填充对象,仍然有红、白、黄三个颜色渐变点。
□第5行与第6行其实是一行代码,因为太长所以使用了回车,在格式化时折到下一行的代码会自动有一个小的缩进。
但是运行后,画布一片空白,没有任何效果,这是为什么呢?
打开谷歌浏览器的开发者工具,留意一下Console面板,上面有一个错误信息:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/78_02.jpg?sign=1739665120-nVyedhQytPMTXcPKr9PkxaKcXkqP4OyD-0-6a55f199ab33127ba6f16c1b2c18b69a)
因为grd在这里是作为常量使用的,这个常量名在前面已经声明过了,不能重复声明。我们可以换个名字,如果非要继续使用grd这个名称,可以将声明常量grd的关键字改为let(参见如下代码的第6行),第二次使用时重新赋值,这样就可以达到复用变量名的目的,示例代码如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/78_03.jpg?sign=1739665120-PwPpUWWdFj4IMw07ztk9ZLfsVTZVITDc-0-e4bd32ea56ec2331f20b7b81bd904cc8)
但这样做需要修改前面的代码,保证代码不出问题的最好方式就是不要改动它。在继续使用grd这个常量名称的前提下,还有一种解决方法:在编写新代码时使用区块作用域:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/78_04.jpg?sign=1739665120-IMIe8pXtFTXxs8GUt3xS7lVTJdXVi9hJ-0-a77572bf0e30bcce490a29641f3ed49c)
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/79_01.jpg?sign=1739665120-TrWfDJNXT7yJTDIYYUTis8xPpWMryVop-0-80db93b68cfa491159388539e4fdaf66)
在上面的代码中,第5行与第12行的花括号创建了一个起隔离作用的区块作用域,在这个作用域内的常量名称与区块外的名称重复也没有关系,同时我们也可以完成对fillStyle属性的设置。
这次运行效果就正常了,如图2-24所示。
右挡板出现自上而下的红、白、黄的渐变效果。
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/79_02.jpg?sign=1739665120-lgr3JrORo49JpIRatq5qEm6Jqys3b0pw-0-4c71b2fd178d412860e98bac6d7f4b95)
图2-24 使用fillRect绘制的右挡板
2.使用放射状颜色渐变对象绘制
颜色渐变对象除了线性渐变之外,还有一种放射状渐变,这种方式还没有使用过。
我们可以使用createRadialGradient方法创建放射状渐变对象,返回的对象类型仍然是CanvasGradient。createRadialGradient的调用语法如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/79_03.jpg?sign=1739665120-sAvZDrmLCx8tS3mC2nudJk1Tkd5Ba1XM-0-7d23ee189ea934684842847ca20baca1)
参数说明如下。
□x0:渐变开始圆的x坐标。
□y0:渐变开始圆的y坐标。
□r0:开始圆的半径。
□x1:渐变结束圆的x坐标。
□y1:渐变结束圆的y坐标。
□r1:结束圆的半径。
(x0,y0)是开始圆的圆心,(x1,y1)是结束圆的圆心。如何理解这种对象的表现呢?图2-25所示为放射状颜色渐变的效果,图中有两个圆,两个圆的圆心重合,小的是开始圆,大的是结束圆,渐变颜色点共3个,从内向外依次是红、绿、蓝,在小圆(开始圆)的内部是全红色,在大圆(结束圆)的外围是全蓝色。在线性渐变中,颜色点是从一个颜色方向到另一个颜色方向,在放射状渐变中,颜色点是从开始圆的圆心向结束圆的圆心呈辐射状渐变。
图2-25是两个同心圆正对屏幕的绘制效果,事实上如果我们换个角度,如图2-26所示,会有各种情况。但不管如何变化,两圆是否同心,是否相交或包含,都可以按照这种放射状颜色渐变在立体透视下的平面模型进行理解。
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/79_04.jpg?sign=1739665120-SALX6opXtGLNLfuzR3A1uWFOBYNFwrZt-0-14bf1db22598a0caddba54d50dc9c50d)
图2-25 放射状颜色绘制
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/79_05.jpg?sign=1739665120-vSjjF2eWTzq4f3Kx9tp6xWmAU9FlseBk-0-8ac23ecf28d05875859e0c2d3491746b)
图2-26 放射状颜色渐变投影模型
现在我们尝试绘制一个放射状渐变的右挡板,示例代码如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/80_01.jpg?sign=1739665120-DthQEVBmgPmF4STIrwkhS4gLb5mLy19o-0-76f010c3548b59ad3f868a03c7185576)
在上面的代码中:
□第6行、第7行,渐变开始圆与结束圆是同一个圆心,坐标均为(canvas.width-5,canvas.height/2),即右挡板的中心坐标。开始圆半径为0,结束圆半径为挡板高度的一半。
□放射状渐变对象与线性渐变对象一样,都是使用addColorStop方法添加渐变颜色点。第8行至第10行添加了红、白、黄三个颜色渐变点,从起始圆圆心开始向外,至结束圆渐变色依次为红、白、黄。
□开始圆内部是位置为0的颜色点的纯色,即红色,第6行将开始圆半径设置为0,内圆纯色就不存在了。
□结束圆之外是位置为1的颜色点的纯色,即黄色,第7行将结束圆半径设置为半个挡板高度,这样挡板上就没有外圆之外的纯色区域了。
放射状渐变对象与线性渐变对象一样,规定了在颜色点(ColorStop)位置0与1之内的渐变区域,之外的区域以边缘纯色填充。
运行效果如图2-27所示。
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/80_02.jpg?sign=1739665120-o2lrPzGqOTWd1PybuKnKnbPXrxRwpmRk-0-cb8a20ac03f38c9a02fdc57e1f9508e5)
图2-27 绘制放射状渐变的右挡板
3.使用材质填充对象绘制
我们已经看到了,使用放射状颜色填充挡板,效果不是很好。挡板一般是木质的,我们能不能使用木质图片填充呢?
答案是可以的,createPattern方法即可创建一个在指定方向有重复特征的木质填充对象,其调用语法如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/80_03.jpg?sign=1739665120-eKjdtaBPdlnrqCThUhKgoAl6iB6VpEVS-0-24c2cda2be7a6f9cf7ae76496ee5540f)
其参数说明如下。
□image:是一个材质内容对象,规定要使用的模式的图片、画布或视频元素。
□repeatPattern:重复策略。它有4个并列的合法选项:repeat是默认值,该模式在水平和垂直方向重复;repeat-x表示该模式只在水平方向重复;repeat-y表示该模式只在垂直方向重复;no-repeat表示该模式只使用材质一遍,不重复。
为了创建材质填充对象,必须有一个图像,为此先修改HTML代码,使用HTML标签<img>加载一个木质图片:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/81_01.jpg?sign=1739665120-Ufvigeh5OhPIDSXKUbwvnfg0KR7vByDV-0-82bce85c8a1ffd094af4c2e6116dcf3e)
在上面的代码中,有以下几点要注意。
□第6行是一个HTML注释。该注释以<!--xxx-->的形式出现,其中xxx是注释内容。
□第7行添加了一个<img>标签,从外网拉取一张木质图片。id设置为mood,稍后这个id在JS代码中会用到;src属性指定了图片的网络地址;style属性设置了内嵌的CSS样式,限制width为100px。在style属性中编写CSS样式,与在独立的<style>标签中编写样式稍有不同,在style属性中因为已经明确样式归属,所以没有必要再指明选择器,直接成对编写CSS样式即可。一个样式名+冒号+样式值+分号,这就是一组样式,在style属性中可以内嵌多组样式。
图片有了,接下来修改JS代码,创建材质填充对象(CanvasPattern)对象,示例代码如下:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/81_02.jpg?sign=1739665120-FKivy62QRIwDPTefnbAmBVeNpmhwnTIv-0-0b7a11352a037c040c805a321745ab90)
在上面的代码中,有以下几点需要注意。
□第8行的getElementById是一个BOM API,它是由浏览器实现的,用于通过id在页面上查找组件,此处它接收一个参数"mood",返回第4行使用<img>标签声明的图片对象(Image)。
□第9行createPattern方法接收两个参数:一个图片对象和一个重复策略no-repeat。这张图片足够大,所以第二个重复策略参数我们选择的是no-repeat。
□第10行将创建的材质填充对象赋值给fillStyle属性。JS是一门动态语言,同样一个属性既可以赋值为字符串,又可以赋值为对象,这在其他编程语言中是难以想象的。这个特征给我们的感觉是,JS像一个编程世界中的杂食动物。
运行效果如图2-28所示。
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/82_01.jpg?sign=1739665120-sCWG9xaweuuw18EsQWAYO5UOnm1W4qpA-0-4fe973b7d0cb14f64408df3bcd632a49)
图2-28 材质填充绘制的右挡板
挡板的木质材料已经显现了,从效果上看,确实比渐变色好多了。
思考与练习2-14:在本节最后一个示例中,因为网络原因,有时挡板看起来仍然是颜色渐变绘制,如图2-29所示。也就是说,图片可以加载,右挡板颜色却变成了渐变色,为什么呢?在浏览器中按住Ctrl键,同时按F5键强刷页面可以复现这个问题。
原因是图片加载是异步的,如果在创建CanvasPattern对象时图片还没有完成加载,此时创建的CanvasPattern是无效的。因为JS是动态语言,fillStyle属性不知道我们是想传一个错误的颜色字符串,还是想传一个企图正确的CanvasPattern对象,所以此时程序并不会报错,但这个Bug很难察觉。
Image对象有一个onload属性,可以设置一个图片加载完成之后的回调函数,如果在这个回调函数之内创建CanvasPattern,应该就没有问题了,请尝试实现。
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/82_02.jpg?sign=1739665120-KXLT8cezOSJKESbEweyFZI9dZ1MRooCp-0-8bdd0e7bca0adf8982b4f4d248841951)
图2-29 材质未起作用的情况
拓展:什么是区块作用域
看一下下面这段实战中出现过的代码:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/82_03.jpg?sign=1739665120-eoSL5uy4Kbs0jm1ITofKLEcwAG5fuiAZ-0-48c90d443ecdbddb1239508ef9d00f93)
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/83_01.jpg?sign=1739665120-M8i2WGIodRYMDVPt2S4QK5Lr9Bm3Wvzy-0-eaecaaf42140143addd3f90c1479eaab)
思考一下,第8行为什么可以重复声明grd常量呢?
在ES6标准出来之前,JS声明变量只有通过关键字var,在一对花括号内使用var声明的变量,在花括号外也能访问,因为内外都是一个作用域。例如下面这行代码:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/83_02.jpg?sign=1739665120-97zVFL4gqibdQMfkLLasziVAaWaNKXba-0-befcfb4d76b7f2392665bef6990a614d)
最后i的输出是10,而不是undefined。
ES6中引入了两个新关键字:let与const,并且规定花括号可以创建区块作用域。在区块作用域内,let、const声明的变量、常量,只有在该区块内(即花括号内)有效,在区块外不能访问;同时在区块外已经声明的标识符,在区块内仍然可以再次声明。
自ES6开始,可以统一使用let关键字替换var关键字。凡声明变量,一律推荐使用let;如果变量在声明之后不需要改变,就用const关键字声明为常量。放开的权限越小,潜在的软件风险越小。
只有一种情况不使用let,而使用var,那就是在旧的浏览器宿主环境中,ES6语法不受支持。不过在这种情况下,在开发阶段借助Babel自动转化代码,仍然可以使用ES6新语法。
思考与练习2-15:下面这段代码,会输出什么?
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/83_03.jpg?sign=1739665120-kP9BVkWBSE83isSXxKC3sZlVZ9mLBWKP-0-b3de522fb24546ac5d9ac7c348cbf545)
拓展:了解数字类型,警惕0.1+0.2不等于0.3
我们在开发中已多次使用数值类型,看下面这段在实战中出现的代码:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/83_04.jpg?sign=1739665120-zPRNAU8HqeIL2Ds5PnKFLxwReGw6VjT9-0-2d1b8071685429e007af6ee5fd62b3d3)
其中0、.5、1都是数字类型,.5等同于0.5。
为什么0.1+0.2不等于0.3呢?所有的编程语言,不仅JS,在使用浮点型数据时都要小心。看一个示例:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/83_05.jpg?sign=1739665120-BItyigAS3P9tHyABWroqvC0CZSNAdnsa-0-d4125eb6094266ba4aafc70bee339888)
变量z应该等于0.3,但是z==0.3返回false。这不是JS语言的Bug,JS中的数字类型是遵循IEEE 754浮点数标准实现的,这是标准本身存在的问题,所有遵照这个标准实现浮点数的语言都存在相似的问题。
有人可能会讲,为什么不将这个标准完善一下,让上面的奇怪问题消失呢?IEEE 754是20世纪80年代以来被广泛采纳和使用的浮点数运算标准,存在偏差本质上是因为电子计算机的底层数据是以二进制形式存储的,并不是标准设计本身存在问题。
思考与练习2-16(面试题):既然直接比较两个小数会存在误判风险,那么开发中应该如何比较呢?
拓展:如何批量声明变量、常量
一般情况下,我们会将变量、常量在文件顶部或函数顶部统一声明,这样方便代码的阅读与维护。
下面这5行都是在实战中出现的代码:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/84_01.jpg?sign=1739665120-XPn3gvf0AxyNfgZKOGEaQSzxvlAnmDwJ-0-e2ddfd8256ae1b1b62a29e01483e3f18)
使用批量声明的方式,可以将其改写为如下形式:
![](https://epubservercos.yuewen.com/298560/25930151901642206/epubprivate/OEBPS/Images/84_02.jpg?sign=1739665120-KOZwxUKiH18yYARBZARuZA992bg5dRbb-0-6cd13c41d7a2684246fdb6f6acf38a2d)
每行声明一个常量,行尾或行首以逗号分隔,行首注意一下缩进对齐。5个常量用一个const关键字声明,是5行内容,但其实只是一句代码。理想情况下,在函数内部或文件顶部声明变量和常量时,至多使用两次let或const关键字。
本课小结
本课源码参见:disc/第2章/2.2。
这节课主要实现了右挡板的绘制,学习了属性strokeStyle、lineWidth、lineCap及方法fill、fillRect的使用,练习了放射状颜色渐变对象和材质填充对象的使用,并在实践中了解了JS的基本数据类型及如何进行类型判断等语法,还有什么是区域作用域,以及如何批量声明变量、常量等。
为什么我们要尝试用这么多的方式实现一个简单的挡板呢?
这不仅是为了实现功能,学习使用lineTo、moveTo、rect、fillRect等绘制方法,根本上还是为了磨炼心性,修炼编程技能。当程序出错时,恰恰是提升编程能力的契机,此时应该沉下心来思考,而不是马上查看作者的源码。读者在学习过程中,切不可只完成某一节的实战,认为只要把这一课所讲的功能完成就可以了。功能不重要,练习才重要!
小球在游戏中是一个不断运动的物体,遇到画布四周墙壁或挡板会镜像反弹,它是运动的,不断变化的,结合第1章、第2课学习过的内容,想一想怎么实现它?