diff --git a/17.md b/17.md index c966c17..75b6d89 100644 --- a/17.md +++ b/17.md @@ -8,13 +8,13 @@ 但是,在一些场景中,使用DOM并不符合我们的设计初衷。比如我们很难使用普通的HTML元素画出任意两点之间的线段这类图形。 -这里有两种解决办法。第一种方法基于 DOM,但使用可缩放矢量图形(SVG,Scalable Vector Graphics)代替 HTML。我们可以将 SVG 看成文档标记方言,专用于描述图形而非文字。你可以在HTML文档中嵌入SVG,还可以在<img>标签中引用它。 +这里有两种解决办法。第一种方法基于 DOM,但使用可缩放矢量图形(SVG,Scalable Vector Graphics)代替 HTML。我们可以将 SVG 看成文档标记方言,专用于描述图形而非文字。你可以在HTML文档中嵌入SVG,还可以在标签中引用它。 我们将第二种方法称为画布(canvas)。画布是一个能够封装图片的DOM元素。它提供了在空白的html节点上绘制图形的编程接口。SVG与画布的最主要区别在于SVG保存了对于图像的基本信息的描述,我们可以随时移动或修改图像。 另外,画布在绘制图像的同时会把图像转换成像素(在栅格中的具有颜色的点)并且不会保存这些像素表示的内容。唯一的移动图形的方法就是清空画布(或者围绕着图形的部分画布)并在新的位置重画图形。 -### 16.1 SVG +## 16.1 SVG 本书不会深入研究SVG的细节,但是我会简单地解释其工作原理。在本章的结尾,我会再次来讨论,对于某个具体的应用来说,我们应该如何权衡利弊选择一种绘图方式。 @@ -29,22 +29,22 @@ ``` -xmlns属性把一个元素(以及他的子元素)切换到一个不同的XML命名空间。这个由url定义的命名空间,规定了我们当前使用的语言。在HTML中不存在的<circle>与<rect>标签,但这些标签在SVG中是有意义的,你可以通过这些标签的属性来绘制图像并指定样式与位置。 +xmlns属性把一个元素(以及他的子元素)切换到一个不同的XML命名空间。这个由url定义的命名空间,规定了我们当前使用的语言。在HTML中不存在的标签,但这些标签在SVG中是有意义的,你可以通过这些标签的属性来绘制图像并指定样式与位置。 -和HTML标签一样,这些标签会创建DOM元素,脚本可以和它们交互。例如,下面的代码可以把<circle>元素的颜色替换为青色。 +和HTML标签一样,这些标签会创建DOM元素,脚本可以和它们交互。例如,下面的代码可以把元素的颜色替换为青色。 ```html let circle = document.querySelector("circle"); circle.setAttribute("fill", "cyan"); ``` -### 16.2 canvas元素 +## 16.2 canvas元素 -我们可以在<canvas>元素中绘制画布图形。你可以通过设置width与height属性来确定画布尺寸(单位为像素)。 +我们可以在元素中绘制画布图形。你可以通过设置width与height属性来确定画布尺寸(单位为像素)。 新的画布是空的,意味着它是完全透明的,看起来就像文档中的空白区域一样。 -<canvas>标签允许多种不同风格的绘图。要获取真正的绘图接口,首先我们要创建一个能够提供绘图接口的方法的上下文(context)。目前有两种得到广泛支持的绘图接口:用于绘制二维图形的“2d”与通过openGL接口绘制三维图形的“webgl”。 +标签允许多种不同风格的绘图。要获取真正的绘图接口,首先我们要创建一个能够提供绘图接口的方法的上下文(context)。目前有两种得到广泛支持的绘图接口:用于绘制二维图形的“2d”与通过openGL接口绘制三维图形的“webgl”。 本书只讨论二维图形,而不讨论WebGL。但是如果你对三维图形感兴趣,我强烈建议大家自行深入研究WebGL。它提供了非常简单的现代图形硬件接口,同时你也可以使用JavaScript来高效地渲染非常复杂的场景。 @@ -66,7 +66,7 @@ circle.setAttribute("fill", "cyan"); 与HTML(或者SVG)相同,画布使用的坐标系统将(0,0)放置在左上角,并且y轴向下增长。所以(10,10)是相对于左上角向下并向右各偏移10像素的位置。 -### 直线和平面 +## 直线和平面 我们可以使用画布接口填充图形,也就是赋予某个区域一个固定的填充颜色或填充模式。我们也可以描边,也就是沿着图形的边沿画出线段。SVG也使用了相同的技术。 @@ -91,7 +91,7 @@ strokeStyle属性的作用很相似,但是它用于规定轮廓线的颜色。 当没有设置width或者height参数时,正如示例一样,画布元素的默认宽度为300像素,默认高度为150像素。 -### 16.4 路径 +## 16.4 路径 路径是线段的序列。2D canvas接口使用一种奇特的方式来描述这样的路径。Path的绘制都是间接完成的。我们无法将路径保存为可以后续修改并传递的值。如果你想修改路径,必须要调用多个方法来描述他的形状。 @@ -128,7 +128,7 @@ strokeStyle属性的作用很相似,但是它用于规定轮廓线的颜色。 你也可以使用closePath方法显示地通过增加一条回到路径起始节点的线段来封闭一个路径。这条线段在勾勒路径的时候将被显示地画出。 -### 16.5 曲线 +## 16.5 曲线 路径也可能会包含曲线。绘制曲线更加复杂。 @@ -191,7 +191,7 @@ bezierCurve(贝塞尔曲线)方法可以绘制一种类似的曲线。不同 上面这段代码绘制出的图形包含了一条从完整圆(第一次调用arc)的右侧到四分之一圆(第二次调用arc)的左侧的直线。arc与其他绘制路径的方法一样,会自动连接到上一个路径上。你可以调用`moveTo`或者开启一个新的路径来避免这种情况。 -### 16.6 绘制饼状图 +## 16.6 绘制饼状图 设想你刚刚从EconomiCorp获得了一份工作,并且你的第一个任务是画出一个描述其用户满意度调查结果的饼状图。results绑定包含了一个表示调查结果的对象的数组。 @@ -231,7 +231,7 @@ const results = [ 但表格并没有告诉我们切片代表的含义,它毫无用处。因此我们需要将文字画在画布上。 -### 16.7 文本 +## 16.7 文本 2D画布的context对象提供了fillText方法和strokeText方法。第二个方法可以用于绘制字母轮廓,但通常情况下我们需要的是fillText方法。该方法使用当前的fillColor来填充特定文字的轮廓。 @@ -251,11 +251,11 @@ const results = [ 在本章末尾的练习中,我们会回顾饼状图,并解决给饼状图分片标注的问题。 -### 16.8 图像 +## 16.8 图像 计算机图形学领域经常将矢量图形和位图图形分开来讨论。本章一直在讨论第一种图形,即通过对图形的逻辑描述来绘图。而位图则相反,不需要设置实际图形,而是通过处理像素数据来绘制图像(光栅化的着色点)。 -我们可以使用drawImage方法在画布上绘制像素值。此处的像素数值可以来自<img>元素,或者来自其他的画布。下例创建了一个独立的<img>元素,并且加载了一张图像文件。但我们无法马上使用该图片进行绘制,因为浏览器可能还没有完成图片的获取操作。为了处理这个问题,我们在图像元素上注册一个“load”事件处理程序并且在图片加载完之后开始绘制。 +我们可以使用drawImage方法在画布上绘制像素值。此处的像素数值可以来自元素,或者来自其他的画布。下例创建了一个独立的元素,并且加载了一张图像文件。但我们无法马上使用该图片进行绘制,因为浏览器可能还没有完成图片的获取操作。为了处理这个问题,我们在图像元素上注册一个“load”事件处理程序并且在图片加载完之后开始绘制。 ```html @@ -309,7 +309,7 @@ clearRect方法可以帮助我们在画布上绘制动画。该方法类似于fi cycle绑定用于记录角色在动画图像中的位置。每显示一帧,我们都要将cycle加1,并通过取余数确保cycle的值在0~7这个范围内。我们随后使用该绑定计算精灵当前形象在图片中的x坐标。 -### 16.9 变换 +## 16.9 变换 但是,如果我们希望角色可以向左走而不是向右走该怎么办?诚然,我们可以绘制另一组精灵,但我们也可以使用另一种方式在画布上绘图。 @@ -372,7 +372,7 @@ function flipHorizontally(context, around) { ``` -### 16.10 存储与清除图像的变换状态 +## 16.10 存储与清除图像的变换状态 图像变换的效果会保留下来。我们绘制出一次镜像特征后,绘制其他特征时都会产生镜像效果,这可能并不方便。 @@ -406,11 +406,11 @@ function flipHorizontally(context, around) { 如果没有调用save与restore方法,第二次递归调用branch将会在第一次调用的位置结束。它不会与当前的分支相连接,而是更加靠近中心偏右第一次调用所画出的分支。结果图像会很有趣,但是它肯定不是一棵树。 -### 16.11 回到游戏 +## 16.11 回到游戏 我们现在已经了解了足够多的画布绘图知识,我们已经可以使用基于画布的显示系统来改造前面几章中开发的游戏了。新的界面不会再是一个个色块,而使用drawImage来绘制游戏中元素对应的图片。 -我们定义了一种对象类型,叫做CanvasDisplay,支持与第15章中的DOMDisplay相同的接口,也就是setState方法与clear方法。 +我们定义了一种对象类型,叫做CanvasDisplay,支持与第14章中的DOMDisplay相同的接口,也就是setState方法与clear方法。 这个对象需要比DOMDisplay多保存一些信息。该对象不仅需要使用DOM元素的滚动位置,还需要追踪自己的视口(viewport)。视口会告诉我们目前处于哪个关卡。最后,该对象会保存一个filpPlayer属性,确保即便玩家站立不动时,它面朝的方向也会与上次移动所面向的方向一致。 @@ -566,21 +566,21 @@ CanvasDisplay.prototype.drawPlayer = function(player, x, y, drawPlayer方法被drawActors方法调用,该方法负责画出游戏中的所有角色。 ```js -CanvasDisplay.prototype.drawActors = function() { - this.level.actors.forEach(function(actor) { - var width = actor.size.x * scale; - var height = actor.size.y * scale; - var x = (actor.pos.x - this.viewport.left) * scale; - var y = (actor.pos.y - this.viewport.top) * scale; +CanvasDisplay.prototype.drawActors = function(actors) { + for (let actor of actors) { + let width = actor.size.x * scale; + let height = actor.size.y * scale; + let x = (actor.pos.x - this.viewport.left) * scale; + let y = (actor.pos.y - this.viewport.top) * scale; if (actor.type == "player") { - this.drawPlayer(x, y, width, height); + this.drawPlayer(actor, x, y, width, height); } else { - var tileX = (actor.type == "coin" ? 2 : 1) * scale; + let tileX = (actor.type == "coin" ? 2 : 1) * scale; this.cx.drawImage(otherSprites, tileX, 0, width, height, x, y, width, height); } - }, this); + } }; ``` @@ -588,7 +588,7 @@ CanvasDisplay.prototype.drawActors = function() { 当计算角色的位置时,我们需要减掉视口的位置,因为(0,0)在我们的画布坐标系中代表着视口层面的左上角,而不是该关卡的左上角。我们也可以使用translate方法,这样可以作用于所有元素。 -这就形成了新的展示系统。最后的游戏界面如下所示。 +这个文档将新的显示屏插入`runGame`中: ```html @@ -598,25 +598,25 @@ CanvasDisplay.prototype.drawActors = function() { ``` -### 16.12 选择图像接口 +## 16.12 选择图像接口 -无论何时,只要在浏览器中绘图时,你都可以选择纯粹的HTML、SVG或画布。没有唯一的最适合的且在所有动画中都是最好的方法。每个选择都有它的利与弊。 +所以当你需要在浏览器中绘图时,你都可以选择纯粹的HTML、SVG或画布。没有唯一的最适合的且在所有动画中都是最好的方法。每个选择都有它的利与弊。 -单纯的HTML的优点是简单。它也可以很好地与文字集成使用。SVG与画布都可以允许你绘制文字,但是它们不会只通过一行代码来帮助你放置text或者包装它,在一个基于HTML的图像中,包含一篇文字是非常方便的。 +单纯的HTML的优点是简单。它也可以很好地与文字集成使用。SVG与画布都可以允许你绘制文字,但是它们不会只通过一行代码来帮助你放置text或者包装它,在一个基于HTML的图像中,包含文本块更加简单。 -SVG可以被用来制造可以任意缩放而仍然清晰的图像。它比单纯的HTML更加难以使用,但是它更加强大。 +SVG可以被用来制造可以任意缩放而仍然清晰的图像。与HTML相反,它实际上是为绘图而设计的,因此更适合于此目的。 -SVG与HTML都会构建一个新的数据结构(DOM)来代表图片对象。这使得在绘制元素之后对其进行修改更为可能。如果你需要重复的修改在一张大图片中的一小部分,来对用户的动作进行响应或者作为动画的一部分时,在画布里做这件事情将会极其的昂贵。DOM也可以允许我们在图片上的每一个元素(甚至在SVG画出的图形上)注册鼠标事件的处理器。在画布里则实现不了。 +SVG与HTML都会构建一个新的数据结构(DOM),它表示你的图片。这使得在绘制元素之后对其进行修改更为可能。如果你需要重复的修改在一张大图片中的一小部分,来对用户的动作进行响应或者作为动画的一部分时,在画布里做这件事情将会极其的昂贵。DOM也可以允许我们在图片上的每一个元素(甚至在SVG画出的图形上)注册鼠标事件的处理器。在画布里则实现不了。 但是画布的基于像素的方法在需要绘制大量的微小元素时会有优势。它不会构建新的数据结构而是仅仅重复的在同一个像素上绘制,这使得画布在每个图形上拥有更低的消耗。 有一些效果,像在逐像素的渲染一个场景(比如,使用光线追踪)或者使用javaScript对一张图片进行后加工(虚化或者扭曲),只能通过基于像素的技术来进行真实的处理。在某些情况下,你可能想要将这些技术整合起来使用。比如,你可能用SVG或者画布画出一个图形,但是通过将一个HTML元素放在图片的顶端来展示像素信息。 -对于一些要求低的程序来说,选择哪个接口并没有什么太大的区别。因为不需要绘制文字,处理鼠标交互或者与大量的外母元素交互。我们在本章游戏中构建的第二个显示屏可以通过使用三种图像技术中的任意一种来实现。 +对于一些要求低的程序来说,选择哪个接口并没有什么太大的区别。因为不需要绘制文字,处理鼠标交互或者与大量的外母元素交互。我们在本章为游戏构建的显示屏,可以通过使用三种图像技术中的任意一种来实现。 -### 16.13 本章小结 +## 16.13 本章小结 -在本章中,我们讨论了在浏览器中绘制图形的技术,重点关注了<canvas>元素。 +在本章中,我们讨论了在浏览器中绘制图形的技术,重点关注了元素。 一个canvas节点代表了我们的程序可以绘制在文档中的一片区域。这个绘图动作是通过一个由getContext方法创建的绘图上下文对象完成的。 @@ -630,81 +630,83 @@ SVG与HTML都会构建一个新的数据结构(DOM)来代表图片对象。 图形变换允许你向多个方向绘制图片。2D绘制上下文拥有一个当前的可以通过translate、scale与rotate进行变换。这些会影响所有的后续的绘制操作。一个变换的状态可以通过save方法来保存,通过restore方法来恢复。 -当在一个画布上绘制动画时,clearRect方法可以用来在重绘之前清除画布的某一部分。 +在一个画布上展示动画时,clearRect方法可以用来在重绘之前清除画布的某一部分。 -### 16.14 习题 +## 16.14 习题 -#### 16.14.1 形状 +### 16.14.1 形状 编写一个程序,在画布上画出下面的图形。 -1.一个不规则四边形(一个在一边比较长的矩形) +1. 一个不规则四边形(一个在一边比较长的矩形) -2.一个红色的钻石(一个矩形旋转45度角) +2. 一个红色的钻石(一个矩形旋转45度角) -3.zigzagging——一个2字形折线 +3. zigzagging——一个2字形折线 -4.一个由100条直线线段构成的螺旋 +4. 一个由100条直线线段构成的螺旋 -5.一个黄色的星星 +5. 一个黄色的星星 ![](../Images/00527.jpeg) -当绘制最后两个图形时,你可以参考在第13章中的Math.cos和Math.sin的定义,这两个函数定义了如何在一个圆上的坐标。 +当绘制最后两个图形时,你可以参考在第14章中的Math.cos和Math.sin的定义,这两个函数定义了如何在一个圆上的坐标。 建议你为每一个图形创建一个方法,传入坐标信息,以及其他的一些参数,比如大小或者点的数量。另一种方法,可以在你的代码中硬编码,会使得你的代码变得难以阅读和修改。 ```html ``` -#### 16.14.2 饼状图 +### 16.14.2 饼状图 -在本章的前部分,我们看到一个绘制饼状图的样例程序。修改这个程序,使得每个部分的名字可以被显示在相应的切片旁边。试着找到一个合适的方法来自动放置这些文字,同时也可以适用于其他数据。你可以假设这些部分不会小于5%(这意味着,不会有大量的非常微小的相邻的部分)。你可能还会需要Math.sin和Math.cos方法,像上面的练习中描述的一样。 +在本章的前部分,我们看到一个绘制饼状图的样例程序。修改这个程序,使得每个部分的名字可以被显示在相应的切片旁边。试着找到一个合适的方法来自动放置这些文字,同时也可以适用于其他数据。你可以假设分类大到足以为标签留出空间。 + +你可能还会需要Math.sin和Math.cos方法,像第 14 章描述的一样。 ```html ``` -#### 16.14.3 弹力球 +### 16.14.3 弹力球 -使用在第13章和第15章出现的requestAnimationFrame方法画出一个装有弹力球的盒子。这个球以匀速运动并且当撞到盒子的边缘的时候反弹。 +使用在第 14 章和第 16 章出现的requestAnimationFrame方法画出一个装有弹力球的盒子。这个球以匀速运动并且当撞到盒子的边缘的时候反弹。 ```html ``` -#### 16.14.4 预处理镜像 +### 16.14.4 预处理镜像 -当进行图形变换时,绘制位图图像会很慢。对于矢量图形,这种效果并不明显,因为只有一小部分点(比如,圆的中心)需要被变换,之后绘制图形可以跟正常情况一样。对一个位图图像,每个像素的位置必须被变换,并且即便浏览器可以在未来以一种更加高效科学的方法绘制,目前绘制位图会产生一种在时间上可度量的复杂度提升。 +当进行图形变换时,绘制位图图像会很慢。每个像素的位置和大小都必须进行变换,尽管将来浏览器可能会更加聪明,但这会导致绘制位图所需的时间显着增加。 在一个像我们这样的只绘制一个简单的子画面图像变换的游戏中,这个不是问题。但是如果我们需要绘制成百上千的角色或者爆炸产生的旋转粒子时,这将会成为一个问题。