1
0
mirror of https://github.com/apachecn/eloquent-js-3e-zh.git synced 2025-05-23 20:02:20 +00:00
This commit is contained in:
wizardforcel 2018-05-12 21:49:02 +08:00
parent 9651dbed33
commit 0a64725152

299
17.md
View File

@ -8,7 +8,7 @@
但是在一些场景中使用DOM并不符合我们的设计初衷。比如我们很难使用普通的HTML元素画出任意两点之间的线段这类图形。
这里有两种解决办法。第一种方法基于DOM但使用可缩放矢量图形SVGScalable Vector Graphics代替HTML元素。我们可以将SVG看成一门专用于描述图形文档而非描述文字文档。你可以在HTML文档中嵌入SVG还可以在<img>标签中引用它。
这里有两种解决办法。第一种方法基于 DOM但使用可缩放矢量图形SVGScalable Vector Graphics代替 HTML。我们可以将 SVG 看成文档标记方言,专用于描述图形而非文字。你可以在HTML文档中嵌入SVG还可以在<img>标签中引用它。
我们将第二种方法称为画布canvas。画布是一个能够封装图片的DOM元素。它提供了在空白的html节点上绘制图形的编程接口。SVG与画布的最主要区别在于SVG保存了对于图像的基本信息的描述我们可以随时移动或修改图像。
@ -31,10 +31,10 @@
xmlns属性把一个元素以及他的子元素切换到一个不同的XML命名空间。这个由url定义的命名空间规定了我们当前使用的语言。在HTML中不存在的<circle><rect>标签但这些标签在SVG中是有意义的你可以通过这些标签的属性来绘制图像并指定样式与位置。
和HTML标签一样这些标签会创建对应的DOM元素。例如下面的代码可以把<circle>元素的颜色替换为青色。
和HTML标签一样这些标签会创建DOM元素,脚本可以和它们交互。例如,下面的代码可以把<circle>元素的颜色替换为青色。
```html
var circle = document.querySelector("circle");
let circle = document.querySelector("circle");
circle.setAttribute("fill", "cyan");
```
@ -44,19 +44,19 @@ circle.setAttribute("fill", "cyan");
新的画布是空的,意味着它是完全透明的,看起来就像文档中的空白区域一样。
<canvas>标签可以支持多种不同风格的绘图。要获取真正的绘图接口首先我们要创建一个能够提供绘图接口的方法的上下文context。目前有两种得到广泛支持的绘图接口用于绘制二维图形的“2d”与通过openGL接口绘制三维图形的“webgl”。
<canvas>标签允许多种不同风格的绘图。要获取真正的绘图接口首先我们要创建一个能够提供绘图接口的方法的上下文context。目前有两种得到广泛支持的绘图接口用于绘制二维图形的“2d”与通过openGL接口绘制三维图形的“webgl”。
本书只讨论二维图形而不讨论WebGL。但是如果你对三维图形感兴趣我强烈建议大家自行深入研究WebGL。它提供了非常简单的现代图形硬件接口同时你也可以使用JavaScript来高效地渲染非常复杂的场景。
我们可以在<canvas>元素上调用getContext方法创建context对象
您可以用`getContext`方法在`<canvas>` DOM 元素上创建一个上下文
```html
<p>Before canvas.</p>
<canvas width="120" height="60"></canvas>
<p>After canvas.</p>
<script>
var canvas = document.querySelector("canvas");
var context = canvas.getContext("2d");
let canvas = document.querySelector("canvas");
let context = canvas.getContext("2d");
context.fillStyle = "red";
context.fillRect(10, 10, 100, 50);
</script>
@ -66,22 +66,22 @@ circle.setAttribute("fill", "cyan");
与HTML或者SVG相同画布使用的坐标系统将00放置在左上角并且y轴向下增长。所以1010是相对于左上角向下并向右各偏移10像素的位置。
### 16.3 填充与描边
### 直线和平面
我们可以使用画布接口填充图形也就是赋予某个区域一个固定的填充颜色或填充模式。我们也可以描边也就是沿着图形的边沿画出线段。SVG也使用了相同的技术。
fillRect方法可以填充一个矩形。他的输入为矩形框左上角的第一个x和y坐标然后是它的宽和高。相似地strokeRect方法可以画出一个矩形的外框。
两个方法都不需要其他任何参数。填充的颜色以及轮廓的粗细等等都不能由方法的参数决定(假如你这样期望而是由context对象的属性决定。
两个方法都不需要其他任何参数。填充的颜色以及轮廓的粗细等等都不能由方法的参数决定(像你的合理预期一样),而是由上下文对象的属性决定。
通过设置fillStyle参数可以改变图形填充的方式。我们可以将其设置为描述颜色的字符串或其他可由css解析的颜色信息
设置fillStyle参数控制图形的填充方式。我们可以将其设置为描述颜色的字符串使用 CSS 所用的颜色表示法
strokeStyle属性的作用很相似但是它用于规定轮廓线的颜色。线条的宽度由lineWidth属性决定。lineWidth的值都为正值。
```html
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
let cx = document.querySelector("canvas").getContext("2d");
cx.strokeStyle = "blue";
cx.strokeRect(5, 5, 50, 50);
cx.lineWidth = 5;
@ -89,7 +89,7 @@ strokeStyle属性的作用很相似但是它用于规定轮廓线的颜色。
</script>
```
当没有设置width或者height参数时正如例一样画布元素的默认宽度为300像素默认高度为150像素。
当没有设置width或者height参数时正如例一样画布元素的默认宽度为300像素默认高度为150像素。
### 16.4 路径
@ -98,9 +98,9 @@ strokeStyle属性的作用很相似但是它用于规定轮廓线的颜色。
```html
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
for (var y = 10; y < 100; y += 10) {
for (let y = 10; y < 100; y += 10) {
cx.moveTo(10, y);
cx.lineTo(90, y);
}
@ -115,7 +115,7 @@ strokeStyle属性的作用很相似但是它用于规定轮廓线的颜色。
```html
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(50, 10);
cx.lineTo(10, 70);
@ -130,14 +130,14 @@ strokeStyle属性的作用很相似但是它用于规定轮廓线的颜色。
### 16.5 曲线
路径也可能会包含曲线。绘制曲线比直线更加复杂。
路径也可能会包含曲线。绘制曲线更加复杂。
quadraticCurveTo方法绘制到某一个点的曲线。为了确定一条线段的曲率需要设定一个控制点以及一个目标点。设想这个控制点会吸引这条线段以此来使线段成为曲线。线段不会穿过控制点。同时,线段的起始节点与终止节点的方向会与两个点到控制点的方向平行。见下例:
quadraticCurveTo方法绘制到某一个点的曲线。为了确定一条线段的曲率需要设定一个控制点以及一个目标点。设想这个控制点会吸引这条线段使其成为曲线。线段不会穿过控制点。但是,它起点与终点的方向会与两个点到控制点的方向平行。见下例:
```html
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(10, 90);
// control=(60,10) goal=(90,90)
@ -155,7 +155,7 @@ bezierCurve贝塞尔曲线方法可以绘制一种类似的曲线。不同
```html
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(10, 90);
// control1=(10,10) control2=(90,10) goal=(50,90)
@ -171,33 +171,15 @@ bezierCurve贝塞尔曲线方法可以绘制一种类似的曲线。不同
由于我们没有明确的方法,来找出我们希望绘制图形所对应的控制点,所以这种曲线还是很难操控。有时候你可以通过计算得到他们,而有时候你只能通过不断的尝试来找到合适的值。
圆弧作为圆的一小段更加容易理解。arcTo方法至少接受5个的参数。前四个参数与quadraticCurveTo方法有类似的作用。第一对参数提供了一些控制点第二对参数提供了线段的方向。第五个参数提供了圆弧的半径。该方法会形成一个无形的拐角并在拐点的周围形成一个由指定半径形成的圆弧。该拐角由一条沿着控制点的方向的线段和另一条从控制点向目标点的线段组成。arcTo方法接着会画出圆弧的部分以及一条从起点到圆弧部分的线段
`arc`方法是一种沿着圆的边缘绘制曲线的方法。 它需要弧的中心的一对坐标,半径,然后是起始和终止角度
```html
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(10, 10);
// control=(90,10) goal=(90,90) radius=20
cx.arcTo(90, 10, 90, 90, 20);
cx.moveTo(10, 10);
// control=(90,10) goal=(90,90) radius=80
cx.arcTo(90, 10, 90, 90, 80);
cx.stroke();
</script>
```
尽管to单词暗示着arcTo方法会画出从圆弧部分终点到目标点的线段但是该方法并不会这样做。你可以通过调用lineTo方法并赋予相同的目标点坐标来画出这部分线段。
如果想画出一个圆你可以调用4次arcTo方法每次旋转90度。但是arc方法提供了一个更加简单的方式。它接受一对弧度中心的坐标一个半径以及一个起始和终止的弧度。
我们可以使用最后两个参数画出部分圆。角度是通过弧度来测量的而不是度数。这意味着一个完整的圆拥有2π的弧度或者2*Math.PI大约为6.28的弧度。弧度从圆心右边的点开始并以顺时针的方向计数。你可以使用一个以0为起始并以一个比2π大的数值比如7作为终止值来画出一个完整的圆。
```html
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
// center=(50,50) radius=40 angle=0 to 7
cx.arc(50, 50, 40, 0, 7);
@ -207,14 +189,14 @@ bezierCurve贝塞尔曲线方法可以绘制一种类似的曲线。不同
</script>
```
上面这段代码绘制出的图形包含了一条从完整圆第一次调用arc的右侧到四分之一圆第二次调用arc的左侧的直线。arc与其他绘制路径的方法一样默认会自动连接到上一个路径上。如果你想避免这种行为需要使用moveTo创建一条新路径
上面这段代码绘制出的图形包含了一条从完整圆第一次调用arc的右侧到四分之一圆第二次调用arc的左侧的直线。arc与其他绘制路径的方法一样会自动连接到上一个路径上。你可以调用`moveTo`或者开启一个新的路径来避免这种情况
### 16.6 绘制饼状图
设想你刚刚从EconomiCorp获得了一份工作并且你的第一个任务是画出一个描述其用户满意度调查结果的饼状图。results变量包含了一个表示调查结果的对象的数组。
设想你刚刚从EconomiCorp获得了一份工作并且你的第一个任务是画出一个描述其用户满意度调查结果的饼状图。results绑定包含了一个表示调查结果的对象的数组。
```js
var results = [
const results = [
{name: "Satisfied", count: 1043, color: "lightblue"},
{name: "Neutral", count: 563, color: "lightgreen"},
{name: "Unsatisfied", count: 510, color: "pink"},
@ -227,14 +209,13 @@ var results = [
```html
<canvas width="200" height="200"></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
var total = results.reduce(function(sum, choice) {
return sum + choice.count;
}, 0);
let cx = document.querySelector("canvas").getContext("2d");
let total = results
.reduce((sum, {count}) => sum + count, 0);
// Start at the top
var currentAngle = -0.5 * Math.PI;
results.forEach(function(result) {
var sliceAngle = (result.count / total) * 2 * Math.PI;
let currentAngle = -0.5 * Math.PI;
for (let result of results) {
let sliceAngle = (result.count / total) * 2 * Math.PI;
cx.beginPath();
// center=100,100, radius=100
// from current angle, clockwise by slice's angle
@ -244,52 +225,53 @@ var results = [
cx.lineTo(100, 100);
cx.fillStyle = result.color;
cx.fill();
});
}
</script>
```
如果表格无法告诉我们其代表的含义,那么这个表格是毫无用处的。因此我们需要将文字画在画布上。
表格并没有告诉我们切片代表的含义,它毫无用处。因此我们需要将文字画在画布上。
### 16.7 文本
2D画布的context对象提供了fillText方法和strokeText方法。第二个方法可以用于绘制字母轮廓但通常情况下我们需要的是fillText方法。该方法使用当前的fillColor来填充特定文字。
2D画布的context对象提供了fillText方法和strokeText方法。第二个方法可以用于绘制字母轮廓但通常情况下我们需要的是fillText方法。该方法使用当前的fillColor来填充特定文字的轮廓
```html
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
let cx = document.querySelector("canvas").getContext("2d");
cx.font = "28px Georgia";
cx.fillStyle = "fuchsia";
cx.fillText("I can draw text, too!", 10, 50);
</script>
```
你可以通过font属性来设定文字的大小样式和字体。本例给出了一个字体的大小和字体族名称。可以添加italic或者bold来选择样式。
你可以通过font属性来设定文字的大小样式和字体。本例给出了一个字体的大小和字体族名称。可以添加italic或者bold来选择样式。
传递给fillText和strokeText的后两个参数用于指定绘制文字的位置。默认情况下这个位置指定了文字的字符基线baseline的起始位置我们可以将其假想为字符所站立的位置基线不考虑j或p字母中那些向下突出的部分。你可以设置textAlign属性end或center来改变起始点的横向位置也可以设置textBaseline属性top、middle或bottom来设置基线的纵向位置。
传递给fillText和strokeText的后两个参数用于指定绘制文字的位置。默认情况下这个位置指定了文字的字符基线baseline的起始位置我们可以将其假想为字符所站立的位置基线不考虑j或p字母中那些向下突出的部分。你可以设置textAlign属性end或center来改变起始点的横向位置也可以设置textBaseline属性top、middle或bottom来设置基线的纵向位置。
在本章末尾的练习中,我们会回头来看看饼状图,并解决给饼状图分片标注的问题。
在本章末尾的练习中,我们会回饼状图,并解决给饼状图分片标注的问题。
### 16.8 图像
计算机图形学领域经常将矢量图形和位图图形分开来讨论。本章一直在讨论第一种图形,即通过对图形的逻辑描述来绘图。而位图则相反,不需要设置实际图形,而是通过处理像素数据来绘制图像(光栅化的着色点)。
我们可以使用drawImage方法在画布上绘制像素值。此处的像素数值可以来自&lt;img&gt;元素,或者来自其他的画布,并且两者都不需要在实际的文档中可见。下例创建了一个独立的&lt;img&gt;元素并且加载了一张图像文件。但我们无法马上使用该图片进行绘制因为浏览器可能还没有完成图片的获取操作。为了处理这个问题我们在图像元素上注册一个“load”事件处理程序并且在图片加载完之后开始绘制。
我们可以使用drawImage方法在画布上绘制像素值。此处的像素数值可以来自&lt;img&gt;元素,或者来自其他的画布。下例创建了一个独立的&lt;img&gt;元素并且加载了一张图像文件。但我们无法马上使用该图片进行绘制因为浏览器可能还没有完成图片的获取操作。为了处理这个问题我们在图像元素上注册一个“load”事件处理程序并且在图片加载完之后开始绘制。
```html
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
var img = document.createElement("img");
let cx = document.querySelector("canvas").getContext("2d");
let img = document.createElement("img");
img.src = "img/hat.png";
img.addEventListener("load", function() {
for (var x = 10; x < 200; x += 30)
img.addEventListener("load", () => {
for (let x = 10; x < 200; x += 30) {
cx.drawImage(img, x, 10);
}
});
</script>
```
默认情况下drawImage会根据原图的尺寸绘制图像。你也可以增加两个参数来规定图片的宽度和高度。
默认情况下drawImage会根据原图的尺寸绘制图像。你也可以增加两个参数来设置不同的宽度和高度。
如果我们向drawImage函数传入9个参数我们可以用其绘制出一张图片的某一部分。第二个到第五个参数表示需要拷贝的源图片中的矩形区域xy坐标宽度和高度同时第六个到第九个参数给出了需要拷贝到的目标矩形的位置在画布上
@ -306,13 +288,13 @@ clearRect方法可以帮助我们在画布上绘制动画。该方法类似于fi
```html
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
var img = document.createElement("img");
let cx = document.querySelector("canvas").getContext("2d");
let img = document.createElement("img");
img.src = "img/player.png";
var spriteW = 24, spriteH = 30;
img.addEventListener("load", function() {
var cycle = 0;
setInterval(function() {
let spriteW = 24, spriteH = 30;
img.addEventListener("load", () => {
let cycle = 0;
setInterval(() => {
cx.clearRect(0, 0, spriteW, spriteH);
cx.drawImage(img,
// source rectangle
@ -325,18 +307,18 @@ clearRect方法可以帮助我们在画布上绘制动画。该方法类似于fi
</script>
```
cycle变量用于记录角色在动画图像中的位置。每显示一帧我们都要将cycle加1并通过取余数确保cycle的值在0~7这个范围内。我们随后使用该变量计算精灵当前形象在图片中的x坐标。
cycle绑定用于记录角色在动画图像中的位置。每显示一帧我们都要将cycle加1并通过取余数确保cycle的值在0~7这个范围内。我们随后使用该绑定计算精灵当前形象在图片中的x坐标。
### 16.9 变换
但是,如果我们希望角色可以向左走而不是向右走该怎么办?诚然,我们可以添加一组新的精灵,但我们也可以使用另一种方式在画布上绘图。
但是,如果我们希望角色可以向左走而不是向右走该怎么办?诚然,我们可以绘制另一组精灵,但我们也可以使用另一种方式在画布上绘图。
我们可以调用scale方法来缩放之后绘制的任何元素。该方法接受两个输入参数第一个参数是横向缩放比例第二个参数是纵向缩放比例。
```html
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
let cx = document.querySelector("canvas").getContext("2d");
cx.scale(3, .5);
cx.beginPath();
cx.arc(50, 50, 40, 0, 7);
@ -351,7 +333,7 @@ cycle变量用于记录角色在动画图像中的位置。每显示一帧
除了scale方法还有一些其他方法可以影响画布里坐标系统的方法。你可以使用rotate方法旋转绘制完的图形也可以使用translate方法移动图形。毕竟有趣但也容易引起误解的是这些变换以栈的方式工作也就是说每个变换都会作用于前一个变换的结果之上。
如果我们沿水平方向将画布平移两次每次移动10像素那么所有的图形都会在右方20像素的位置重新绘制。如果我们先把坐标系的原点移动到5050的位置然后旋转20度0.1π弧度此次的旋转会围绕点5050进行。
如果我们沿水平方向将画布平移两次每次移动10像素那么所有的图形都会在右方20像素的位置重新绘制。如果我们先把坐标系的原点移动到5050的位置然后旋转20度大约 0.1π 弧度此次的旋转会围绕点5050进行。
![](../Images/00511.jpeg)
@ -371,18 +353,18 @@ function flipHorizontally(context, around) {
![](../Images/00513.jpeg)
上图显示了通过中线进行镜像翻转前后的坐标系。如果我们在x坐标为正值的位置绘制一个三角形默认情况下它会出现在图中三角形1的位置。调用filpHorizontally首先做一个向右的平移得到三角形2。然后将其翻转到三角形3的位置。这不是它的根据给定的中线翻转之后应该在的最终位置。第二次调用translate方法解决了这个问题。它“去除”了最初的平移的效果并且使三角形4变成我们希望的效果。
上图显示了通过中线进行镜像翻转前后的坐标系。对三角形编号来说明每一步。如果我们在x坐标为正值的位置绘制一个三角形默认情况下它会出现在图中三角形1的位置。调用filpHorizontally首先做一个向右的平移得到三角形2。然后将其翻转到三角形3的位置。这不是它的根据给定的中线翻转之后应该在的最终位置。第二次调用translate方法解决了这个问题。它“去除”了最初的平移的效果并且使三角形4变成我们希望的效果。
我们可以沿着特征的纵向中心线翻转整个坐标系这样就可以画出位置为1000处的镜像特征。
```html
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
var img = document.createElement("img");
let cx = document.querySelector("canvas").getContext("2d");
let img = document.createElement("img");
img.src = "img/player.png";
var spriteW = 24, spriteH = 30;
img.addEventListener("load", function() {
let spriteW = 24, spriteH = 30;
img.addEventListener("load", () => {
flipHorizontally(cx, 100 + spriteW / 2);
cx.drawImage(img, 0, 0, spriteW, spriteH,
100, 0, spriteW, spriteH);
@ -392,11 +374,11 @@ function flipHorizontally(context, around) {
### 16.10 存储与清除图像的变换状态
图像变换的效果会保留下来。我们绘制出一次镜像特征后,绘制其他特征时都会产生镜像效果,这会成为一个问题
图像变换的效果会保留下来。我们绘制出一次镜像特征后,绘制其他特征时都会产生镜像效果,这可能并不方便
对于需要临时转换坐标系统的函数来说,我们经常需要保存当前的信息,画一些图,变换图像然后重新加载之前的图像。首先,我们需要将当前函数调用的所有图形变换信息保存起来。接着,函数完成其工作,并添加更多的变换。最后我们恢复之前保存的变换状态。
2D画布的save与restore方法负责执行这类变换状态管理任务。这两个方法维护变换状态堆栈。save方法将当前状态压到堆栈中restore方法将堆栈顶部的状态弹出并将该状态作为当前context对象的状态。
2D画布上下文的save与restore方法执行这个变换管理。这两个方法维护变换状态堆栈。save方法将当前状态压到堆栈中restore方法将堆栈顶部的状态弹出并将该状态作为当前context对象的状态。
下面示例中的branch函数首先修改变换状态然后调用其他函数本例中就是该函数自身继续在特定变换状态中进行绘图。
@ -405,7 +387,7 @@ function flipHorizontally(context, around) {
```html
<canvas width="600" height="300"></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
let cx = document.querySelector("canvas").getContext("2d");
function branch(length, angle, scale) {
cx.fillRect(0, 0, 1, length);
if (length < 8) return;
@ -428,72 +410,68 @@ function flipHorizontally(context, around) {
我们现在已经了解了足够多的画布绘图知识我们已经可以使用基于画布的显示系统来改造前面几章中开发的游戏了。新的界面不会再是一个个色块而使用drawImage来绘制游戏中元素对应的图片。
我们会定义一种叫作CanvasDisplay的对象类型支持与第15章中的DOMDisplay相同的接口也就是drawFrame方法与clear方法。
我们定义了一种对象类型叫做CanvasDisplay支持与第15章中的DOMDisplay相同的接口也就是setState方法与clear方法。
这个对象需要比DOMDisplay多保存一些信息。该对象不仅需要使用DOM元素的滚动位置还需要追踪自己的视口viewport。视口会告诉我们目前处于哪个关卡,也会记录时间以及当前使用哪一帧的数据。最后该对象会保存一个filpPlayer属性确保即便玩家站立不动时它面朝的方向也会与上次移动所面向的方向一致。
这个对象需要比DOMDisplay多保存一些信息。该对象不仅需要使用DOM元素的滚动位置还需要追踪自己的视口viewport。视口会告诉我们目前处于哪个关卡。最后该对象会保存一个filpPlayer属性确保即便玩家站立不动时它面朝的方向也会与上次移动所面向的方向一致。
```js
function CanvasDisplay(parent, level) {
this.canvas = document.createElement("canvas");
this.canvas.width = Math.min(600, level.width * scale);
this.canvas.height = Math.min(450, level.height * scale);
parent.appendChild(this.canvas);
this.cx = this.canvas.getContext("2d");
class CanvasDisplay {
constructor(parent, level) {
this.canvas = document.createElement("canvas");
this.canvas.width = Math.min(600, level.width * scale);
this.canvas.height = Math.min(450, level.height * scale);
parent.appendChild(this.canvas);
this.cx = this.canvas.getContext("2d");
this.level = level;
this.animationTime = 0;
this.flipPlayer = false;
this.flipPlayer = false;
this.viewport = {
left: 0,
top: 0,
width: this.canvas.width / scale,
height: this.canvas.height / scale
};
this.viewport = {
left: 0,
top: 0,
width: this.canvas.width / scale,
height: this.canvas.height / scale
};
}
this.drawFrame(0);
clear() {
this.canvas.remove();
}
}
CanvasDisplay.prototype.clear = function() {
this.canvas.parentNode.removeChild(this.canvas);
};
```
我们之所以要将步长传入第15章中定义的drawFrame函数是因为我们添加了一个animationTime计数器。新的drawFrame函数使用计数器来记录时间这样该函数可以根据当前时间切换动画帧
`setState`方法首先计算一个新的视口,然后在适当的位置绘制游戏场景。
```js
CanvasDisplay.prototype.drawFrame = function(step) {
this.animationTime += step;
this.updateViewport();
this.clearDisplay();
this.drawBackground();
this.drawActors();
CanvasDisplay.prototype.setState = function(state) {
this.updateViewport(state);
this.clearDisplay(state.status);
this.drawBackground(state.level);
this.drawActors(state.actors);
};
```
除了跟踪时间这个方法会为当前玩家更新视口的位置。为整个画布填充背景颜色并且画出背景以及上面的角色。注意这个方法与第15章中提到的方法不同。第15章中我们只画一次背景并且通过滚动DOM元素来移动它。
因为画布上的图形在我们绘制之后都只是像素,所以我们没有办法移动他们(或者删除他们)。唯一的更新画布的方式是清除并重新绘制场景。
`DOMDisplay`相反,这种显示风格确实必须在每次更新时重新绘制背景。 因为画布上的形状只是像素,所以在我们绘制它们之后,没有什么好方法来移动它们(或将它们移除)。 更新画布显示的唯一方法,是清除它并重新绘制场景。 我们也可能发生了滚动,这要求背景处于不同的位置。
updateViewport方法与DOMDisplay的scrollPlayerintoView方法相似。它检查玩家是否过于接近屏幕的边缘并且当这种情况发生时移动视口。
```js
CanvasDisplay.prototype.updateViewport = function() {
var view = this.viewport, margin = view.width / 3;
var player = this.level.player;
var center = player.pos.plus(player.size.times(0.5));
CanvasDisplay.prototype.updateViewport = function(state) {
let view = this.viewport, margin = view.width / 3;
let player = state.player;
let center = player.pos.plus(player.size.times(0.5));
if (center.x < view.left + margin)
if (center.x < view.left + margin) {
view.left = Math.max(center.x - margin, 0);
else if (center.x > view.left + view.width - margin)
} else if (center.x > view.left + view.width - margin) {
view.left = Math.min(center.x + margin - view.width,
this.level.width - view.width);
if (center.y < view.top + margin)
state.level.width - view.width);
}
if (center.y < view.top + margin) {
view.top = Math.max(center.y - margin, 0);
else if (center.y > view.top + view.height - margin)
} else if (center.y > view.top + view.height - margin) {
view.top = Math.min(center.y + margin - view.height,
this.level.height - view.height);
state.level.height - view.height);
}
};
```
@ -502,38 +480,38 @@ CanvasDisplay.prototype.updateViewport = function() {
在清空图像时,我们依据游戏是获胜(明亮的颜色)还是失败(灰暗的颜色)来使用不同的颜色。
```js
CanvasDisplay.prototype.clearDisplay = function() {
if (this.level.status == "won")
CanvasDisplay.prototype.clearDisplay = function(status) {
if (status == "won") {
this.cx.fillStyle = "rgb(68, 191, 255)";
else if (this.level.status == "lost")
} else if (status == "lost") {
this.cx.fillStyle = "rgb(44, 136, 214)";
else
} else {
this.cx.fillStyle = "rgb(52, 166, 251)";
}
this.cx.fillRect(0, 0,
this.canvas.width, this.canvas.height);
};
```
要画出一个背景,我们使用在前面章节中的obstacleAt中相似的手段,遍历在当前视口中可见的所有瓦片。
要画出一个背景,我们使用来自上一节的touches方法中的相同技巧,遍历在当前视口中可见的所有瓦片。
```js
var otherSprites = document.createElement("img");
let otherSprites = document.createElement("img");
otherSprites.src = "img/sprites.png";
CanvasDisplay.prototype.drawBackground = function() {
var view = this.viewport;
var xStart = Math.floor(view.left);
var xEnd = Math.ceil(view.left + view.width);
var yStart = Math.floor(view.top);
var yEnd = Math.ceil(view.top + view.height);
for (var y = yStart; y < yEnd; y++) {
for (var x = xStart; x < xEnd; x++) {
var tile = this.level.grid[y][x];
if (tile == null) continue;
var screenX = (x - view.left) * scale;
var screenY = (y - view.top) * scale;
var tileX = tile == "lava" ? scale : 0;
CanvasDisplay.prototype.drawBackground = function(level) {
let {left, top, width, height} = this.viewport;
let xStart = Math.floor(left);
let xEnd = Math.ceil(left + width);
let yStart = Math.floor(top);
let yEnd = Math.ceil(top + height);
for (let y = yStart; y < yEnd; y++) {
for (let x = xStart; x < xEnd; x++) {
let tile = level.rows[y][x];
if (tile == "empty") continue;
let screenX = (x - left) * scale;
let screenY = (y - top) * scale;
let tileX = tile == "lava" ? scale : 0;
this.cx.drawImage(otherSprites,
tileX, 0, scale, scale,
screenX, screenY, scale, scale);
@ -550,36 +528,37 @@ CanvasDisplay.prototype.drawBackground = function() {
我们不需要等待精灵图片加载完成。调用drawImage时使用一幅并未加载完毕的图片不会有任何效果。因为图片仍然在加载当中我们可能无法正确地画出游戏的前几帧。但是这不是一个严重的问题因为我们持续更新荧幕正确的场景会在加载完毕之后立即出现。
前面展示过的走路的特征将会被用来代替玩家。绘制它的代码需要根据玩家的当前动作选择正确的动作和方向。前8个子画面包含一个走路的动画。当玩家沿着地板移动时我们根据animationTime属性把他围起来。这是用秒为单位来衡量的并且我们希望每秒更换12次画面所以时间是先被放大12倍。当玩家站立不动时我们画出第九张子画面。当竖直方向的速度不为0从而被判断为跳跃时我们使用第10张也是最右边的子画面。
前面展示过的走路的特征将会被用来代替玩家。绘制它的代码需要根据玩家的当前动作选择正确的动作和方向。前8个子画面包含一个走路的动画。当玩家沿着地板移动时我们根据当前时间把他围起来。我们希望每 60 毫秒切换一次帧,所以时间先除以 60。当玩家站立不动时我们画出第九张子画面。当竖直方向的速度不为0从而被判断为跳跃时我们使用第10张也是最右边的子画面。
因为子画面宽度为24像素而不是16像素会稍微比玩家的对象宽这时为了腾出脚和手的空间该方法需要根据某个给定的值playerXOverlap调整x坐标的值以及宽度值。
```js
var playerSprites = document.createElement("img");
let playerSprites = document.createElement("img");
playerSprites.src = "img/player.png";
var playerXOverlap = 4;
const playerXOverlap = 4;
CanvasDisplay.prototype.drawPlayer = function(x, y, width,
height) {
var sprite = 8, player = this.level.player;
CanvasDisplay.prototype.drawPlayer = function(player, x, y,
width, height){
width += playerXOverlap * 2;
x -= playerXOverlap;
if (player.speed.x != 0)
if (player.speed.x != 0) {
this.flipPlayer = player.speed.x < 0;
}
if (player.speed.y != 0)
sprite = 9;
else if (player.speed.x != 0)
sprite = Math.floor(this.animationTime * 12) % 8;
let tile = 8;
if (player.speed.y != 0) {
tile = 9;
} else if (player.speed.x != 0) {
tile = Math.floor(Date.now() / 60) % 8;
}
this.cx.save();
if (this.flipPlayer)
if (this.flipPlayer) {
flipHorizontally(this.cx, x + width / 2);
this.cx.drawImage(playerSprites,
sprite * width, 0, width, height,
x, y, width, height);
}
let tileX = tile * width;
this.cx.drawImage(playerSprites, tileX, 0, width, height,
x, y, width, height);
this.cx.restore();
};
```