HTML5-和-JavaScript-项目教程-全-

HTML5 和 JavaScript 项目教程(全)

原文:HTML5 and JavaScript Projects

协议:CC BY-NC-SA 4.0

一、构建 HTML5 徽标:使用缩放和语义标签在画布上绘图

在本章中,您将学习以下内容:

  • 在画布上绘制路径

  • 将文本放置在画布上

  • 坐标变换

  • 画布上绘制的文本字体和其他元素中的文本字体

  • 语义标签

  • 范围输入元素

介绍

这一章的项目是一个官方 HTML5 标志的演示,并附有文字。徽标的盾和字母绘制在画布元素上,附带的文本演示了语义标签的使用。观众可以使用滑动输入设备来改变标志的大小。对于这本书来说,这是一个合适的开始,这是一个利用 HTML5、JavaScript 和其他技术的项目集,因为它的主题很好地回顾了 HTML5 中基本的事件驱动编程和其他重要特性。我开发这个项目的方式,建立在其他人的工作之上,是我们大多数人的典型工作方式。特别是,环境提供了使用坐标变换的动机。

本书的方法是在具体示例的上下文中解释 HTML5、级联样式表(CSS)和 JavaScript 章节。这些项目代表了各种各样的应用,希望你能在每一个项目中发现一些你可以学习和适应的东西。

注意

如果你需要使用 HTML5 和 JavaScript 编程的入门,可以查阅我的书,html 5 的基本指南或者 Apress 等出版的其他书籍。网上也有相当多的材料,例如 W3Schools。

图 1-1 显示了 Chrome 浏览器上徽标项目的打开屏幕。重要的是要认识到浏览器可能是不同的。当我第一次写这个例子时,看看这是如何在 Firefox 中出现的。

img/272384_2_En_1_Fig1_HTML.jpg

图 1-1

HTML5 徽标的打开屏幕

请注意滑块功能、附带的文本(包含看似超链接的内容)以及黄线下方页脚中的文本。页脚还包括一个超链接。正如我稍后将解释的,页脚和任何其他语义元素的功能和格式完全由我决定,但提供给徽标所有者的参考,万维网联盟将被视为一种适当的使用。

观众可以使用滑块来改变标志的大小。图 1-2 显示了滑块调整后的应用,显示徽标的宽度和高度缩小到原来的三分之一。

img/272384_2_En_1_Fig2_HTML.jpg

图 1-2

徽标缩小

HTML5 在所有浏览器中的实现都是完整的,或者说非常接近。然而,我想给你看一些过去的东西来说明术语优雅的退化。图 1-3 显示了老款火狐浏览器的打开屏幕。范围输入被视为文本。注意,初始值显示为 100。

img/272384_2_En_1_Fig3_HTML.jpg

图 1-3

使用 Firefox 的应用程序

作为每一章的实践,我现在解释应用程序的关键需求,或多或少地独立于 HTML5 实现的事实,然后描述 HTML5、JavaScript 和其他实现中需要使用的技术的特性。“构建”部分包括一个表格,其中有每行代码的注释和构建类似应用程序的指南。“测试”部分提供了上传和测试的详细信息。这一部分在某些项目中比其他项目更重要。最后,有一个“总结”部分,回顾了涵盖的编程概念,并预览了本书的下一步内容。

项目历史和关键要求

这个项目的关键需求有些人为,不容易从 HTML 中分离出来。例如,我想画标志,而不是从网上复制图像。我的设计目标总是包括想练习编程和为我的学生准备例子。徽标盾牌部分的形状似乎适合在画布上绘制,HTML 字母可以使用绘制文本功能来完成。此外,绘制图像而不是使用图像文件也有实际优势。需要管理、存储和下载单独的文件。图 1-4 所示图像为 90KB。保存程序代码的文件只有 4KB。绘制徽标或其他图形意味着可以使用代码动态更改比例和其他属性。

img/272384_2_En_1_Fig4_HTML.jpg

图 1-4

徽标的图像

我在网上找了一个丹尼尔·戴维斯做的盾牌的例子。这太棒了,因为这意味着我不必测量一份徽标图像来获得坐标。这就引出了他是如何确定坐标的问题。我不知道答案,尽管我们愉快地交换了电子邮件。一种可能是下载图像并使用图像处理程序(如 Adobe Photoshop 或 Corel Paint Shop Pro)的网格功能。另一种可能是使用(老式的)透明绘图纸。

然而,在丹尼尔·戴维斯的工作基础上有一个问题。他的申请不包括 HTML 字母。解决这个问题的方法是在屏幕上定位字母,然后向下移动,也就是说,使用丹尼尔的例子中提供的坐标来定位盾牌的图形。“向下移动屏幕”的技术术语是执行坐标转换。因此,执行坐标转换的能力成为这个项目的一个关键需求。

我选择写一些关于这个标志的东西,特别是,以超链接的形式给出信用和参考。我决定在文档底部的一行下面引用徽标的官方来源作为简短的文本。提到丹尼尔·戴维斯是正文写作的一部分。我们就字体的选择交换了意见,我将在下一节详细讨论。

为了给观众一些与标志有关的东西,我决定呈现一种改变尺寸的方法。一个好的方法是使用一个滑块,指定最小和最大值以及步长。因此,这个应用程序的关键需求包括绘制特定字体的形状和字母、坐标转换、用主要部分和脚注部分格式化文档,以及包含超链接。

HTML5、CSS 和 JavaScript 特性

我假设读者对 HTML 和 HTML5 文档有一些经验。HTML5 中最重要的新特性之一是用于绘图的 canvas 元素。我简要描述了适当颜色和填充文本的填充路径的绘制。接下来,我描述坐标转换,在这个项目中使用的标志本身的两个部分和缩放,改变整个标志的大小。最后,我描述了范围输入元素。这就产生了滑块。

在画布上绘制路径

Canvas 是 HTML5 中引入的一种元素。所有画布元素都有一个被称为 2D 上下文的属性(也称为属性)。上下文有一些绘图方法,您将会看到它们的使用。通常,在加载文档后,会将一个变量设置为该属性:

ctx = document.getElementById('canvas').getContext('2d');

理解 canvas 是一个好名字很重要:代码将颜色应用于画布的像素,就像 paint 一样。稍后编写的代码可以在画布上放置不同的颜色。旧颜色看不出来。即使我们的代码使矩形、形状和字母出现,这些不同的实体并不保留它们作为要被重新定位的对象的身份。

用累加的结果连续绘制六条填充路径,产生盾牌,如图 1-5 所示。检查代码时可以参考这张图。请记住,在坐标中,第一个数字是距画布左边缘的距离,第二个数字是距画布上边缘的距离。

img/272384_2_En_1_Fig5_HTML.jpg

图 1-5

绘制徽标的路径顺序

顺便说一句,我选择给你看有累积结果的序列。如果我展示所画的东西,你将看不到构成五子棋左侧的白色部分。您可以看到它,因为它是橙色顶部的两条白色填充路径。

所有的绘制都是使用保存 canvas 元素的 2D 上下文属性的ctx变量的方法和属性来完成的。任何后续填充操作的颜色是通过将颜色分配给 canvas 上下文的fillStyle属性来设置的。

ctx.fillStyle = "#E34C26";

这种以十六进制格式给出的特殊颜色(其中前两个十六进制(16 进制)数字代表红色,后两个十六进制数字代表绿色,最后两个代表蓝色)是由 W3C 网站提供的,与其他颜色一起,作为盾牌背景的特殊橙色。这可能违反直觉,但在这个系统中,白色是由值#FFFFFF指定的。把这想象成所有颜色一起构成白色。缺少的颜色是黑色,由#000000指定。徽标中 5 右侧使用的珍珠灰色具有值#EBEBEB。这是一个高值,接近白色。你不需要记住这些值中的任何一个,但是知道黑色和白色是有用的,一个纯红色是#FF0000,一个纯绿色是#00FF00,一个纯蓝色是#0000FF。您可以使用 Adobe Photoshop、Corel Paint Shop Pro 等绘图程序中的吸管/拾色器工具或在线工具 http://pixlr.com/ 找出图像中的颜色值。您可以对官方图像使用官方名称(如果有)。

所有绘图都是使用二维坐标系完成的。形状是使用路径方法生成的。这些假设了当前位置,您可以将它想象为画笔或油漆刷在画布上的位置。关键方法是移动到一个位置,并建立一条从当前位置到指定位置的线路。下面一组语句从左下角开始绘制五边形橙色。closePath方法通过画一条回到起点的线来封闭路径。

ctx.fillStyle = "#E34C26";
ctx.beginPath();
ctx.moveTo(39, 250);
ctx.lineTo(17, 0);
ctx.lineTo(262, 0);
ctx.lineTo(239, 250);
ctx.lineTo(139, 278);
ctx.closePath();
ctx.fill();

如果你没有在画布上画过任何东西,下面是生成五边形所需的整个 HTML 脚本。标签<body>中的onLoad属性导致在加载文档时调用init函数。init函数设置ctx变量,设置fillStyle属性,然后绘制路径。

<!DOCTYPE html>
<html>
<head>
<title>HTML5 Logo</title>
<meta charset="UTF-8">
<script>
function init() {
 ctx = document.getElementById('canvas').getContext('2d');
 ctx.fillStyle = "#E34C26";
 ctx.beginPath();
 ctx.moveTo(39, 250);
 ctx.lineTo(17, 0);
 ctx.lineTo(262, 0);
 ctx.lineTo(239, 250);
 ctx.lineTo(139, 278);
 ctx.closePath();
 ctx.fill();
}
</script>
</head>
<body onLoad="init();">
<canvas id="canvas" width="600" height="400">
Your browser does not support the canvas element.
</canvas>
</body>
</html>

如果你以前没有在画布上画过画,请练习和尝试,但我会继续下去。其他形状以类似的方式产生。顺便说一句,如果你在盾牌中间看到一条线,这是一种视错觉。

在画布上和文档正文中放置文本

使用上下文的方法和属性在画布上绘制文本。可使用fillText方法填充文本,或使用strokeText方法绘制轮廓。颜色是当前fillStyle属性或strokeStyle属性的颜色。上下文的另一个属性是font。该属性可以包含文本的大小和一种或多种字体。包含多种字体的目的是在运行浏览器的计算机上第一种字体不可用时为浏览器提供选项。对于这个项目,我使用

var fontfamily = "65px 'Gill Sans Ultra Bold', sans-serif";

并且在init功能中

ctx.font = fontfamily;

这将指导浏览器使用 Gill Sans Ultra Bold 字体(如果可用),如果不可用,则使用计算机上的任何默认 sans serif 字体。

我本可以把这些都放在一个语句中,但是我选择把它变成一个变量。你可以决定我选择的字体是否足够接近 W3C 的官方标志。

注意

对于这个例子,至少还有另外两种方法。一种可能是而不是使用文本,而是将字母绘制成填充路径。另一种方法是定位并获取一种字体,将其放在保存 HTML5 文档的服务器上,并使用@font-face直接引用它。

设置好字体和颜色后,绘制文本的方法需要一个字符串和一个位置:x 和 y 坐标。这个项目中绘制字母的语句是

ctx.fillText("HTML", 31,60);

在 HTML 文档的其余部分,也就是在 canvas 元素之外格式化文本,需要同样注意字体。在这个项目中,我选择使用 HTML5 中新的语义元素,并遵循将格式放入样式元素中的做法。我的 HTML 脚本主体包含两个文章元素和一个页脚元素。一篇文章包含带有注释的输入元素,另一篇文章包含其余的解释。footer 元素包含对 W3C 的引用。格式化和使用这些取决于开发人员/程序员。这包括确保页脚是文档中的最后一项。如果我将页脚放在一篇或两篇文章之前,它将不再显示在底部,即文档的底部。该项目的样式指令如下:

footer {display:block; border-top: 1px solid orange; margin: 10px;
 font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; font-weight: bold;}
article {display:block; font-family: Georgia, "Times New Roman", Times, serif; margin: 5px;}

每个样式都将这些元素的所有实例设置为显示为块。这将在前后放置一个换行符。页脚顶部有一个边框,在文本上方产生一行。两种样式都指定了四种字体的列表。因此,浏览器首先查看 Trebuchet MS 是否可用,然后检查 Arial,然后检查 Helvetica,如果仍然不成功,则使用系统默认的 sans serif 字体作为页脚元素。类似地,浏览器检查 Georgia,然后 Times New roman,然后 Times,如果不成功,则使用标准 serif 字体。这可能有点过了,但这是安全的操作方式。页脚文本以粗体显示,每篇文章周围有 5 个像素的边距。

包括字体在内的格式很重要。HTML5 提供了许多用于格式化以及将格式从结构和内容中分离出来的特性。您确实需要将画布上的文本与其他元素中的文本区别对待。

坐标变换

我已经给出了我使用坐标变换的动机,特别是继续使用一组坐标。回顾一下,坐标系是在画布上指定位置的方式。位置被指定为距原点的距离。对于二维画布,两个坐标是必要的:第一个坐标控制水平方向,通常称为 x,第二个坐标控制垂直方向,称为 y。一个讨厌的事实是,当绘制到屏幕上时,y 轴被翻转,因此垂直方向是从画布的顶部开始测量的。水平线是从左侧开始测量的。这意味着点(100,200)比点(100,100)更靠屏幕下方。

在 logo 项目中,我编写了代码来显示字母 HTML,然后移动原点来绘制 logo 的其余部分。打个比方,我知道我的房子在镇中心的位置,所以我可以给镇中心指路,然后给我的房子指路。我在徽标中绘制字母并“向下移动屏幕”的情况需要 translate 转换。平移是在垂直方向上完成的。翻译的数量存储在名为offsety的变量 I 中:

var offsety = 80;
...
ctx.fillText("HTML", 31, 60);
ctx.translate(0, offsety);

因为我决定为查看者提供一种改变徽标大小的方法,所以我使用了缩放转换。继续方向的类比,这相当于改变了单位。你可以用英里(或公里)给出一些方向,而用码、英尺或米,或者可能是块给出另一些方向。可以为每个维度单独进行缩放。在这个应用程序中,有一个名为factorvalue的变量,它由输入改变时调用的函数设置。该声明

ctx.scale(factorvalue, factorvalue);

更改水平和垂直方向的单位。

HTML5 提供了一种保存坐标系当前状态和恢复已保存内容的方法。如果您需要代码返回到以前的状态,这一点很重要。保存和恢复是使用所谓的完成的:后进先出。恢复坐标状态被称为弹出栈,保存坐标状态是某个东西推到栈上。我的 logo 项目并没有充分利用这一点,但是如果你正在做更复杂的应用程序,这是需要记住去研究的。在 logo 项目中,我的代码保存了文档第一次加载时的原始状态。然后,在绘制徽标之前,它会恢复保存的内容,然后再次保存,以便下次使用。对于这种情况来说,这是大材小用,但这是一个很好的实践,以防我将来添加一些东西。自己做实验!绘制徽标的函数dologo开头的代码如下所示:

function dologo() {
var offsety = 80 ;
ctx.restore();
ctx.save();
ctx.clearRect(0,0,600,400);
ctx.scale(factorvalue,factorvalue);
ctx.fillText("HTML", 31,60);
ctx.translate(0,offsety);

// 5 sided orange background
ctx.fillStyle = "#E34C26";
ctx.beginPath();
ctx.moveTo(39, 250);
ctx.lineTo(17, 0);
ctx.lineTo(262, 0);
ctx.lineTo(239, 250);
ctx.lineTo(139, 278);
ctx.closePath();
ctx.fill();

// right hand, lighter orange part of the background
ctx.fillStyle = "#F06529";
ctx.beginPath();
ctx.moveTo(139, 257);
ctx.lineTo(220, 234);
ctx.lineTo(239, 20);
ctx.lineTo(139, 20);
ctx.closePath();
ctx.fill();
...

请注意,画布上先前绘制的任何内容都将被清除(擦除)。

使用范围输入元素

我称之为滑块的输入设备是新的 HTML5 输入type range,它被放置在 HTML 文档的主体中。我的放在一个 article 元素里面。此类型的属性和其他输入元素提供了指定初始值、最小值和最大值、最小增量调整以及查看者更改滑块时要采取的操作的方法。代码是

<input id="slide" type="range" min="0" max="100" value="100"
 onChange="changescale(this.value)" step="10"/>

minmax、【初始】、step可以任意设置。因为我使用了百分比,并且因为我不想让标志变得比初始值大或者处理负值,所以我使用了 0 和 100。

在滑块的正确实现中,观察者看不到初始值或最大值或最小值。我的代码使用输入的百分比。表达式this.value被解释为这个元素的 value 属性,强调将切换到英语!术语this在 JavaScript 和其他几种编程语言中有特殊的含义。changescale函数获取由分配给onChange属性的参数指定的值,并使用它来设置名为factorvalue的全局变量(在任何函数外部声明的变量,因此它持久存在并可用于任何函数)。

function changescale(val) {
        factorvalue = val / 100;
        dologo();
}

浏览器将提供表单验证,这是 HTML5 规范的一部分,也就是说,浏览器将检查输入元素中的属性所指定的条件是否得到遵守。就减少程序员需要做的工作和性能提升而言,这可能是一个显著的生产力提升,因为由浏览器完成检查可能会更快。在 HTML5 logo 项目中,滑块的一个优点是查看者不需要关心值,而只需要移动设备。无法输入非法值。图 1-6 显示了在输入字段输入值 200 的结果。

img/272384_2_En_1_Fig6_HTML.jpg

图 1-6

在 Firefox 中显示比例为 200

画布具有固定的宽度和高度,并且在画布外部绘制,这是在缩放以接受数字并将它们拉伸到原始值的两倍时所做的,被忽略。

构建应用程序并使之成为您自己的应用程序

这个项目做一件事,它画标志。为此定义了一个函数dologo。非正式地说,这个计划的大纲是

  1. init :初始化

  2. dologo :以 HTML 字母开始画 logo,然后画盾牌

  3. changescale :改变刻度

表 1-1 显示了功能之间的关系。第一次加载文档时调用dologo功能,之后每当缩放比例改变时调用。

表 1-1

html 5 Logo 项目中的函数

|

功能

|

调用/调用者

|

打电话

|
| --- | --- | --- |
| init | 由<body>标签中的onLoad属性的动作调用 | dologo |
| dologo | 由initchangescale调用 |   |
| changescale | 由<input type="range"...>标签中的onChange属性的动作调用 | dologo |

dologo函数的编码将前面描述的技术结合在一起。特别是,代码恢复了原来的坐标系并清空了画布。

该应用程序中的全局变量是

var ctx;
var factorvalue = 1;
var fontfamily = "65px 'Gill Sans Ultra Bold', sans-serif";

如前所述,可以不使用fontfamily而是直接在代码中使用字符串。方便让ctxfactorvalue全球化。

表 1-2 显示了基本应用程序的代码,每一行都有注释。

表 1-2

html 5 Logo 项目的完整代码

|

代码行

|

描述

|
| --- | --- |
| <!DOCTYPE html> | 页眉 |
| <html> | 开始html标签 |
| <head> | 开始head标签 |
| <title>``HTML5 Logo | 完成title元素 |
| <meta charset="UTF-8"> | 什么时候 |
| <style> | 开始style标签 |
| footer {display:block; border-top: 1px solid orange; margin: 10px; font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; font-weight: bold;} | 页脚的样式,包括上边框和字体系列 |
| article {display:block; font-family: Georgia, "Times New Roman", Times, serif; margin: 5px;} | 两篇文章的风格 |
| </style> | 关闭样式元素 |
| <!-- Start of script element --> | HTML comment |
| <script language="JavaScript"> | 开始script标签。注意:在 JavaScript 中大小写无关紧要 |
| var ctx ; | 变量来保存上下文;在所有图纸中使用 |
| var factorvalue = 1; | 设置缩放的初始值 |
| var fontfamily = "65px 'Gill Sans Ultra Bold', sans-serif"; | 为画布上绘制的文本设置字体 |
| function init() { | 开始init功能 |
| ctx = document.getElementById('canvas').getContext('2d'); | 设置ctx |
| ctx.font = fontfamily; | 为画布上绘制的文本设置字体 |
| ctx.save(); | 保存原始坐标状态 |
| dologo(); | 调用函数来绘制徽标 |
| } | 关闭功能 |
| /* dologo function definition . This is the main function. It uses factorvalue to change the scale.``*/ | JavaScript 中的多行注释 |
| function dologo() { | 开始dologo功能 |
| var offsety = 80 ; | 指定量来调整坐标以绘制标志的盾形部分 |
| ctx.restore(); | 恢复坐标的原始状态 |
| ctx.save(); | 保存它(压入栈),以便可以再次恢复 |
| ctx.clearRect(0,0,600,400); | 擦除整个画布 |
| ctx.scale(factorvalue,factorvalue); | 使用滑块设置的值进行水平和垂直缩放 |
| ctx.fillText("HTML", 31,60); | 画出字母:HTML |
| ctx.translate(0,offsety); | 向下移动屏幕(画布) |
| // 5 sided orange background | 单行注释 |
| ctx.fillStyle = "#E34C26"; | 设置为官方亮橙色 |
| ctx.beginPath(); | 开始一条路 |
| ctx.moveTo(39, 250); | 移动到左下角的指定位置 |
| ctx.lineTo(17, 0); | 向上画一条线,再向左画一点 |
| ctx.lineTo(262, 0); | 向右画直线 |
| ctx.lineTo(239, 250); | 向下并稍微向左画线 |
| ctx.lineTo(139, 278); | 画一条线到盾的中间低点 |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 用指定的颜色填充 |
| // right hand, lighter orange part of the background |   |
| ctx.fillStyle = "#F06529"; | 将颜色设置为官方的深橙色 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(139, 257); | 移动到中间点,靠近顶部 |
| ctx.lineTo(220, 234); | 向右稍微向上画一条线 |
| ctx.lineTo(239, 20); | 向右上方画线 |
| ctx.lineTo(139, 20); | 向左画线(指向中间) |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 用指定的颜色填充 |
| //light gray, left hand side part of the five |   |
| ctx.fillStyle = "#EBEBEB"; | 将颜色设置为灰色 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(139, 113); | 水平移动到中间,垂直移动到中间 |
| ctx.lineTo(98, 113); | 向左画线 |
| ctx.lineTo(96, 82); | 向上画线,再向左一点 |
| ctx.lineTo(139, 82); | 向右画线 |
| ctx.lineTo(139, 51); | 排队 |
| ctx.lineTo(62, 51); | 向左画线 |
| ctx.lineTo(70, 144); | 向左下方画一条线 |
| ctx.lineTo(139, 144); | 向右画线 |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 用指定的颜色填充 |
| ctx.beginPath(); | 开始新的道路 |
| ctx.moveTo(139, 193); | 移动到中间点 |
| ctx.lineTo(105, 184); | 向左上方画线 |
| ctx.lineTo(103, 159); | 向左上方稍稍画一条线 |
| ctx.lineTo(72, 159); | 向左多画一条线 |
| ctx.lineTo(76, 207); | 向右下方稍稍画一条线 |
| ctx.lineTo(139, 225); | 向左下方画一条线 |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 用指定的颜色填充形状 |
| // white, right hand side of the 5 |   |
| ctx.fillStyle = "#FFFFFF"; | 将颜色设置为白色 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(139, 113); | 从中品脱开始 |
| ctx.lineTo(139, 144); | 向下画线 |
| ctx.lineTo(177, 144); | 向右画线 |
| ctx.lineTo(173, 184); | 稍微向左下方画线 |
| ctx.lineTo(139, 193); | 向左下方多画一条线 |
| ctx.lineTo(139, 225); | 向下画线 |
| ctx.lineTo(202, 207); | 向右上方画线 |
| ctx.lineTo(210, 113); | 稍微向右上方画线 |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 填写白色 |
| ctx.beginPath(); | 开始新的道路 |
| ctx.moveTo(139, 51); | 移动到中间点 |
| ctx.lineTo(139, 82); | 下移 |
| ctx.lineTo(213, 82); | 向右移动 |
| ctx.lineTo(216, 51); | 稍微向右上方移动 |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 填写白色 |
| } | 关闭dologo功能 |
| // The changescale function, response to user input. |   |
| function changescale(val) { | 用参数打开function changevalue |
| factorvalue = val / 100; | 将factorvalue设置为除以 100 的输入 |
| dologo(); | 调用函数来绘制徽标 |
| } | 关闭changevalue功能 |
| </script> | 关闭脚本元素 |
| </head> | 关闭头部元件 |
|   | 文档的其余部分是主体元素 |
| <body onLoad="init();"> | 属性设置为调用init的 Body 标签 |
| <canvas id="canvas" width="600" height="400"> | 画布标签设置代码中要使用的尺寸和 ID |
| Your browser does not support the canvas element. | 不支持画布时出现的消息 |
| </canvas> | 关闭canvas标签 |
| <article> | 文章标签 |
| Scale percentage:  <input id="slide" type="range" min="0" max="100" value="100" onChange="changescale(this.value)" step="10"/> | 滑块(范围)输入与设置 |
| Note: slider treated as text field in some browsers. | 注释要注意,滑块可能是文本字段;它仍然是可用的 |
| </article> | 文章结束标签 |
| <article>Built on <a href="``http://daniemon.com/tech/html5/html5logo/ | 带有一些文本的文章标签,包括超链接 |
| <footer>HTML5 Logo by <a href="http://``www.w3.org | 页脚标签和页脚内容,包括abbr元素 |
| </footer> | 页脚关闭标记 |
| </body> | 身体关闭 |
| </html> | HTML 关闭 |

您可以将此应用程序的全部或部分内容用于您自己的工作中。您可能想省略关于字体的注释。

测试和上传应用程序

这是一个简单的应用程序来测试和上传(和测试),因为它是一个单一的文件。当修改范围输入元素和调用changescale功能时,变量factorvalue发生变化,可用于适应不同的屏幕。这个程序在不同的设备上运行良好。所谓的响应式设计的挑战将在第十章中讨论。

摘要

在这一章中,你学习了如何制作一个特定的绘图,并学习了制作其他类似应用程序的步骤。本章中使用的功能包括

  • 小路

  • 画布上的文本和正文中语义元素中的文本

  • 范围输入元素及其关联的更改事件

  • 坐标变换,即平移和缩放

  • 字体集的规范

  • 语义元素的样式,包括在页脚前加一条线的顶部边框

下一章将描述如何构建一个实用的应用程序来制作照片和形状的组合或剪贴画。它将在画布上绘图和创建 HTML 元素的技术与计算中的标准技术——对象——结合起来。它还使用坐标变换。

二、家庭剪贴画(FamilyCollage):在画布上操作程序员定义的对象

在本章中,您将学习以下内容:

  • 为在画布上绘图创建和操作面向对象的编程

  • 处理鼠标事件,包括双击

  • 将画布存储到图像

  • 使用trycatch捕捉错误

  • 涉及代码位置的浏览器差异

  • 使用代数和几何构造形状并确定光标何时位于特定对象上

  • 控制用于光标的图标

介绍

这一章的项目是一个在画布上操作对象来产生图片的工具。我称之为实用工具,因为一个人做编程,收集照片和设计,然后可以将程序提供给朋友、家人、同事和其他人来制作构图/剪贴画。结果可以是任何东西,从抽象设计到照片剪贴画。我的示例中的对象包括一个矩形、两个椭圆形、一颗心、两张家庭照片和一个视频(单杠上的 Annika)。您或者您的最终用户/客户/客户/玩家有可能复制任何对象并删除项目。最终用户使用鼠标拖放来定位对象。当判断图片完整时,可以创建一个可以下载到文件中的图像。

图 2-1 显示了我的程序的开始屏幕。请注意,您从要排列的七个对象开始。

img/272384_2_En_2_Fig1_HTML.jpg

图 2-1

家庭照片的打开屏幕

图 2-2 显示了作为最终用户的我制作的最终产品,并在新窗口中保存为图像。我复制了两张照片和一段视频,添加了两个心形,去掉了矩形和椭圆形。

img/272384_2_En_2_Fig2_HTML.jpg

图 2-2

最终产品样本:重新排列的对象

我决定加入一颗心,不仅仅是出于感情上的原因,还因为这需要我用到代数和几何。不要害怕数学。它非常有用。可以说,我发明了一颗规范的心。对于其他形状,您可能能够找到数学表达式方面的标准定义。

在这种情况下,我创建了一组对象,然后我用程序来制作一个组合。您可以计划您的应用程序包括图片和视频与一些图形,矩形和心脏。完成后,您可以将此程序提供给其他人使用。这类似于为玩家构建游戏程序。该应用程序的最终用户可能是家庭成员、朋友或同事。项目列表存储在独立于主程序的文件中,因此很容易更改包含的内容。将内容规范从程序中分离出来的技术是一个很好的技巧。

当然,当然可以使用 Adobe Photoshop 或 Corel Paint Shop Pro 等绘图程序来创建这样的作品,但该应用程序为其特定目的提供了相当好的易用性。该项目也是学习重要编程技术以及 HTML5 和 JavaScript 特性的一个途径。而且,正如人们不断重复的那样,各种浏览器之间也有不同之处需要讨论。

关键要求

这个项目的关键需求包括构建一个在屏幕上操作对象的框架,包括检测对象上的鼠标事件、删除对象和创建对象的副本,以及在外部文件中指定内容。当前的框架提供了一种指定矩形、椭圆形、心形和图像的方法,但是这种方法可以适应其他形状,这是本章的重要一课。

目标是使拖放操作相当精确:不仅仅是将某物从窗口的一个区域移动到另一个区域。我将在关于制作拼图游戏的第 8 和 9 章中再次讨论这个话题。

我还决定控制光标的外观。当鼠标不在画布上时,光标就是标准箭头。当在画布元素上时,光标将成为十字光标。当用户按下鼠标按钮并拖动一个对象时,光标变成一只带指针的手。

当工作完成时,很自然地希望保存它,也许作为一个图像文件,所以这也是项目的一个需求。

在为这本书的第二版做这个项目时,我发现了 Chrome 浏览器的一个特性,我需要讨论一下:自动播放策略。

自动播放策略

自动播放是指自动播放视频剪辑,无需用户操作。在剪贴画项目中,将在第三章中描述的弹跳视频,以及将在第八章中描述的拼图变成视频程序,我的意图是让视频在程序控制下播放。我承认有人反对这一点。视频的自动播放可能会使用户支付数据费用,并可能使网络过载。视频广告可能很烦人。

截至 2018 年 4 月,Chrome 浏览器采用了视频自动播放的政策(详见 https://developers.google.com/web/updates/2017/09/autoplay-policy-changes ),在大多数情况下不允许自动播放。但是,也有例外,包括静音。对于剪贴画程序,我决定通过静音来启用视频播放。我的原始程序提供了一种方法,让每个视频有不同的音量级别。因为这可以在 Firefox 上工作,也许在其他浏览器上也可以,至少现在,我在程序中保留了指定视频音量的机制。但是,该代码在视频标记中包含一个静音属性,因此您需要移除它才能听到音频。苹果要求 iPhones 和 iPads 用户启动任何视频已经有一段时间了,我将在第八章中描述这一点的含义。

这对我们来说是一个教训:1)事物在变化,2) HTML/JavaScript/CSS 程序依赖于浏览器。

HTML5、CSS 和 JavaScript 特性

我们现在探索用于家庭剪贴画项目的 HTML5 和 JavaScript 的特性。这个想法是维护画布上的材料列表。这个列表将是一个 JavaScript 数组。这些信息将包括每个项目的位置,如何在画布上绘制,以及如何确定鼠标光标是否在项目上。

JavaScript 对象

面向对象编程是计算机科学的标准,也是大多数编程语言的关键部分。对象有属性,也叫属性,和方法。方法是一个函数。换句话说,一个对象有数据和可能使用这些数据的代码。HTML 和 JavaScript 有很多内置对象,比如documentwindow,还有数组和字符串。对于家庭剪贴画项目,我使用 JavaScript 中的一个基本工具(在 HTML5 之前建立)来定义我自己的对象。这些有时被称为用户定义的对象,但是我和其他人更喜欢的术语是程序员定义的对象。这对于家庭剪贴画项目来说是一个重要的区别,在这个项目中,你,程序员,可以用你识别和设计的图片和其他形状创建一个应用程序,然后提供给家庭成员使用。

这个项目的目标是建立一个框架,用于在画布上创建和操作不同的形状,记住,一旦一些东西被绘制到画布上,它作为矩形或图像的身份就丢失了。每个形状的第一步是定义一个称为构造函数的函数,它存储指定形状的信息。下一步是定义方法和代码,使用这些信息来做需要做的事情。

我的方法给人一种在画布上移动物体的感觉。事实上,保存在内部变量中的信息会发生变化,每次发生改变画布外观的情况时,画布会被清除并绘制新的图形。

我的策略是定义新类型的对象,每个对象都定义了两个方法:

  • draw用于在画布上绘制对象

  • overcheck用于确定给定位置,特别是鼠标位置,是否在对象上

这些方法引用对象的属性,并在数学表达式中使用这些值来产生结果。一旦定义了构造函数,就可以将值创建为这些对象的新实例。一个名为stuff的数组保存所有的对象实例。

注意

面向对象编程的辉煌之处在于它拥有丰富的、通常令人望而生畏的词汇。类是定义对象的东西。我在这里已经暗示了所谓的接口。类可以是其他类的子类,这可能对图片和矩形有用。我想用更随意的语气。例如,我将谈到对象和对象实例。

让我们抛开泛泛而谈,看看这是如何工作的。有一种说法:先有鸡还是先有蛋?我在订单上有一个“先有鸡还是先有蛋”的问题。我首先描述外部文件中内容的规范。然后我将描述我创建并命名为RectOvalPicture, VideoblockHeart的函数。这些将被称为RectOvalPictureVideoblockHeart对象实例的构造函数。以大写字母开始这些函数是一种惯例。然后我将描述调用constructor函数的createelements函数。关于绘制对象,有一个类似的“鸡和蛋”的问题。

规范的外部文件

对象的规范保存在一个单独的文件中。我显示长注释是因为我需要它来记住每个对象的参数是什么。

/*
Information on videos, other objects used in collagebase.html
You need to produce 3 video files for each video, type mp4,ogg,webm, with names as indicated in the videoinfo array.
The first element of each subarray indicates the type of object, that is, 'video', 'heart', 'picture', 'oval','rectangle'.
The elements for video objects are
"video", basename of video files, angle in radians, source x, source y, destination on canvas x, destination y, width, height, scale factor (x and y), volume level (0 to 1)
The angle can be used to change the orientation for clips shot on iPhone or iPads.
The source x and y, with the width and height, allows you to use only some of the source video.

The elements for 'picture' are
'picture',x,y,w,h,imagename.

The elements for heart are
'heart',x,y,h,drx,color

The elements for oval are
'oval',x,y,r,horizontal scaling, vertical scaling, color

The elements for rectangle are
'rect',x,y, w,h,color

The element for video

are
'video',videoname, angle, sourcex, sourcey, x, y, width, height, scale, volume, alpha
Note: the width and height are the final (destination) width and height.
*/

var mediainfo=
[
['heart', 300,40,100,30,'red'],

['rect',620,400,100,150,"purple"],

['oval',600,50,30,2,1,'green'],

['oval',80, 500, 30, 2, 1, 'blue'],

['video','monkeyMar18',0,0,0,1000,800,896,1198,.25,1],

['picture',5,150, 150, 200,'danielAndAnnika.jpg'],

['picture',500,150,280,210,'threePlusDog.jpg']

];

矩形

Rect constructor功能的定义是

function Rect(x,y,w,h,c) {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.draw = drawrect;
        this.color = c;
        this.overcheck = overrect;
}

可以通过以下方式调用该函数:

var r1 = new Rect(2,10,50,50,"red");

变量r1被声明并设置为使用函数Rect构造的新对象。内置术语new完成创建新对象的任务。新构造的对象保存初始 x 和 y 位置的值 2 和 10,使用属性名xy访问,使用属性名wh访问宽度和高度的值 50 和 50。术语this指的是正在构建的对象。的英文意思和计算机行话意思相符。Rect函数还存储属性drawovercheck的离开值。到目前为止,您所看到的并不明显,但是这些值将用于调用名为drawrectoverrect的函数。这是为程序员定义的对象指定方法的方式。最后,color属性被设置为“red”。存在指定颜色的其他可能性。

卵形的

接下来,Oval的构造函数是类似的。

function Oval(x,y,r,hor,ver,c) {
        this.x = x;
        this.y = y;
        this.r = r;
        this.radsq = r*r;
        this.hor = hor;
        this.ver = ver;
        this.draw = drawoval;
        this.color = c;
        this.overcheck = overoval;
}

xy值是指椭圆的中心。horver属性将分别用于缩放水平轴和垂直轴,并根据值生成一个不是圆的椭圆。在overoval功能中,计算并存储radsq属性以节省时间。

注意

电脑速度很快,我通过储存然后用半径的平方来显示我的年龄。尽管如此,为了节省计算时间而进行额外存储的权衡可能是合理的。

设置蓝绿色椭圆形的一种方法是

var oval1 = new Oval(200,30,20,2.0,1.0, "teal");

紫色圆圈的horver值相同,圆圈也是如此。你完全有权利询问如何或在哪里使用这些信息来产生一个椭圆或圆。答案就在后面将要展示的drawoval函数中。类似地,overoval函数检查给定的 x,y 位置是否在椭圆上。

对象的构造函数存储位置、宽度和高度,以及图像对象的名称。

function Picture(x,y,w,h,imagename) {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.imagename = imagename;
        this.draw = drawpic;
        this.overcheck = overrect;
}

设置一张图片需要下面的代码,设置一个Image变量,然后是Picture对象:

var dad = new Image();
dad.src = "daniel1.jpg";
var pic1 = new Picture(10,100,100,100,dad);

视频块

Videoblock 的构造函数提供了相当大的灵活性。视频可以倾斜一个角度(虽然我选择不这样做的猴子酒吧视频剪辑)。可以控制音频的音量。如果您决定包含多个视频,并且所有视频都有音频,您可能需要控制音量。视频可以缩放。sxsy ( s表示信号源)允许我指定从视频中的哪个位置开始提取要显示的视频。xy指定画布中的位置,而wh指定最终的宽度和高度。alpha参数可用于设置视频的不同透明度。对于视频,我坚持设置为 1,没有透明度,但是我的代码确实提供了一种使视频看起来比其他元素更亮的方法。这应该作为一个通知,以调查结合图像和属性,如globalAlpha

function Videoblock(sx,sy,x,y,w,h,scale,videoel,volume,angle,alpha) {
      this.sx = sx;
      this.sy = sy;
      this.x = x;
      this.y = y;
      this.w = w;
      this.h = h;
      this.videoelement = videoel;
      this.volume = volume;
      this.draw = drawvideo;
      this.overcheck = overvideo;  //need more complex checking because of angle and scale
      this.angle = angle;
      this.cosine = Math.cos(angle);
      this.sine = Math.sin(angle);
      this.scale = scale;
      this.alpha = alpha;
      videoel.volume = 0;
}

我们还要介绍一个程序员定义的对象。我给自己设定的挑战是定义指定心形的值。我得出了以下结论:心形由位置定义——一对 x,y 值,它们将是心脏裂口的位置;从裂缝到底点的距离;以及代表心脏弯曲部分的两个部分圆的半径。你可以认为这是一颗规范的心。关键信息如图 2-3 所示。如果向应用程序添加新类型的形状,您将需要发明或发现定义该形状的数据。

img/272384_2_En_2_Fig3_HTML.jpg

图 2-3

定义心脏的数据

构造函数将指示的值和颜色一起保存到任何新构造的对象中。你可能会怀疑绘图和检查会比矩形的函数更复杂,你是对的。构造函数类似于另一个构造函数。

function Heart(x,y,h,drx,color) {
        this.x = x;
        this.y = y;
        this.h = h;
        this.drx = drx;
        this.radsq = drx*drx;
        this.color = color;
        this.draw = drawheart;
        this.overcheck = overheart;
        this.ang = .25*Math.PI;

}

属性是我两面下注的一个例子。你注意到它是一个常量,我可以避免把它变成一个属性。稍后当我解释drawheart我的编码如何使用它来使心脏变圆时,你会看到。我把它作为一个属性,以防万一我想改变,让心脏有更多的可变性。

创建元素

至此,我已经向您展示了对象的规范以及如何创建对象,但是我还没有向您展示从规范到创建的代码。这是在我的createelements函数中完成的。注意使用元素类型来决定动作的switch语句。最复杂的案例是视频,其次是picture。我的代码需要创建一个 HTML 视频元素,这是通过在我的代码创建的名为videomarkup的字符串中插入视频文件的名称来完成的。反过来,videomarkup是通过组合三个变量创建的:videotext1videotext2videotext3。我可以只用一个,但是初始化它们的语句会很长。使用 JavaScript 提供的名为replaceString方法来插入名称。这是所谓的正则表达式的完整实现的一部分。视频案例也有两个对addEventListener的调用。针对事件loadeddata的一个调用用于等待所有视频完全加载。针对事件ended的另一个调用调用了restart函数。这对于不支持循环的浏览器是必要的。所有案例都包含一个将元素添加到名为stuff的数组中的语句。

function createelements() {
      var name;
      var i;
      var type;
      var divelement;
      var videomarkup;
      var velref;
      var vb;
      var imgdummy;

      for (i=0;i<mediainfo.length;i++) {
             type = mediainfo[i].shift();  //removes 1st element from array
          info = mediainfo[i];

             switch(type) {
              case 'video':
               videocount++;
               name = info[0];
            divelement= document.createElement("div");
                   videomarkup = videotext1+videotext2+videotext3;
                   videomarkup = videomarkup.replace(/XXXX/g,name);
                   divelement.innerHTML = videomarkup;
                   document.body.appendChild(divelement);
                   velref = document.getElementById(name);
                   velref.addEventListener("ended",restart,false);
                   velref.addEventListener("loadeddata",videoloaded,false);
       vb = new Videoblock(info[2],info[3],info[4],info[5],info[6],info[7],info[8],velref,info[9],info[1],info[10]);
                   stuff.push(vb);
                   break;
              case 'picture':
               imgdummy = new Image();
               imgdummy.src = info[4];
               images.push(imgdummy);
               stuff.push( new Picture(info[0],info[1],info[2],info[3],images[images.length-1]));

               break;
              case 'heart':
                stuff.push(new Heart(info[0],info[1],info[2],info[3],info[4]));
                break;
              case 'oval':
                stuff.push(new Oval(info[0],info[1],info[2],info[3],info[4],info[5]));
              break;
              case 'rect':
                stuff.push(new Rect(info[0],info[1],info[2],info[3],info[4]));
              break;
             }

        }

}

图画

我仍然需要解释为每个不同类型的元素完成 draw 方法的函数,但是为了演示所有这些是如何一起工作的,让我们先来看看绘制是在哪里完成的。我定义了一个数组,最初是空的

var stuff = [];

createelements函数调用 array push方法将每个元素添加到数组中。

在适当的时候,即在任何变化之后,调用函数drawstuff。它的工作方式是擦除画布,绘制一个矩形来制作一个框架,然后遍历stuff数组中的每个元素并调用draw方法。该功能是

function drawstuff() {
        ctx.clearRect(0,0,800,600);
        ctx.strokeStyle = "black";
        ctx.lineWidth = 2;
        ctx.strokeRect(0,0,800,600);
        for (var i=0;i<stuff.length;i++) {
                stuff[i].draw();
        }
}

注意,没有代码会问,这是一个椭圆吗,如果是这样做,还是一张图片,如果是这样做....相反,为数组的每个成员建立的draw方法完成了它的工作!当检查一个位置(鼠标)是否在一个对象上时,同样的魔法也会发生。随着更多对象类型的添加,这种方法的好处也会增加。

我确实意识到,由于我的代码从不改变strokeStylelineWidth,我可以将这些语句移到init函数中,只做一次。然而,我想到我可能有一个形状会改变这些值,所以为了准备以后应用程序中可能的改变,我在drawstuff中设置了strokeStylelineWidth

现在我将解释绘制的方法和检查对象上是否有位置的方法。drawrect函数非常简单:

function drawrect() {
        ctx.fillStyle = this.color;
        ctx.fillRect(this.x, this.y, this.w, this.h);
}

记住术语this指的是drawrect作为方法的对象。drawrect函数是矩形的方法。

drawoval函数稍微复杂一点,但仅仅是稍微复杂一点。您需要回忆一下坐标变换是如何工作的。HTML5 JavaScript 只允许圆弧,但允许缩放坐标以生成不是圆的椭圆。drawoval函数中的代码所做的是保存坐标系的当前状态,然后执行向对象中心的平移。然后应用缩放变换,使用horver属性。现在,在将fillStyle设置为color属性中指定的颜色后,我使用代码绘制一条由圆弧组成的路径并填充该路径。圆弧可以是圆的一部分,具有指定的开始和结束角度,true 表示逆时针方向。对于顺时针方向,默认值为 false。对于一个完整的圆,也就是这里指出的,我可以省略 true,因为它和 false 有相同的结果。参见心脏的编码,其方向至关重要。最后一步是恢复坐标系的原始状态。

function drawoval() {
        ctx.save();
        ctx.translate(this.x,this.y);
        ctx.scale(this.hor,this.ver);
        ctx.fillStyle = this.color;
        ctx.beginPath();
        ctx.arc(0,0,this.r,0,2*Math.PI,true);
        ctx.closePath();
        ctx.fill();
        ctx.restore();
}

这就是在画布上画出可能是圆也可能不是圆的椭圆的方式。由于我的代码恢复了坐标系统的原始状态,这具有撤销缩放和平移转换的效果。

同样,以this开头的术语后跟一个点,然后是引用存储属性的属性名。

注意

请记住,我没有计划和编程这整个应用程序一次。我画了长方形和椭圆形,然后添加了图片,很久以后才添加了心形。我还在很久以后添加了复制操作和删除操作。分阶段工作才是正确的做法。计划很重要也很有用,但是你不必一开始就把所有的细节都做好。

drawheart函数从定义稍后使用的变量开始。leftctrx是左圆弧中心的 x 坐标,rightctrx是右圆弧中心的 x 坐标。每个圆弧都超过半个圆。多了多少?我决定将它设为.25* Math.PI,并将这个值存储在ang属性中。

棘手的事情是确定弧线在右侧的终止位置。我的代码使用 trig 表达式来设置cxcy值。cx,cy位置是圆弧与直线相交的地方。图 2-4 表示变量的含义。

img/272384_2_En_2_Fig4_HTML.jpg

图 2-4

添加了函数中使用的数据

路径将从我们称之为裂缝或乳沟(咯咯笑)的地方开始,并在左边画出弧线。然后它会画一条线到底点,再往上到cx,cy点,然后以右边的弧线结束。该函数如下所示:

function drawheart() {
        var leftctrx = this.x-this.drx;
        var rightctrx = this.x+this.drx;
        var cx = rightctrx+this.drx*Math.cos(this.ang);
        var cy = this.y + this.drx*Math.sin(this.ang);
        ctx.fillStyle = this.color;
        ctx.beginPath();
        ctx.moveTo(this.x,this.y);
        ctx.arc(leftctrx,this.y,this.drx,0,Math.PI-this.ang,true);
        ctx.lineTo(this.x,this.y+this.h);
        ctx.lineTo(cx,cy);
        ctx.arc(rightctrx,this.y,this.drx,this.ang,Math.PI,true);
        ctx.closePath();
        ctx.fill();
}

画图画是直截了当的。对于图片和视频,如果一个对象在另一个对象之上,我提供了一种生成合成图的方法。

function drawpic() {
    ctx.globalAlpha = 1.0;
    ctx.drawImage(this.imagename,this.x,this.y,this.w,this.h);
}

视频块的绘制更为复杂,因为需要将视频放在一个角度并进行缩放。了解正在绘制的是视频剪辑的当前帧也很重要。这是使用 canvas 元素的drawimage方法完成的。

function drawvideo() {
      var savedalpha = ctx.globalAlpha;
      ctx.globalCompositeOperation = "lighter";
      ctx.globalAlpha = this.alpha;
   if (this.angle!=0) {
      ctx.save();
      ctx.translate(this.x,this.y);
      ctx.rotate(this.angle);
      ctx.translate(-this.x,-this.y)
      if (this.scale!=1) {

             ctx.scale(this.scale,this.scale); }
      ctx.drawImage(this.videoelement,this.sx,this.sy,this.w,this.h,this.x,this.y, this.w, this.h);

      ctx.restore();

      }
      else {
             if (this.scale!=1) {
                   ctx.save();
                   ctx.scale(this.scale,this.scale);

       ctx.drawImage(this.videoelement,this.sx,this.sy,this.w,this.h,this.x,this.y, this.w, this.h);

                   ctx.restore();

             }
             else {
       ctx.drawImage(this.videoelement,this.sx,this.sy,this.w,this.h,this.x,this.y, this.w, this.h);
             }
      }
      ctx.globalAlpha = savedalpha;
      ctx.globalCompositeOperation = savedgco;
}

检查对象上的鼠标

在描述overcheck方法的函数之前,我将预览一下为什么需要它。HTML5 和 JavaScript 提供了处理(监听和响应)画布上鼠标事件的方法,并提供事件发生的坐标。然而,我们的代码必须完成确定所涉及的对象的工作。记住:画布上实际上没有物体,只有残余物,把它想象成颜料,不管画的是什么。我的代码通过遍历stuff数组并为每个对象调用overcheck方法来完成这项任务。只要有一个命中(我稍后会解释这个操作的顺序),我的代码就会按照所选的对象继续执行。发生这种检查的函数是startdraggingmakenewitem,将在下一节中解释。

因为PictureRect指的是同一个函数,所以overcheck方法有四个函数要解释。每个函数都有两个参数。把mxmy想象成鼠标的位置。overrect功能检查四个条件是否都为真。英文中的问题是:mx是否大于等于this.x是否小于等于this.x + this.w是否大于等于this.y是否小于等于this.y + this.h?该函数更简洁地说明了这一点:**

function overrect (mx,my) {
        return (
             (mx>=this.x)&&(mx<=(this.x+this.w))&&(my>=this.y)&&(my<=(this.y+this.h)));
}

为椭圆定义overcheck方法的函数是overovaloveroval功能执行检查某物是否在一个圆内的操作,但在一个平移和缩放的坐标系中。通过将圆心设置为x1,y1并将点设置为x2,y2并查看两者之间的距离是否小于半径,可以检查一个点是否在圆内。为了节省时间,我用了一种不同的方法来比较距离的平方和半径的平方。我定义了一个名为distsq的函数,它返回距离的平方。但是现在我需要弄清楚如何在一个平移和缩放的坐标系中做到这一点。答案是将x1,y1设置为 0,0。这是椭圆中心在转换坐标系中的位置。然后,我的代码将代码中指示的x2y2设置为缩放后的值。

function overoval(mx,my) {
        var x1 = 0;
        var y1 = 0;
        var x2 = (mx-this.x)/this.hor;
        var y2 = (my-this.y)/this.ver;
        if (distsq(x1,y1,x2,y2)<=(this.radsq) ){
                return true
        }
        else {return false}
}

这不是我立即想到的。我通过尝试相对于椭圆中心位于不同位置的mxmy的值算出了它。代码确实代表了转换在平移和缩放方面所做的事情。

overheart函数由几个不同的if语句组成。这是一个不是为了一个简单的表达,而是考虑各种情况的例子。该函数从设置稍后要使用的变量开始。该函数进行的第一项检查是确定mx,my点是否在作为心脏边界矩形的矩形之外。我编写了 outside 函数,如果最后两个参数指定的位置在前四个参数指示的矩形之外,则返回 true。qx,qy点是左上角。qwidth是最宽处的宽度,qheight是总高度。我认为这是一个快速检查,大多数情况下会返回 false。接下来的两个if语句决定了mx,my点是否包含在任一圆内。也就是说,我再次使用从mx,my到每个弧的中心的距离的平方与存储的radsq属性的比较。在函数的这一点上,也就是说,如果mx,my位置没有足够靠近任一圆的中心,并且如果my高于(小于)this.y,则代码返回 false。最后,代码将mx值放入每条斜线的等式中,并将结果与my进行比较。直线的方程可以用斜率m和直线上的一点x2,y2来写(注意:这是数学,不是编程):

y = m * (x – x2) + y2

代码为左边的行设置mx2,y2,然后通过改变m的符号来修改它以适用于右边的行。该检查是为x设置为mx,是我小于所示的表达式。这里一个可能的问题是屏幕坐标系具有颠倒的垂直值(垂直值沿屏幕向下增加)是否会引起问题。我查了案例,代码是有效的。

function overheart(mx,my) {
        var leftctrx = this.x-this.drx;
        var rightctrx = this.x+this.drx;
        var qx = this.x-2*this.drx;
        var qy = this.y-this.drx;
        var qwidth = 4*this.drx;
        var qheight = this.drx+this.h;

//quick test if it is in bounding rectangle
        if (outside(qx,qy,qwidth,qheight,mx,my)) {

                return false;}
//compare to two centers

  if (distsq(mx,my,leftctrx,this.y)<this.radsq) return true;
  if (distsq(mx,my,rightctrx,this.y)<this.radsq) return true;
// if outside of circles AND below (higher in the screen) than this.y, return false
  if (my<this.y) return false;

// compare to each slope
 var x2 = this.x;
 var y2 = this.y + this.h;
 var m = (this.h)/(2*this.drx);
// left side
 if (mx<=this.x) {
         if (my < (m*(mx-x2)+y2)) {return true;}
         else { return false;}
 }
 else {
//right side
 m = -m;

 if (my < (m*(mx-x2)+y2)) { return true}
 else return false;
 }
}

outside函数的推理类似于overrect函数。您需要编写代码来比较mx,my值和矩形的边长。然而,对于outside,我选择使用OR操作符||,并返回它的值。如果任何一个因素为真,这将是真的,否则为假。

function outside(x,y,w,h,mx,my) {
        return ((mx<x) || (mx > (x+w)) || (my < y) || (my > (y+h)));
}

实际上,我说的是真的,但是忽略了如果性能是一个问题的话可能是一个重要的考虑因素。||从第一个(最左边)条件开始评估每个条件。只要其中一个为真,它就停止计算并返回真。&&操作符做类似的事情。只要其中一个条件为假,它就返回假。

overvideo功能必须考虑角度和比例。

function overvideo (mx,my) {
  //need to add code to check in rotation case and scaling
             omx = mx;
             omy = my;

    if (this.angle!=0) {
             omx = omx-this.x;
             omy = omy - this.y;
             mx = omx*this.cosine + omy*this.sine;
             my = -omx*this.sine + omy*this.cosine;
             mx = this.x +mx;
             my = this.y + my;

       }
       if (this.scale!=1) {
             //alert("prescaling mx is "+mx+" prescaling my is "+my);
             mx = mx/this.scale;
             my = my/this.scale;
             //alert("post scaling mx is "+mx+" post scaling my is "+my);
       }
       return (
        (mx>=this.x)&&(mx<=(this.x+this.w))&&(my>=this.y)&&(my<=(this.y+this.h)));

}

这是我为在画布上操作而设计的五种对象的基础。您可以向前看,检查所有代码,或者继续查看这些对象是如何在对鼠标事件的响应中使用的。

注意

这个例子没有展示面向对象编程的全部能力。在 Java(或为艺术家设计的变体处理)这样的语言中,我可以用这种方式编写程序,以检查每个附加对象是否定义正确,也就是用 x 和 y 属性表示位置,用方法表示绘制和检查。

用户界面

用户界面的应用要求包括拖动,即鼠标按下、鼠标移动和鼠标抬起,用于重新定位项目和双击以产生项目的副本。我决定对其他终端用户操作使用按钮:从画布中移除一个项目并创建一个要保存的图像。按钮操作非常简单。我编写了 HTML5 按钮元素的两个实例,并将onClick属性设置为适当的函数。

<button onClick="saveasimage();">Open window with image (which you can save into image file)
 </button></br>
<button onClick="removeobj();">Remove last object moved </button>

下一节将解释saveasimage功能。removeobj函数从stuff数组中删除最后移动的对象,因为最后移动的对象已经被定位为数组中的最后一个元素。这使得编码极其简单:

function removeobj() {
        stuff.pop();
        drawstuff();
}

任何数组的一个pop删除最后一个元素。然后,该函数调用drawstuff函数来显示除最后一个元素之外的所有元素。顺便说一句,如果在应用程序开始时点击按钮,在stuff数组上按下的最后一个元素将被删除。如果这是不可接受的,您可以添加一个检查来防止这种情况发生。代价是用户每次点击按钮都需要这样做。

幸运的是,HTML5 提供了这个应用程序需要的鼠标事件。在init函数中,我包含了以下几行:

   canvas1 = document.getElementById('canvas');
   canvas1.onmousedown = function () { return false; };
   canvas1.addEventListener('dblclick',makenewitem,false);
   canvas1.addEventListener('mousedown',startdragging,false);

第一条语句设置canvas1变量来引用canvas元素。第二条语句是关闭光标的默认操作所必需的。我还为画布添加了一个样式指令,它使定位成为绝对的,然后将画布定位在距离顶部 80 像素的位置。这对于方向和按钮来说是足够的空间。

canvas {position:absolute; top:80px;
  cursor:crosshair;
}

第三和第四个语句为双击和mouse button down事件设置事件处理。我们应该意识到,作为程序员,我们不必编写代码来区分鼠标按下、单击和双击。然而,不幸的是,双击会同时调用makenewitem函数和startdragging函数。在这种情况下,这没什么,但在未来的工作中一定要意识到这一点。

makenewitemstartdragging功能开始时相同。代码首先确定鼠标光标的坐标,然后遍历stuff数组来确定哪个对象被点击了。你可能以前在 HTML5 的基本指南中见过鼠标光标坐标代码,例如。以相反的顺序循环数组。调用overcheck方法,为不同类型的对象适当地定义。如果命中,那么makenewitem函数调用clone函数来复制该项。代码稍微修改了 x 和 y,这样新的项目就不会直接位于原始项目之上。新的项目被添加到数组中,并且有一个断点离开for循环。

function makenewitem(ev) {
        var mx;
        var my;
        if (ev.layerX ||  ev.layerX == 0) {
                mx= ev.layerX;
                my = ev.layerY;
                } else if (ev.offsetX || ev.offsetX == 0) {
                          mx = ev.offsetX;
                          my = ev.offsetY;
                }
        var endpt = stuff.length-1;
        var item;
        for (var i=endpt;i>=0;i--) {  //reverse order
                if (stuff[i].overcheck(mx,my)) {
                   item = clone(stuff[i]);
                   item.x +=20;
                   item.y += 20;
                   stuff.push(item);
                   break;
                }
        }
}

如前所述,clone函数复制了stuff数组中的一个元素。你可能会问,为什么不直接写呢

         item = stuff[i];

答案是,这种分配并没有创造新的、独特的价值。JavaScript 只是将item变量设置为指向与stuff的第 I 个成员相同的东西。这叫做“引用复制”。我们不想那样。我们想要一个全新的、独立的、我们可以改变的东西。复制的方法在clone功能中演示。创建一个新对象,然后调用一个for循环。for(var info in obj)说:对于obj的每一个属性,将item中的一个同名属性设置为该属性的值。

function clone(obj) {
        var item = new Object();
        for (var info in obj) {
                item[info] = obj[info];
        }
        return item;

}

所以这两个函数的作用是复制鼠标光标下的任何元素。然后,您或您的最终用户可以将鼠标放在原始或克隆的对象上,并四处移动它。

startdragged功能按照指示进行,以确定哪个对象在鼠标下面。然后,代码确定我(和其他人)所说的鼠标坐标在 x 和 y 方向相对于对象的 x,y 位置的偏移量。这是因为我们希望对象四处移动,保持对象和鼠标之间的关系不变。有些人称之为捕蝇纸效应。这就好像鼠标光标落在对象上,像捕蝇纸一样粘在上面。offsetxoffsety是全局变量。请注意,编码适用于 x、y 值指向左上角(图片和矩形)、中心(椭圆形)和特定内部点(心形)的对象。

然后,代码执行一系列操作,将该对象移动到数组的末尾。第一条语句是设置变量 item 的引用复制操作。下一步将填充数组最后一个元素的索引保存到全局变量thingInMotion。该变量将被moveit函数使用。splice语句删除原始元素,push语句将它添加到数组的末尾。引用游标的语句是指定游标的方式。“指针”指的是其中一个内置选项。函数中的最后两条语句设置了移动鼠标和释放鼠标按钮的事件处理。该事件处理将在dropit功能中移除。

function startdragging(ev) {
        var mx;
        var my;
        if (ev.layerX ||  ev.layerX == 0) {
                        mx= ev.layerX;
                my = ev.layerY;
                } else if (ev.offsetX || ev.offsetX == 0) {
                mx = ev.offsetX;
                my = ev.offsetY;
                }
        var endpt = stuff.length-1;
        for (var i=endpt;i>=0;i--) {  //reverse order
                if (stuff[i].overcheck(mx,my)) {
                offsetx = mx-stuff[i].x;
                 offsety = my-stuff[i].y;
                 var item = stuff[i];
                 thingInMotion = stuff.length-1;
                 stuff.splice(i,1);
                 stuff.push(item);
                 canvas1.style.cursor = "pointer";   // change to finger
                 canvas1.addEventListener('mousemove',moveit,false);
                 canvas1.addEventListener('mouseup',dropit,false);
                 break;
                }
        }
}

moveit函数移动由thingInMotion引用的对象,并使用offsetxoffsety变量移动对象。调用drawstuff函数来显示修改后的画布。

function moveit(ev) {
        var mx;
        var my;
        if ( ev.layerX ||  ev.layerX == 0) {
                mx= ev.layerX;
                my = ev.layerY;
                } else if (ev.offsetX || ev.offsetX == 0) {
                mx = ev.offsetX;
                my = ev.offsetY;
                }
        stuff[thingInMotion].x = mx-offsetx; //adjust for flypaper dragging
        stuff[thingInMotion].y = my-offsety;
  }

如果鼠标向任何方向移动一个像素,就会触发一个mousemove事件。如果这看起来太多了,请记住是计算机做的,而不是你或我。用户移动鼠标会得到平滑的响应。

mouseup事件时调用dropit功能。响应是移除、停止移动和释放鼠标的监听,然后将光标变回十字光标。

function dropit(ev) {
        canvas1.removeEventListener('mousemove',moveit,false);
        canvas1.removeEventListener('mouseup',dropit,false);
        canvas1.style.cursor = "crosshair";  //change back to crosshair
}

总而言之,这个应用程序的用户界面包括两个按钮和几个鼠标动作。拖放操作是通过按下鼠标、移动鼠标和抬起鼠标来实现的,克隆对象是通过双击来完成的。

将画布存储到图像

创建合成后,我为用户提供了一种将它保存到图像文件的方法。火狐浏览器让这变得简单。使用 PC 时,您可以在画布上单击鼠标右键,或者在 Mac 上执行相同的操作,会出现一个弹出菜单,其中包含将图像另存为的选项...然而,Chrome、Safari 和 Opera 不提供这种功能。如果右键单击,这些选项与 HTML 文档有关。然而,HTML5 中提供了一种替代方案,适用于 Firefox,或许还适用于其他浏览器。对 Chrome 的支持随着最近的更新而改变。

canvas 元素有一个名为to DataURL的方法,它将从 canvas 中产生一个图像。该方法提供了图像文件类型的选择,包括 PNG 和 JPG。我选择对这个操作的结果做的是编写代码来打开一个以图像为内容的新窗口。然后,用户可以通过保存文件选项或右键单击图像,将该图像保存为文件。但是,还有一个考虑。Firefox 要求这段代码在服务器上运行,而不是在客户端计算机上运行。客户端计算机是运行浏览器程序的计算机。服务器计算机将是网站,你将上传你完成的工作。你可能有也可能没有。Opera 和 Safari 允许代码从客户端电脑运行。这对测试有影响,因为一般来说,我们在本地测试程序,然后上传到服务器。由于这种情况,这是使用 JavaScript 的try / catch工具捕捉错误(可以这么说)以便程序员采取行动的合适地方。下面是saveasimage函数的代码。变量canvas1已被设置为加载文档时调用的init函数中的 canvas 元素。

function saveasimage() {
 try {
  window.open(canvas1.toDataURL("image/png"));}
  catch(err) {
          alert("You need to change browsers AND/OR upload the file to a server.");
  }
}

构建应用程序并使之成为您自己的应用程序

您可以通过识别您自己的媒体文件,指定您想要在要操作的对象集合中包括哪些矩形、椭圆形和心形,并在您完成一些工作后,添加新的对象类型,来使该应用程序成为您自己的应用程序。该应用程序有许多功能,但每个功能都很小,并且许多功能与其他功能具有相同的属性。申请的非正式摘要/大纲如下

  1. init用于初始化,包括createelement函数,设置双击、鼠标按下、鼠标移动和鼠标抬起的事件处理。

  2. 对象定义方法:构造函数、绘制函数和检查函数。

  3. 事件处理功能:鼠标事件和按钮onClick

更正式的说法是,表 2-1 列出了所有的函数,并指出它们是如何被调用的以及它们调用了哪些函数。请注意,由于函数被指定为对象类型的方法,因此会调用几个函数。

表 2-1

html 5 家庭剪贴画项目中的函数

|

功能

|

调用/调用者

|

打电话

|
| --- | --- | --- |
| init | 由标签<body>中的onLoad attribute动作调用 | PictureRect, OvalHeartdrawstuff |
| saveasimage | 由按钮标签中的onClick属性的动作调用 |   |
| removeobj | 由按钮标签中的onClick属性的动作调用 | drawstuff |
| createelements | 由init调用 | VideoblockPictureRectOvalHeart |
| restart | 由createelementsaddEventListener的动作调用 |   |
| videoloaded | 由createelementsaddEventListener的动作调用 |   |
| loading | 由initsetInterval的动作调用 |   |
| Picture | 在createelements功能中调用 |   |
| Rect | 在createelements功能中调用 |   |
| Oval | 在createelements功能中调用 |   |
| Heart | 在createelements功能中调用 |   |
| Videoblock | 在createelements功能中调用 |   |
| drawheart | 在drawstuff中调用 |   |
| drawrect | 在drawstuff中调用 |   |
| drawoval | 在drawstuff中调用 |   |
| drawpic | 在drawstuff中调用 |   |
| drawvideo | 在drawstuff中调用 |   |
| overheart | 在startdraggingmakenewitem中调用 | distsq, outside |
| overrect | 在startdraggingmakenewitem中调用 |   |
| overoval | 在startdraggingmakenewitem中调用 | distsq |
| overvideo | 在startdraggingmakenewitem中调用 |   |
| distsq | 由overheartoveroval调用 |   |
| drawstuff | 加载时由makenewitemremoveobjinitsetInterval动作调用 | 填充数组中每一项的绘制方法 |
| moveit | 由在startdragging中设置的mousemoveaddEventListener设置的动作调用 |   |
| dropit | 由在startdragging中设置的mouseupaddEventListener设置的动作调用 |   |
| outside | 由overheart调用 |   |
| makenewitem | 由在init中设置的dblclickaddEventListener设置的动作调用 | clone |
| clone | 由makenewitem调用 |   |
| startdragging | 由 init 中设置的mousedownaddEventListener设置的动作调用 |   |

表 2-2 显示了基本应用程序的代码,每一行都有注释。

表 2-2

家庭剪贴画项目的完整代码

| `` | HTML5 文档的标准标题 | | `` | `html`标签 | | `` | `head`标签 | | `Collage, with video` | 完整标题 | | `` | `meta`标签 | | `` | 关闭样式 | | `` | 外部脚本元素链接 | | `` | 关闭`script`元素 | | `` | 关闭`head`元素 | | `` | 设置了`onLoad`的主体标签 | | `Mouse` `down, move and mouse up to move objects. Double-click for make a copy of any object.` | 给出方向的文本 | | `
` | 换行符 | | `` | 画布标签 | | `Your browser doesn't recognize the canvas element` | 针对旧浏览器的消息 | | `` | 结束画布标签 | | `
` | 保存图像的按钮 | | `` | 用于移除对象的按钮 | | `` | 关闭`body`标签 | | `` | 关闭`html`标签 |

只使用我的例子中演示的技术,如何使这个应用程序成为您自己的应用程序是显而易见的:收集您自己家庭的照片和视频或获取其他媒体,并使用矩形、椭圆形和心形来创建您自己的一组形状。

您可以使用这里的代码作为模型来定义自己的对象。例如,HTML5 书的基本指南包括显示多边形的编码。你可以让多边形的overcheck函数把多边形当作一个圆,也许是一个半径更小的圆,你的客户不会反对。

下一步可能是构建一个允许最终用户指定图像文件地址的应用程序。为此,您需要设置一个表单。另一个增强是允许最终用户输入文本,可能是一个问候,并将其放置在画布上。您将创建一个新的对象类型并编写drawovercheck方法。overcheck方法可以是overrect,也就是说,程序接受包围矩形中的任何文本。

测试和上传应用程序

该应用程序由代码文件组成,其中一个文件的扩展名为。html,另一个扩展名为。js 加上所有的媒体文件。您需要收集所有想要包含在应用程序中的媒体文件,并创建。js 文件,它引用媒体文件并指定您希望如何处理它们。换句话说,你可以保留我的。html 文件,并替换为您自己的文件。js 文件,引用您的所有媒体。测试过程取决于您使用的浏览器。实际上,用几种浏览器进行测试是一种很好的做法。如果您使用的是 Firefox,您需要上传应用程序。html 文件和所有图像文件—发送到服务器,以测试创建图像的功能。但是,应用程序的其他方面可以在您自己的(客户端)计算机上进行测试。

摘要

在本章中,您学习了如何构建一个应用程序,包括创建和定位特定的形状,即矩形、椭圆形和心形,以及图片,如画布上的照片。编程技术和 HTML5 特性包括:

  • 分离内容和动作

  • HTML5 元素的动态创建

  • 编程定义的对象

  • 画布上的鼠标事件

  • 使用trycatch捕捉错误

  • 几个函数的代数和几何

  • 对视频自动播放策略的思考

下一章将描述如何创建一个应用程序,展示一个像盒子里的球一样来回跳动的视频剪辑。***

三、弹跳视频(BouncingVideo):动画和遮罩 HTML5 视频

在本章中,您将学习以下内容:

  • 通过在画布上的不同位置绘制视频的当前帧来制作移动视频剪辑

  • 通过在文档窗口中重新定位视频元素来制作移动的视频剪辑

  • 通过绘制一个随视频移动的遮罩,使移动的视频成为画布上画框中的一个圆

  • 使用clipPath使运动视频在运动元素情况下成为一个圆

  • 构建一个适应不同窗口大小的应用程序

介绍

本章的项目是展示一个在盒子里弹跳的球形状的视频剪辑。HTML5 中的一个重要特性是对视频(和音频)的原生支持。Silvia Pfeiffer(2010 年出版)的书《HTML5 视频权威指南》是一个很好的参考。这个项目的挑战是让视频剪辑在屏幕上移动。我将描述实现该应用程序的两种不同方式。截图并没有揭示出区别。

图 3-1 显示了应用程序在 Firefox 中的全窗口视图中的样子。该视频是标准的矩形视频剪辑。它看起来像球,因为我的编码。您可以跳到图 3-8 和图 3-9 来学习制作球形视频的两种技术。注意:所有图形都是静态的动画截图。你需要相信我的话,视频确实会在盒子里移动和跳动。

img/272384_2_En_3_Fig1_HTML.jpg

图 3-1

屏幕捕获,全窗口

当虚拟球撞到墙上时,它看起来像是从墙上反弹回来。例如,如果虚拟球在屏幕上向右下方移动,当它碰到盒子的右侧时,它会向左移动,但仍然在屏幕上向下移动。当虚拟球碰到盒子的底壁时,它会向左反弹,向屏幕上方移动。轨迹如图 3-2 所示。为了生成这个图像,我将虚拟球更改为一个简单的圆,并且没有编写代码来在每个时间间隔擦除画布。你可以把它想象成定格摄影。改变虚拟球是必要的,因为它很复杂:一个视频剪辑的图像和一个全白的面具。

img/272384_2_En_3_Fig2_HTML.jpg

图 3-2

虚拟球的轨迹

如果我将浏览器窗口调整得更小一点,并重新加载应用程序,代码将调整画布的大小,产生如图 3-3 所示的内容:一个更小的框,但大小相同的视频。

img/272384_2_En_3_Fig3_HTML.jpg

图 3-3

较小窗口中的应用程序

如果窗口变得非常小,这将迫使视频剪辑本身以及画布和框的大小发生变化,如图 3-4 所示。

img/272384_2_En_3_Fig4_HTML.jpg

图 3-4

窗口调整到非常小

在 HTML 文档第一次被加载时,应用程序使框尺寸和虚拟视频球尺寸适应窗口尺寸。如果查看器稍后调整了窗口的大小,则在应用程序运行期间,画布和视频剪辑不会调整大小。在这种情况下,您会看到类似图 3-5 的东西,一个大窗口中的一个小盒子。

img/272384_2_En_3_Fig5_HTML.jpg

图 3-5

运行过程中,窗口大小被调整为更大

类似地,如果您使用全尺寸窗口或任何大窗口启动应用程序,并在程序运行期间将其调整为较小的尺寸,您将看到类似图 3-6 的内容,其中浏览器显示滚动条以指示文档内容比窗口更宽更长。在重新出现之前,视频剪辑会周期性地消失一小段时间。

img/272384_2_En_3_Fig6_HTML.jpg

图 3-6

调整大窗口大小

这两个应用程序(我将它们命名为“在画布上绘制视频帧”的videobounceC和“使用clipPath的视频元素”的videobounceEwithClipPath)已经在 Firefox 和 Chrome 中测试成功。注意:回头看第二章中关于 Chrome 自动播放政策的讨论。我已经为这个项目的视频标签添加了静音属性。该项目演示了使用 HTML5、JavaScript 和 CSS 处理视频的编码技术,以及将视频与画布结合使用以获得特殊效果。该项目还解释了有助于将应用程序修改为浏览器窗口尺寸的计算。在第十章中讨论了更多关于适应不同尺寸的内容。

项目历史和关键要求

我一直喜欢模拟球在盒子里弹跳的应用程序。在的第三章中,HTML5 的基本指南展示了展示一个由路径绘制产生的球和一个由图像产生的球的项目,每个球在一个二维的外壳中弹跳。我决定制作一个视频剪辑来做同样的事情。我对编码的解释在这一章已经完成了。然而,如果你有第一本书(无耻之徒),你可能会受益于看到各种版本之间的相同和不同之处。在球和图像应用程序中,画布被设置为固定尺寸,并与文档中的其他材料放在一起。因为我不想我的视频剪辑太小,我决定在这种情况下使用整个窗口。这个目标产生了确定文档窗口尺寸的挑战。在另一本书描述的球和图像应用程序中,我想演示表单验证,所以程序提供了表单元素来改变垂直和水平速度。对于弹跳球视频剪辑,应用程序只为用户提供了一个动作:一个反转方向的按钮。学习完本章后,您应该能够将其他界面操作添加到视频应用程序中。

在第二章中,你会读到包含图像和图画的视频。对于该应用程序,我使用了将当前帧作为图像绘制在画布上的技术。绘画是周期性的,并且足够频繁,以获得观看不间断视频的体验。在这一章中,我使用了这种技术,制作了一个遮罩——想象一个长方形的甜甜圈——让视频看起来像一个球。此外,我使用直接移动视频元素的方法构建了另一个 HTML/JavaScript 脚本。元素方法的一个优点是我可以使用clipPath工具使矩形视频元素看起来像球一样。这比绘制矩形圆环遮罩需要的编码要少得多。

目标是模拟一个在盒子里弹跳的球状物体。因此,应用程序必须显示盒子的墙壁并执行计算,以便当视频剪辑看起来与任何墙壁碰撞时,运动方向以适当的方式改变。描述这种变化的一种奇特方式是,反射角必须等于入射角。实际上,这意味着当视频剪辑实际上碰到底部或顶部墙壁时,它会保持水平方向不变(如果向左移动则向左移动,如果向右移动则向右移动),但会垂直切换方向。当视频剪辑虚拟地击中左墙或右墙时,它会垂直地保持相同的方向(如果它向上移动,则向上移动;如果它向下移动,则向下移动),但会水平地切换方向。如果你对模拟现实生活中的物理感兴趣,你可以在每次虚拟撞墙时减慢运动速度。

按照我的习惯,我越来越详细地描述编码。可以去找源头。

HTML5、CSS 和 JavaScript 特性

任何解释的顺序都意味着在事情的原因清楚之前,事情经常被讨论。在这一节中,我将展示如何设置某些变量,这些变量将在稍后的使用中展示。一般的计划是提取窗口尺寸来为画布和视频剪辑设置变量,这些变量将在绘制视频和遮罩的编码中被引用。

车身和车窗尺寸的定义

文档对象模型(DOM)提供关于浏览器显示 HTML 文档的窗口的信息。特别是属性window.innerWidthwindowinnerHeight表示窗户的可用尺寸。我的代码将在设置应用程序时使用这些值。

回想一下,HTML5 视频元素可以包含引用不同视频文件的任意数量的源元素作为子元素。此时,这是必要的,因为识别视频元素的浏览器不接受相同的视频格式(编解码器)。这种情况将来可能会改变。如果你知道所有你的潜在客户所使用的浏览器,你就可以确定单一的视频格式。如果不是这样,您需要制作同一视频剪辑的三个版本。可以从 www.mirovideoconverter.com/ 下载的开源 Miro 视频转换器是一个将视频剪辑转换成其他格式的好产品。

有了这个提示,我就可以呈现这个应用程序的 body 元素了。它包含一个视频元素、一个按钮和一个画布元素:

<body onLoad="init();">
<video id="vid" loop="loop" preload="auto" muted>
<source src="joshuahomerun.mp4" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>
<source src="joshuahomerun.webmvp8.webm" type='video/webm; codec="vp8, vorbis"'>
<source src="joshuahomerun.theora.ogg type='video/ogg; codecs="theora, vorbis"'>

Your browser does not accept the video tag.
</video>
<button id="revbtn" onClick="reverse();">Reverse </button><br/>
<canvas id="canvas" >
This browser doesn't support the HTML5 canvas element.
</canvas>
</body>

样式指令将改变三个元素的位置:视频、画布和按钮。我们将在本章后面讨论这些指令。

在加载文档时调用的init函数中,以下语句设置画布的尺寸以匹配窗口的尺寸:

        canvas1 = document.getElementById('canvas');
        ctx = canvas1.getContext('2d');
        canvas1.width = window.innerWidth;
        cwidth = canvas1.width;
        canvas1.height = window.innerHeight;
        cheight = canvas1.height;

这些语句设置了全局变量canvas1ctxcwidthcheight,稍后会用到。这样,使画布适应窗口的任务就完成了。

现在下一个任务需要更多的思考。我希望视频在多大程度上适应窗口尺寸?我决定要保持纵横比,但不要让视频宽度超过窗口宽度的一半,也不要让视频高度超过窗口高度的一半。我不想触发垂直或水平滚动。我很好,甚至很高兴,圆圈超出了盒子壁,因为它看起来像球变平了。我确实认为这里有改进的余地。

Math.min方法返回最小的操作数,所以语句

        v = document.getElementById("vid");
        var aspect= v.videoWidth/v.videoHeight;
        v.width = Math.min(v.videoWidth,.5*cwidth);
        v.height =   v.width/aspect;
        v.height = Math.min(v.height,.5*cheight);
        v.width = aspect*v.height;
        var videow = v.width;
        var videoh = v.height;

首先将变量v设置为指向视频元素,您可以看到我在主体中编码了这个元素,使其具有id "vid"。然后,它会计算用于保持视频部分的纵横比。我的代码首先将视频宽度与画布宽度的 0 . 5 进行比较,将其设置为两个值中的最小值,并对视频高度进行相应的更改。然后代码对新调整的高度执行类似的操作,调整宽度。

某些其他变量设置在init函数中,用于绘制一个示例中的方框和遮罩,以及另一个示例中的方框和clipPath。您可以在表 3-2 和表 3-4 中对此进行检查。

动画

动画是静止图像以足够快的速度连续呈现的技巧,以至于我们的眼睛和大脑将我们看到的解释为运动。在接下来的两个部分中解释了如何绘制事物的确切机制。请记住,有两个动画正在进行:视频的呈现和视频在盒子中的位置。在本节中,我将讨论盒子中视频的位置。

在 HTML 和 JavaScript 中获得动画的一种方法是使用setInterval函数。这个函数用两个参数调用。第一个是我们希望定期调用的函数的名称,第二个表示每次调用函数之间的时间间隔。时间的单位是毫秒。

我首先描述了画框的例子。移动元素的例子是相似的,我将描述不同之处。以下语句位于init函数中,用于设置动画:

setInterval(drawscene,50);

参数drawscene指的是将完成大部分工作的函数。它从视频中画出一帧,然后画出蒙版,我称之为矩形甜甜圈。稍后我将描述该操作。setInterval呼叫中的50代表 50 毫秒。这意味着每隔 50 毫秒(或每秒 20 次),就会调用drawscene函数。大概,drawscene会做需要做的事情,在一个新的位置显示视频剪辑。您可以尝试间隔持续时间。

如果您想增强这个应用程序或者构建另一个停止动画有意义的应用程序,您可以为调用setInterval声明一个局部变量(姑且称之为tid)并使用语句

tid = setInterval(drawscene,50);

在您希望停止动画,或者更正式地说,停止间隔计时事件时,您可以编写代码

clearInterval(tid);

如果有多个计时事件,可以将每个事件的输出分配给一个新变量。注意不要用同一个函数多次调用setInterval。这样做的效果是添加新的计时事件并多次调用该函数。不是改变你时钟上的闹钟功能,而是设置多个时钟。

drawscene函数的许多细节将在下一节中描述,但是我在这里描述两个关键任务。一个是擦除画布,另一个是确定视频剪辑的下一个位置。擦除整个画布的语句是

ctx.clearRect(0,0,cwidth,cheight);

注意,它使用基于窗口尺寸计算的cwidthcheight值。

弹跳的模拟由一个名为checkPosition的函数执行。虚拟球的位置由变量ballxbally定义。(ballx,bally)位置是视频的左上角。该运动也被称为位移,由变量ballvxballvy定义。这两个变量分别称为水平位移和垂直位移。

注意

当我可以只用一个函数时,为什么我要用两个函数,drawscenecheckPositioncheckposition功能仅由drawscene调用。答案是,对我来说,这些操作看起来像是不同的操作,为不同的任务创建不同的函数是一个很好的实践。

checkPosition函数的目的是通过将ballxbally分别设置为包含ballvxballvy,的表达式.来重新定位虚拟球。在适当的时候,我的代码必须改变ballvxballvy的符号。代码的工作方式是尝试新值(见函数中的nballxnbally),然后设置ballxbally。通过改变下一个间隔的适当水平或垂直调整,改变位移值的符号具有使球反弹的效果。如果球打在一个角上,那么两个位移值都会改变符号,但通常只有一个会改变。

现在的任务是决定何时反弹。你需要接受,就计算机而言,没有球,没有弹跳或其他,也没有墙。只有计算。此外,计算是在离散的时间间隔内进行的。没有连续的运动。虚拟球从一个位置跳到另一个位置。轨迹看起来很平滑,因为跳跃足够小,我们的眼脑将图片解释为连续运动。由于墙壁是在视频之后绘制的(这将在后面解释),因此效果是虚拟球在改变方向之前接触并稍微走到墙壁后面。

我的方法是为ballxbally设置试验值或替代值,并基于这些值进行计算。你可以从逻辑上把它想成是问如果视频球被移动了,它会超出任何一面墙吗?如果是这样的话,重新调整到刚好碰到墙,并改变适当的位移值。新的位移值不会立即使用,但会成为下一次迭代计算的一部分。如果试验值不在墙处或墙外,则保持试验值不变,并保持相应的位移值不变。然后将ballxbally更改为可能调整的替代值。

videobounceC程序的checkPosition功能的功能定义为

function checkPosition() {
        var nballx = ballx + ballvx +.5*videow;
        var nbally = bally + ballvy +.5*videoh;
  if (nballx > cwidth) {
         ballvx =-ballvx;
         nballx = cwidth;
  }
  if (nballx < 0) {

        nballx = 0;
        ballvx = -ballvx;
  }
  if (nbally > cheight) {
        nbally = cheight;
        ballvy =-ballvy;
  }
  if (nbally < 0) {
       nbally = 0;
       ballvy = -ballvy;
  }
  ballx = nballx-.5*videow;
  bally = nbally-.5*videoh;
}

我决定为视频元素示例更改函数名称drawscene。我想强调的是,视频是移动的,而不是画出来的。drawscene的对应者是moveVideo。这是在setInterval调用中引用的函数。

function moveVideo(){
        checkPosition();
        v.style.left = String(ballx)+"px";
        v.style.top = String(bally)+"px";
}

checkPosition函数进行计算以确定何时发生弹跳。

function checkPosition() {
      var nballx = ballx + ballvx;
      var nbally = bally + ballvy;

  if ((nballx+v.width) > cwidth) {
       ballvx =-ballvx;
       nballx = cwidth-v.width;
  }
  if (nballx < 0) {

       nballx = 0;
       ballvx = -ballvx;
  }
  if ((nbally+v.height) > cheight) {
       nbally = cheight-v.height;
       ballvy =-ballvy;
  }
  if (nbally < 0) {
    nbally = 0;
       ballvy = -ballvy;
  }
  ballx = nballx;
  bally = nbally;

}

请注意,videobounceC版本将ballx + ballvx + .5*videowcwidth进行比较,而videobounceEwithClipPathballx + ballvx + videowcwidth进行比较。这意味着与右墙相比,videobounceE程序将更快地强制反弹——也就是说,更快地转向。对底壁的检查也是如此。我这样做是为了避免涉及自动滚动的问题。视频元素并不局限于画布,所以如果它从画布下面移出,它就是文档的一部分并被显示。因为新的显示比窗口大,这导致滚动。滚动条会出现,虽然你什么也看不到,但我不喜欢这种效果。如果您从一个较小的窗口开始,并在程序执行过程中将其放大,您会看到如图 3-7 所示的内容。视频元素也可以移动得如此之快,以至于它逃离了盒子。这是留给读者的一个有趣的练习。

img/272384_2_En_3_Fig7_HTML.jpg

图 3-7

具有较少限制检查的视频元素弹跳

为了避免这种情况,您将看到我更改了对视频元素应用程序的检查。这样做的缺点是视频球几乎不接触右侧和底部的墙壁。

这些效果在画布上绘制视频应用程序videobounceC中不会发生的原因是,在画布上用画布之外的坐标进行绘制没有可见的效果。你可以回头看第一章,图 1-6 ,看一个画“线外”的例子,在画布外什么也不画。

注意

可能有其他方法来避免滚动问题。这不会阻止图 3-7 中所示的不美观。也许可以防止浏览器中的滚动。阻止用户滚动是可能的,但是自动滚动似乎是一个更大的挑战。

在画布上或作为可移动元素的视频画框

我现在描述两个不同的实现:一个是在画布上绘制的视频素材,另一个是在文档中移动的视频元素。

在画布上绘制的视频

正如我前面提到的,HTML5 确实提供了在画布上绘制视频的功能,就像绘制图像一样。这实际上是用词不当。视频剪辑由称为的静止图像序列组成。帧速率有所不同,但通常是每秒 15 到 32 帧,因此您可以理解视频文件往往很大。视频使用不同类型的编码进行存储,每种编码都可能在质量和存储大小方面做出不同的技术权衡。我们不需要关心这些技术细节,但是可以把视频看作一系列的帧。播放视频包括按顺序显示帧。在drawImage命令中发生的事情是,视频剪辑的当前帧是在画布上绘制的图像。如果通过定时间隔事件执行该操作,则观众将在每个时间间隔看到一帧。不能保证显示的图像是视频剪辑中的连续帧,但如果速度足够快,绘制的帧将足够接近实际序列,我们的眼睛和大脑会将其视为视频剪辑的现场动作。

伪代码中的命令是

ctx.drawImage(video element, x position, y position, width, height);

这个命令,形式上是ctx canvas 上下文的一个方法,提取对应于视频的当前帧的图像,并在 x 和 y 值处用指定的宽度和高度绘制它。如果图像没有指定的宽度和高度,则缩放图像。这种情况不会发生。

目标是使旅行视频剪辑像一个球。对于这个应用程序,这意味着我们要屏蔽掉矩形视频剪辑中心的所有部分,只保留一个圆。我通过制作一个旅行面具来实现这个目标。遮罩是画布中的一幅画。由于我想将 video 元素放在 canvas 元素上,并通过在从视频剪辑中提取的图像上绘制一条路径来定位一个形状,所以我使用 CSS 指令来使用绝对定位来定位 video 和 canvas。我希望反向按钮在画布上。这些指令完成了这个任务:

#vid {position:absolute; display:none;}
#canvas {position:absolute; z-index:10; top:0px; left:0px;}
#revbtn {position:absolute; z-index:20;}

记住分层如何工作的一种方法是将 z 轴想象成从屏幕出来。设置为较高值的元素位于设置为较低值的元素之上。画布的topleft属性都被设置为 0 像素,以将画布的左上角定位在窗口的左上角。

注意

在 JavaScript 中引用或修改 z-index 时,其名称为zIndex。希望您能理解为什么名称 z-index 不起作用:连字符(-)会被解释为减号运算符。

视频元素在 style 指令中被设置为不显示。这是因为作为一个元素本身,它不应该显示任何东西。相反,使用以下语句将当前帧的内容绘制到画布上:

ctx.drawImage(v, ballx, bally, videow,videoh);

ballxbally值在init函数中初始化,并如上一节所述递增。视频剪辑的宽度和高度已被修改以适合窗口大小。

理解这一点的一种方法是想象视频正在屏幕外的某个地方播放,浏览器可以访问该信息,因此它可以提取当前帧以在drawImage方法中使用。

可移动视频元素

videobounceE应用程序在文档上移动实际的视频元素。视频元素并不绘制在画布上,而是 HTML 文档中的一个独立元素。为了使视频看起来像一个圆圈,我使用了剪辑路径功能。样式指令包括

#vid {position:absolute; display:none; z-index: 1;}
#canvas {position:absolute; z-index:10; top:0px; left:0px;}
#revbtn {position:absolute; z-index:20;}

init函数中,我包含了调整视频尺寸以适应屏幕的代码。

v = document.getElementById("vid");

    aspect= v.videoWidth/v.videoHeight;
    v.width = Math.min(v.videoWidth,.5*cwidth);
    v.height = v.width/aspect;
    v.height = Math.min(v.height,.5*cheight);
    v.width = aspect*v.height;
    videow = v.width;
    videoh = v.height;

然后我计算出一个圆的半径是视频宽度和视频高度的一半中较小的一个。我的代码使用这个数字产生一个以"px"结尾的字符串。该字符串用于设置clipPath值。

    amt = .5*Math.min(videow,videoh);
    amtS = String(amt)+"px";
    v.style.clipPath="circle("+amtS+" at center)";

四处移动视频元素需要使视频可见并开始播放视频。还需要定位。通过参考style.leftstyle.top来定位视频元素。此外,lefttop属性的设置必须是代表数字的字符串形式,后跟代表像素的字符串"px"。下面的代码

        v.style.left = String(ballx)+"px";
        v.style.top = String(bally)+"px";
        v.play();
        v.style.visibility = "visible";
        v.style.display = "block";

init功能中执行。还要注意,视频的初始位置被更改为初始的ballxbally值。数值需要被转换成字符串,然后"px"需要被连接到字符串的末尾。这是因为 HTML/JavaScript 假设样式属性是字符串。我编写了相同的代码,将视频元素的topleft属性设置为与movevideo函数中的ballxbally相对应的值。取代ctx.drawImage语句的语句是

        v.style.left = String(ballx)+"px";
        v.style.top = String(bally)+"px";

最后,绘制矩形(方框)。不需要再次绘制。调用setInterval函数来移动视频元素。

       ctx.strokeRect(0,0,cwidth,cheight);  //box
       setInterval(moveVideo,50);

这个程序不停止运动,所以我不需要存储所谓的定时事件标识符。你可以考虑增强程序来提供一种停止运动的方法。我用它产生了一些数字。videobounceCvideobounceEwithClipPath的所有代码都将在“构建应用程序并使之成为您自己的”一节中列出,并附有注释。

旅行面具

遮罩的目的是遮蔽(即遮盖)视频中除中心圆圈以外的所有部分。样式指令确保我可以使用相同的变量——即ballxbally——在两种情况下引用视频和遮罩:绘制视频和移动视频元素。所以现在的问题是如何制作一个带有圆孔的矩形甜甜圈面具。

我通过编写代码绘制两条路径并用白色填充它们来实现这一点。由于面具的形状很难想象,我创造了两个图形来展示它是什么。图 3-8 显示了两条路径的轮廓。

img/272384_2_En_3_Fig8_HTML.jpg

图 3-8

遮罩路径的轮廓

图 3-9 显示了轮廓和填充的路径。两条小的水平路径将不存在,因为代码中没有笔画。

img/272384_2_En_3_Fig9_HTML.jpg

图 3-9

填充和描边后的蒙版路径

现在,实际路径只有填充,填充颜色是白色。你需要想象这两个白色的形状在视频上面移动。遮罩的作用是掩盖视频剪辑的大部分。可以说,画布上没有绘画的部分是透明的,视频剪辑内容可以透过它们显示出来。换句话说,画布在视频元素的上面,但它相当于一片玻璃。没有绘制任何内容的每个像素都是透明的。

第一条路径从左上角开始,然后向右,向下到中间点,最后向左回到中心,但是停止了。该路径是一个半圆弧。指示圆弧方向的最后一个参数是true表示逆时针方向。该路径以一条线延伸到左边缘,然后回到起点。第二条路径从左边缘的中间开始,向下到左下角,到右下角,向上移动到右侧的中间,然后移动到左侧。这次圆弧的方向参数值为false,表示圆弧为顺时针方向。这条路在它开始的地方结束了。然而,我确实需要做一些调整,以防止框架的某些边缘出现。这些由编码中出现的+2 和-2 表示。

ctx.beginPath();
 ctx.moveTo(ballx,bally);
 ctx.lineTo(ballx+videow+2,bally);
 ctx.lineTo(ballx+videow+2,bally+.5*videoh+2);
 ctx.lineTo(ballx+.5*videow+ballrad, bally+.5*videoh+2);
 ctx.arc(ballx+.5*videow,bally+.5*v.height,ballrad,0,Math.PI,true);
 ctx.lineTo(ballx,bally+.5*v.height);
 ctx.lineTo(ballx,bally);
 ctx.fill();
 ctx.closePath();

 ctx.beginPath();
 ctx.moveTo(ballx,bally+.5*v.height);
 ctx.lineTo(ballx,bally+v.height);
 ctx.lineTo(ballx+v.width+2,bally+v.height);
 ctx.lineTo(ballx+v.width+2,bally+.5*v.height-2);
 ctx.lineTo(ballx+.5*v.width+ballrad,bally+.5*v.height-2);
 ctx.arc(ballx+.5*v.width,bally+.5*v.height,ballrad,0,Math.PI,false);
 ctx.lineTo(ballx,bally+.5*v.height);
 ctx.fill();
 ctx.closePath();

你可以跟随我的“英语”描述来看看它是如何工作的。

顺便说一下,我最初的尝试是绘制一个由四个边组成的路径,代表外部的矩形,然后在中间画一个圆。这适用于某些浏览器,但不适用于其他浏览器。

对于videobounceC应用程序,遮罩位于画布上绘制的视频帧的顶部,因为两条白色填充路径是在drawImage语句从视频中绘制一帧之后绘制的。下一章将展示一个聚光灯在 Google Maps 的地图上移动,并使用 JavaScript 修改 z 索引。z 轴从屏幕中出来,可以用来改变靠近观察者的部分和远离观察者的部分。我将在下一节提到这一点。

用户界面

两个版本的videobounce项目的用户界面只包含一个用户动作:用户可以反转行进方向。按钮由主体中的元素定义:

<button id="revbtn" onClick="reverse();">Reverse </button><br/>

onClick设置的作用是调用名为reverse的函数。该函数定义为改变水平和垂直位移的符号:

function reverse() {
        ballvx = -ballvx;
        ballvy = -ballvy;
}

任何用户界面都有一个重要的考虑因素。你需要确保它是可见的。这是通过以下样式指令实现的:

#revbtn {position:absolute; z-index:20;}

z-index 将按钮放置在画布的顶部,而画布又位于视频的顶部。

在解释了可用于满足弹跳视频的关键要求的各个 HTML5、CSS 和 JavaScript 特性之后,我现在将展示两个弹跳视频应用程序中的代码:带有在帧上移动的遮罩的绘图帧和由剪辑路径遮罩的视频元素。

构建应用程序并使之成为您自己的应用程序

模拟视频剪辑球在二维盒子中反弹的两个应用程序包含类似的代码,生成轨迹图片的程序也是如此。移动视频元素的时间更短,因为剪辑路径样式功能有效地产生了遮罩。裁剪路径样式功能有其他的可能性,包括多边形,所以这是值得研究的。下面是应用程序的快速摘要。视频应用总结如下:

  1. init:初始化,包括适应适应窗口,设置调用显示新场景的定时事件。

  2. drawscene

    1. 擦除画布。

    2. 使用moveandcheck确定视频(虚拟球)的新位置。

    3. 在画布上的指定位置从视频中绘制图像。

    4. 在画布上绘制路径,创建旅行(矩形甜甜圈)面具。

    5. 画盒子。

  3. moveVideo

    1. 使用checkPosition确定视频的新位置。

    2. checkPosition:检查虚拟球是否会碰到墙壁。如果是这样,请更改适当的置换值。

    3. 将视频元素定位在当前位置。

表 3-1 描述了图框应用程序的调用/被调用者和调用关系。

表 3-1

videobounceC 程序中的函数

|

功能

|

调用/调用

|

来电

|
| --- | --- | --- |
| init | 由<body>标签中的onLoad属性的动作调用 |   |
| drawscene | 通过init中发出的setInterval命令的动作调用 | moveAndCheck |
| moveAndCheck | 在drawscene中调用 |   |
| reverse | 通过按钮中的onClick动作调用 |   |

表 3-2 显示了videobounceC应用程序的代码,它以设定的时间间隔在画布上绘制视频的当前帧。

表 3-2

videobounceC 应用程序的完整代码

|

代码行

|

描述

|
| --- | --- |
| <!DOCTYPE html> | 页眉 |
| <html> | 开始html标签 |
| <head> | 开始head标签 |
| <title>Video frames bounce</title> | 完整标题 |
| <meta charset="UTF-8"> | 其中元素 |
| <style> | 开场风格 |
| #vid {position:absolute; display:none;} | 设置视频的定位;将显示设置为无;视频元素从不出现 |
| #canvas {position:absolute; z-index:10; top:0px; left:0px;} | 将定位设置为绝对,并将位置设置为左上角;设置 z-index,使其位于反转按钮下方 |
| #revbtn {position:absolute; z-index:20;} | 将定位设置为绝对和 z-index,使其位于画布上 |
| </style> | 关闭样式 |
| <script type="text/javascript"> | 开始script标签 |
| var canvas1; |   |
| var ctx; | 用于保存画布上下文;用于所有绘图 |
| var cwidth; | 用于保持画布宽度 |
| var cheight; | 用于保持画布高度 |
| var videow; | 用于保存调整后的视频宽度 |
| var videoh; | 用于保持调整后的视频高度 |
| var ballrad = 50; | 设置球半径 |
| var ballx = 50; | 球的初始水平坐标 |
| var bally = 60; | 球的初始垂直坐标 |
| var maskrad; | 用于遮罩半径 |
| var ballvx = 2; | 初始球水平位移 |
| var ballvy = 4; | 初始球垂直位移 |
| var v; | 将保存视频元素 |
| var videow; |   |
| var videoh; |   |
| function restart() { | 重启的功能头 |
| v.currentTime=0; | 将视频中的位置重置为开始 |
| v.play(); | 播放视频 |
| } | 关闭restart功能 |
| function init(){ | init的功能头 |
| canvas1 = document.getElementById('canvas'); | 为画布设置参考 |
| ctx = canvas1.getContext('2d'); | 为画布上下文设置引用 |
| canvas1.width = window.innerWidth; | 设置画布宽度以匹配当前窗口宽度 |
| cwidth = canvas1.width; | 设置变量 |
| canvas1.height = window.innerHeight; | 设置画布高度以匹配当前窗口高度 |
| cheight = canvas1.height; | 设置变量 |
| v = document.getElementById("vid"); | 设置对视频元素的引用 |
| aspect =  v.videoWidth/v.videoHeight; | 计算纵横比;videoWidthvideoHeight值描述原始视频,不会改变 |
| v.width = Math.min(v.videoWidth,.5*cwidth); | 设置视频宽度 |
| v.height = v.width/aspect; | 调整v.height以保持比例 |
| v.height = Math.min(v.height,.5*cheight); | 设置视频高度 |
| v.width = aspect*v.height; | 调整v.width以保持比例 |
| window.onscroll = function () {``window.scrollTo(0,0);``}; | 如果出现滚动条,则停止任何用户滚动操作 |
| videow = v.width; | 设置变量 |
| videoh = v.height; | 设置变量 |
| ballrad = Math.min(.5*videow,.5*videoh); | 修改ballrad |
| ctx.lineWidth = ballrad; | 设置绘制方框的线宽 |
| ctx.strokeStyle ="rgb(200,0,50)"; | 将颜色设置为红色 |
| ctx.fillStyle="white"; | 将蒙版的填充样式设置为白色 |
| v.play(); | 开始视频 |
| setInterval(drawscene,50); | 设置定时事件 |
| } | 关闭init功能 |
| function drawscene(){ | drawscene的功能头 |
| ctx.clearRect(0,0,cwidth,cheight); | 擦除画布 |
| checkPosition(); | 检查下一次移动是否在墙壁上,如果是,调整位移和位置;否则,就采取行动 |
| ctx.drawImage(v,ballx, bally, videow,videoh); | 在指定位置从视频中绘制图像 |
| ctx.beginPath(); | 开始遮罩上半部分的路径 |
| ctx.moveTo(ballx,bally); | 移动到起点 |
| ctx.lineTo(ballx+videow+2,bally); | 水平移动 |
| ctx.lineTo(ballx+videow+2,bally+.5*videoh+2); | 向下移动到一半 |
| ctx.lineTo(ballx+.5*videow+ballrad, bally+.5*videoh+2); | 移动到洞口的起点 |
| ctx.arc(ballx+.5*videow,bally+.5*videoh,ballrad,0, Math.PI,true); | 做半圆弧 |
| ctx.lineTo(ballx,bally+.5*videoh); | 向左移动 |
| ctx.lineTo(ballx,bally); | 移动到开始 |
| ctx.fill(); | 填充面具的白色顶部 |
| ctx.closePath(); | 关闭面罩的顶部 |
| ctx.beginPath(); | 开始遮罩的底部 |
| ctx.moveTo(ballx,bally+.5*videoh); | 移动到开始蒙版的底部;移动到左边的中间点 |
| ctx.lineTo(ballx,bally+videoh); | 向下移动到左下方 |
| ctx.lineTo(ballx+videow+2,bally+videoh); | 移到右边的角落 |
| ctx.lineTo(ballx+videow+2,bally+.5*videoh-2); | 移到右边的中间 |
| ctx.lineTo(ballx+.5*videow+ballrad,bally+.5*videoh-2); | 移动到遮罩孔的开始处 |
| ctx.arc(ballx+.5*videow,bally+.5*videoh,ballrad,0,Math.PI,false); | 做半圆弧 |
| ctx.lineTo(ballx,bally+.5*videoh); | 向右移动 |
| ctx.fill(); | 填充面具的白色底部 |
| ctx.closePath(); | 关闭面具的底部 |
| ctx.strokeRect(0,0,cwidth,cheight); | 画这个盒子 |
| } | 关闭drawscene功能 |
| function checkPosition() { | checkPosition功能的标题 |
| var nballx = ballx + ballvx+.5*videow; | 设置 x 的试用值 |
| var nbally = bally +ballvy+.5*videoh; | 设置 y 的试用值 |
| if (nballx > cwidth) { | 与右侧墙壁相比,击中时 |
| ballvx =-ballvx; | 改变水平位移的符号 |
| nballx = cwidth; | 将试验值设置在正确的墙上 |
| } | 关闭条款 |
| if (nballx < 0) { | 与左侧墙壁相比,击中时 |
| nballx = 0; | 将试验值准确设置在左侧墙上 |
| ballvx = -ballvx; | 改变水平位移的符号 |
| } | 关闭条款 |
| if (nbally > cheight) { | 与底壁相比,击中时 |
| nbally = cheight; | 将试验值设置为精确的高度 |
| ballvy =-ballvy; | 改变垂直位移的符号 |
| } | 关闭条款 |
| if (nbally < 0) { | 与击中的顶壁比较 |
| nbally = 0; | 将试验值更改为正好在顶壁处 |
| ballvy = -ballvy; | 改变垂直位移的符号 |
| } | 关闭条款 |
| ballx = nballx-.5*videow; | 使用试验值设置ballx,偏移到左上角,而不是中心 |
| bally = nbally-.5*videoh; | 使用试验值设置bally,偏移到左上角,而不是中心 |
| } | 关闭checkPosition功能 |
| function reverse() { | 按钮操作的功能标题 |
| ballvx = -ballvx; | 改变水平位移的符号 |
| ballvy = -ballvy; | 改变垂直位移的符号 |
| } | 关闭反向功能 |
| </script> | 结束脚本标记 |
| </head> | 结束标题标签 |
| <body onLoad="init();"> | 打开正文标记;设置对init的呼叫 |
| <video id="vid" loop="loop" preload="auto" muted> | 视频元素头;注意静音属性 |
| <source src="joshuahomerun.mp4" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'> | MP4 视频的来源 |
| <source src="joshuahomerun.webmvp8.webm" type='video/webm; codec="vp8, vorbis"'> | WEBM 视频的来源 |
| <source src="joshuahomerun.theora.ogg type='video/ogg; codecs="theora, vorbis"'> | OGG 视频的来源 |
| Your browser does not accept the video tag. | 针对不兼容浏览器的消息 |
| </video> | 关闭video标签 |
| <button id="revbtn" onClick="reverse();">Reverse </button><br/> | 观众反转方向的按钮 |
| <canvas id="canvas"> | 开始canvas标签 |
| This browser doesn't support the HTML5 canvas element. | 针对不兼容浏览器的消息 |
| </canvas> | 结束canvas标签 |
| </body> | 结束body标签 |
| </html> | 结束html标签 |

这个应用程序的第二个版本移动视频元素,而不是在画布上绘制视频的当前帧。我的研究表明,它在执行时可能会使用较少的计算机资源。表 3-3 显示了函数关系。

表 3-3

videobounceEwithClipPath 程序的函数关系

|

功能

|

调用/调用

|

来电

|
| --- | --- | --- |
| init | 由<body>标签中的onLoad属性的动作调用 |   |
| moveVideo | 通过init中发出的setInterval命令的动作调用 | checkPosition |
| checkPosition | 在moveVideo中调用 |   |
| reverse | 通过按钮中的onClick动作调用 |   |

表 3-4 显示了重新定位视频元素而不是从视频中绘制帧的程序版本代码。我省略了代码语句相同时的描述性注释。请记住,这两种方法的主要区别在于,弹跳视频元素程序不需要为每次迭代生成矩形圆环遮罩,也不需要擦除然后重新绘制方框。

表 3-4

VideobounceEwithClipPath 程序的完整代码

|

代码行

|

描述

|
| --- | --- |
| <!DOCTYPE html> |   |
| <html> |   |
| <head> |   |
| <title>Video element bounce</title> |   |
| <meta charset="UTF-8"> |   |
| <style> |   |
| #vid {position:absolute; display:none; z-index: 1; | 需要设置定位和 z 索引,因为显示设置将被更改以使元素可见 |
| } | 结束指令 |
| #canvas {position:absolute; z-index:10; top:0px; left:0px;} | 这将在视频的上面和按钮的下面 |
| #revbtn {position:absolute; z-index:20;} |   |
| </style> |   |
| <script type="text/javascript"> |   |
| var ctx; |   |
| var cwidth; |   |
| var cheight; |   |
| var ballrad = 50; |   |
| var ballx = 80; | 起点是任意的 |
| var bally = 80; | 起点是任意的 |
| var maskrad; |   |
| var ballvx = 2; |   |
| var ballvy = 4; |   |
| var v; |   |
| function init(){ |   |
| canvas1 = document.getElementById('canvas'); |   |
| ctx = canvas1.getContext('2d'); |   |
| canvas1.width = window.innerWidth; |   |
| cwidth = canvas1.width; |   |
| canvas1.height = window.innerHeight; |   |
| cheight = canvas1.height ; |   |
| window.onscroll = function () { |   |
| window.scrollTo(0,0); |   |
| }; |   |
| v = document.getElementById("vid"); |   |
| aspect = v.videoWidth/v.videoHeight; |   |
| v.width = Math.min(v.videoWidth,.5*cwidth); |   |
| v.height =   v.width/aspect; |   |
| v.height = Math.min(v.height,.5*cheight); |   |
| v.width = aspect * v.height; |   |
| videow = v.width; |   |
| videoh = v.height; |   |
| amt = .5*Math.min(videow,videoh); | 使用较小的值计算半径 |
| amtS = String(amt)+"px"; | 变成以"px"结尾的字符串 |
| v.style.clipPath="circle("+amtS+" at center)"; | 设置clipPath,有效屏蔽视频元素为圆形 |
| ballrad = Math.min(50,.5*videow,.5*videoh); |   |
| ctx.lineWidth = ballrad; |   |
| ctx.strokeStyle ="rgb(200,0,50)"; |   |
| ctx.fillStyle="white"; |   |
| v.style.left = String(ballx)+"px"; |   |
| v.style.top = String(bally)+"px"; |   |
| v.play(); |   |
| v.style.display = "block"; | 使视频元素可见 |
| ctx.strokeRect(0,0,cwidth,cheight); | 绘制框;请注意,这只需要绘制一次 |
| setInterval(moveVideo,50); |   |
| } |   |
| function moveVideo(){ | setInterval中引用的函数的标题 |
| checkPosition(); | 检查下一个位置;使用全局变量 |
| v.style.left = String(ballx)+"px"; | 设置元素的水平位置 |
| v.style.top = String(bally)+"px"; | 设置元素的垂直位置 |
| } |   |
| function checkPosition() { | checkPosition的表头;计算新位置并检查下一次迭代是否有反弹 |
| var nballx = ballx + ballvx; | 试用值 |
| var nbally = bally +ballvy; | 试用值 |
| if ((nballx+videow) > cwidth) { | 添加总宽度并比较 |
| ballvx =-ballvx; | 改变水平位移的符号 |
| nballx = cwidth-videow; | 设置到精确位置 |
| } |   |
| if (nballx < 0) { |   |
| nballx = 0; |   |
| ballvx = -ballvx; |   |
| } |   |
| if ((nbally+videoh) > cheight) { | 比较总长度 |
| nbally = cheight-videoh; | 设置到精确位置 |
| ballvy =-ballvy; | 改变垂直位移的符号 |
| } |   |
| if (nbally < 0) { |   |
| nbally = 0; |   |
| ballvy = -ballvy; |   |
| } |   |
| ballx = nballx; | 设置到试验位置,可能已调整 |
| bally = nbally; | 设置到试验位置,可能已调整 |
| } |   |
| function reverse() { |   |
| ballvx = -ballvx; |   |
| ballvy = -ballvy; |   |
| } |   |
| </script> |   |
| </head> |   |
| <body onLoad="init();" > |   |
| <video id="vid" loop="loop" preload="auto" muted> |   |
| <source src="joshuahomerun.webmvp8.webm" type='video/webm; codec="vp8, vorbis"'> |   |
| <source src="joshuahomerun.mp4" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'> |   |
| <source src="joshuahomerun.theora.ogg" type='video/ogg; codecs="theora, vorbis"'> |   |
| Your browser does not accept the video tag . |   |
| </video> |   |
| <button id="revbtn" onClick="reverse();">Reverse </button><br/> |   |
| <canvas id="canvas" > |   |
| This browser doesn't support the HTML5 canvas element. |   |
| </canvas> |   |
| </body> |   |
| </html> |   |

为了生成图 3-2 ,我通过修改videobounceC中的drawscene制作了trajectory函数。因为我想让这个圆的大小与被遮罩的视频剪辑相似,所以在设置好视频的宽度和高度后,我临时给videobounceC函数添加了一个alert语句。然后我用这些值运行程序:

         v.width = Math.min(v.videoWidth/3,.5*cwidth);
         v.height = Math.min(v.videoHeight/3,.5*cheight);
         alert("width "+v.width+" height "+v.height);

然后,我使用值 106 和 80 作为轨迹程序中的videowvideoh值。

让应用程序成为你自己的

让这个应用成为你自己的第一个方法是使用你自己的视频。当显示为小圆圈时,您确实需要找到可以接受的内容。您还可以探索使用clipPath功能。如前所述,您需要使用不同的视频编解码器制作版本。下一步是添加其他用户界面动作,包括改变水平和垂直速度,就像在《HTML5 基本指南中的弹跳球项目中所做的那样。另一组增强功能是添加视频控件。视频控件可以是视频元素的一部分,但我不认为这对于需要小且移动的视频剪辑有用!但是,您可以使用模仿“反向”按钮的按钮来实现自己的控件。例如,语句

v.pause();

确实会暂停视频。

可以引用或设置属性v.currentTime来控制视频剪辑中的位置。你在第一章中看到了范围输入类型是如何工作的,所以考虑构建一个滑块输入元素来调整视频。

你可以决定改变我的方法来适应窗户的尺寸。一种替代方法是改变视频剪辑的尺寸以保持纵横比。另一种方法是一直改变视频尺寸。这意味着视频尺寸和画布方向将始终成比例。还有另一种选择,虽然我认为这将是令人不安的,是在每个时间间隔引用窗口的尺寸,并在画布中进行更改,每次都可能是视频。有一个事件可以插入到body标签中:

<body onresize="changedims();" ... >

这段代码假设您已经定义了一个名为changedims的函数,该函数包含了当前init函数中的一些语句,用于提取window.innerWidthwindow.innerHeight属性来设置画布和视频的尺寸。

更一般地说,本章的目的是向您展示以动态方式将视频融入项目的方法,包括在屏幕上的位置和时间。特别是,可以将视频播放与画布上的绘图相结合,以获得令人兴奋的效果。

存在屏幕保护程序,其中屏幕被类似于轨迹程序的弹跳物体填满。您可以更改drawscene功能来产生不同的形状。此外,正如我之前提到的,你可以应用在html 5基本指南中解释的技术来提供浏览者的动作。关于范围输入(滑块)的使用,可以参考本书第一章。还有一种可能性是为查看者提供一种方法,使用颜色的输入类型来改变圆形(或您设计的其他形状)的颜色。Opera 浏览器提供了颜色选择器选项。

测试和上传应用程序

正如已经提到的,但值得重复,你需要获得一个合适的视频剪辑。在撰写本书时,您需要使用 Miro 等程序来制作 WEBM、MP4 和 OGG 版本,因为浏览器可能会识别不同的视频编码(编解码器)。这种情况可能会改变。同样,如果您满足于只在一个浏览器上实现,您可以检查哪个视频编码适用于该浏览器,并只准备一个视频文件。当您将此应用程序上传到您的服务器帐户时,视频文件和 HTML 文件需要位于您计算机上的同一文件夹中,也需要位于您服务器上的同一文件夹中。或者,您可以在源元素中使用完整的 web 地址或正确的相对地址。

自动播放策略可能会不断变化,因此您需要决定什么是您的应用程序的基本要素。

摘要

在本章中,您学习了操纵视频的不同方法。其中包括以下内容:

  • 将视频的当前帧作为图像绘制到画布上

  • 通过改变lefttop样式属性在屏幕上重新定位视频元素

  • 使用样式指令将视频、画布和按钮分层

  • 在画布上创建移动遮罩

  • 使用clipPath样式属性创建蒙版效果

  • 获取关于窗口尺寸的信息以使应用适应不同的情况

下一章将向您展示如何在 HTML5 项目中使用 Google Maps 应用程序编程接口(API)。该项目涉及使用画布和改变 z-index,使画布交替位于谷歌地图产生的材料之下和之上。

四、地图制作器(MapMaker):结合谷歌地图和画布

在本章中,您将学习以下内容:

  • 使用 Google Maps API 显示特定位置的地图

  • 使用透明度(也称为 alpha 或不透明度)和自定光标图标在画布上绘制图形

  • 通过管理事件和 z 索引级别,结合使用 Google Maps 和 HTML5 功能,为您的用户提供图形用户界面(GUI)

  • 计算两个地理位置之间的距离

介绍

本章的项目是一个涉及地理地图的应用程序。如今,许多应用程序都需要使用由他人或组织提供的应用程序编程接口(API)。本章将介绍 Google Maps API 的使用,并且是使用 Google Maps JavaScript 版本 3 API 的两章中的第一章。图 4-1 为开启画面。

img/272384_2_En_4_Fig1_HTML.jpg

图 4-1

地图聚光灯项目的打开屏幕

请注意位于地图中间和单词 College 顶部的红色小(手绘)x。在决定地图标记时,你面临一个权衡。较小的标记更难看到。更大和/或更复杂的标记更容易被看到,但是会阻挡地图的更多部分或者分散地图的注意力。这张地图以采购学院校园为中心。对于此程序,这是初始基准位置。基准位置用于计算距离。注意单选按钮显示三个选择,在三个大洲。中间的选择是开始的选择。

在地图上移动鼠标,如图 4-2 所示。

img/272384_2_En_4_Fig2_HTML.jpg

图 4-2

地图上的阴影/聚光灯

注意现在地图上的阴影和聚光灯的组合。地图的大部分被半透明的阴影覆盖。你需要相信我,这张截图是我在地图上移动鼠标时拍摄的。鼠标位置周围有一个圆圈,原始地图显示通过。在地图上移动时,光标不是标准的默认光标,而是我用一个代表紧凑型荧光灯的小图像创建的。

屏幕上的文字显示,从基地到地图上我点击的最后一个点的距离为 4.96 公里。所有这些位置的标记都是手绘的 x 。这个位置的纬度和经度用括号表示。

更改位置的界面是一组单选按钮(一次只能选择一个按钮)和一个标记为“更改”的按钮,当用户/观众/访问者决定进行更改时,可以单击该按钮。

这个项目的用户可以使用 Google Maps 提供的通用 GUI 功能。这包括用于放大和缩小的+和–按钮。图 4-3 展示了使用我的单选按钮切换到位于伦敦的 Springer Nature/a press Publishers 和 Google Maps +进行放大的结果。有可能放大得更远。注意哈利波特商店。

img/272384_2_En_4_Fig3_HTML.jpg

图 4-3

放大到伦敦

也可以将地图类型更改为卫星地图,并通过单击鼠标然后再次按下来平移地图。图 4-4 显示了切换到卫星和平移的效果。

img/272384_2_En_4_Fig4_HTML.jpg

图 4-4

缩小并向西移动,卫星图像

我用界面换到第三种可能:日本京都。如图 4-5 所示。

img/272384_2_En_4_Fig5_HTML.jpg

图 4-5

日本京都

接下来,我使用谷歌地图功能来改变京都的地图/地形。结果如图 4-6 所示。

img/272384_2_En_4_Fig6_HTML.jpg

图 4-6

日本京都基地,地形图

同样,请注意红色的小 x 表示基准位置,屏幕顶部的文本显示新基准位置的名称。

每个基地的位置由我确定的三个值中的每一个的纬度和经度值决定。我的代码不是“要求”Google Maps 按名称查找这些位置。如果你在谷歌或谷歌地图中输入“购买大学,纽约”和其他地点,你可能会得到稍微不同的结果。要使这个应用程序成为您自己的应用程序,您需要决定一组基本位置,并查找纬度和经度值。我将在下一节中提出实现这一点的方法。

以防你好奇,缩小到缩放比例上最远的位置会产生如图 4-7 所示的结果。这个投影展示了所谓的格陵兰问题。格陵兰并不比非洲大,但实际上大约是它的 1/14。

img/272384_2_En_4_Fig7_HTML.jpg

图 4-7

地图的最远视图

图 4-8 显示了接近最接近极限的地图。使用左上角的按钮,地图也被更改为卫星视图。

img/272384_2_En_4_Fig8_HTML.jpg

图 4-8

放大到可以探测到城市街区的地方,购买大学基地

最后,图 4-9 显示了放大到极限的地图。这本质上是在建筑层面。这栋建筑是自然和社会科学学院的所在地,也是我的办公室和电脑教室所在地。

img/272384_2_En_4_Fig9_HTML.jpg

图 4-9

一直放大

通过使用界面缩小,平移和再次放大,我可以确定从任何基本位置到世界上任何其他位置的距离!我还可以使用这个应用程序来确定任何位置的纬度和经度值。您需要知道纬度和经度,以便在第五章中更改或添加基本位置列表,以及确定项目的位置。我将在下一节回顾纬度和经度。

谷歌地图本身是一个非常有用的应用。本章和下一章将演示如何将该功能引入到您自己的应用程序中。也就是说,我们将谷歌地图的一般功能与我们可以使用 HTML5 和 JavaScript 开发的任何东西或几乎任何东西结合起来。

纬度和经度以及其他关键要求

这个项目最基本的要求是理解地理坐标系统。正如在画布上指定点或在屏幕上指定位置需要一个坐标系统一样,在地球上使用一个位置系统也是必要的。在过去的几百年里,经纬度系统已经得到了发展和标准化。这些值是角度,纬度表示与赤道的角度,经度表示与英国格林威治本初子午线的角度。后者是一个随意的选择,在 19 世纪晚期成为标准。

这里有一个北半球的偏差:纬度值从赤道的 0 度到北极的 90 度和南极的-90 度。类似地,经度值从格林威治本初子午线向东为正值,向西为负值。纬度与赤道平行,经度垂直。纬度通常被称为纬线,通常表现为水平线,经度被称为经线,通常表现为垂直线。这种定位是任意的,但是相当牢固地建立起来了。

我将使用十进制值,这是谷歌地图中默认显示的值,但你会看到度数、分钟(1/60 度)和秒(1/60 分钟)的组合。您没有必要记住经纬度值,但这有助于培养对系统的一些直观感觉。你可以通过我所说的“双向选择”来做到这一点首先,识别和比较你知道的地方的经纬度值,其次,选择值,看看它们是什么。例如,我的项目版本的基本值如下:

var locations = [
       [51.534467,-0.121631, "Springer Nature (Apress Publishers) London, UK"],
       [41.04796,-73.70539,"Purchase College/SUNY, NY, USA"],
       [35.085136,135.776585,"Kyoto, Japan"]
       ];

首先要注意的是纬度值相当接近,而经度值是负的,不太接近。因为我决定选择三大洲的地方作为基地,所以你需要进行实验,看看纬度和经度的微小变化会产生什么影响。你可以看到这三个地方都在赤道以北。在经度上,伦敦的值接近于零,接近格林威治本初子午线。你也可以注意到京都的经度是正的,其他的是负的。这些都有道理,但是你需要自己做实验来适应这些单元。

有许多方法可以找到特定位置的经纬度。您可以按如下方式使用谷歌地图:

img/272384_2_En_4_Fig10_HTML.jpg

图 4-10

在谷歌地图中获取经纬度值

  1. 从 Gmail 中的方形点阵调用谷歌地图,或者进入 http://.maps.google.com

  2. 在位置字段中输入一个位置。我在自由女神像打字。

  3. 点按以获得菜单:

    图 4-10 显示了一个出现在地图底部的小窗口。

点击这里得到一个显示经度和纬度的小窗口。

img/272384_2_En_4_Fig11_HTML.jpg

图 4-11

显示纬度和经度的框

另一种选择是使用 Wolfram Alpha ( www.wolframalpha.com ),如图 4-12 ,它提供了一种确定经纬度值以及许多其他东西的方法。

img/272384_2_En_4_Fig12_HTML.jpg

图 4-12

Wolfram Alpha 上的查询结果

请注意结果的格式。这是度/分/秒的格式,N 代表北方,W 代表西方。当我点击显示十进制按钮时,程序显示如图 4-13 所示的内容。

img/272384_2_En_4_Fig13_HTML.jpg

图 4-13

Wolfram Alpha 查询的十进制结果

请注意,经度仍然显示为 W 代表西方,而不是谷歌地图给出的负值。

按照我所说的“反方向走”,你可以把纬度和经度值输入谷歌地图。图 4-14 显示了放入 0.0 和 0.0 的结果。它是加纳南部海洋中的一个点。这是赤道上的一点和格林威治本初子午线上的

img/272384_2_En_4_Fig14_HTML.jpg

图 4-14

格林威治本初子午线处的赤道

我试图在格林威治本初子午线上找到英格兰的一个地方,并在 52.0 度的纬度上猜测时产生了图 4-15 所示的结果。

img/272384_2_En_4_Fig15_HTML.jpg

图 4-15

结果在格林威治本初子午线附近的一个地方

A 标记表示 Google 数据库中离请求位置最近的地方。我使用了 Drop LatLng 标记选项来显示准确的纬度和经度值。

该项目的关键需求始于使用指定的纬度和经度值将 Google Maps 引入 HTML5 应用程序的任务。一个额外的需求是在地图上产生阴影/聚光灯的组合来跟踪鼠标的移动。我还要求将鼠标的默认光标改为我自己选择的光标。

接下来,我添加了一个在地图上放置标记的要求,但还是用我选择的图形图标,而不是谷歌地图中标准的上下颠倒的泪珠。泪珠标记很好,但我的设计目标是与众不同,向您展示如何将您自己的创造力融入到应用程序中。

除了图形,我希望用户能够使用谷歌地图设备和我用 HTML5 构建的任何 GUI 功能。这都需要管理由 Google Maps API 设置的事件和使用 HTML5 JavaScript 设置的事件。对我想要制作的用户界面的事件的响应包括以下内容:

  • 用阴影/聚光灯图形跟踪鼠标移动

  • 通过在地图上放置一个 x 来响应点击

  • 保留对谷歌地图界面的相同响应(滑块、平移按钮、通过抓取地图进行平移)

  • 以适当的方式处理单选按钮和更改按钮

谷歌地图提供了一种确定位置之间距离的方法。因为我想设置这个项目来根据基本位置工作,所以我需要一种直接计算距离的方法。

这些是地图聚光灯项目的关键要求。现在我将解释我用来构建项目的 HTML5 特性。目标是使用 Google Maps 特性和 JavaScript 特性,包括事件,并且不让它们互相干扰。你可以把你学到的东西用于这个项目和其他项目。

HTML5、CSS 和 JavaScript 特性

map-maker 项目面临的挑战是引入谷歌地图,然后在外观和 GUI 操作方面一起使用地图、画布和按钮。我将描述基本的 Google Maps API,然后解释 HTML5 特性如何提供部分屏蔽和事件处理。

谷歌地图应用编程接口

谷歌地图 JavaScript API 第 3 版基础在 http://code.google.com/apis/maps/documentation/javascript/basics.html 有很好的文档。您现在不需要参考它,但是如果您决定构建自己的项目,它会对您有所帮助。这对于为移动设备开发应用程序特别有帮助。

大多数 API 都表现为相关对象的集合,每个对象都有属性(也称为属性)和方法。API 还可以包括事件和设置事件的方法。这就是谷歌地图 API 的情况。重要的对象是MapLatLngMarker。设置事件的方法是addListener,这可以用来设置对点击地图的响应。

使用谷歌地图 API 的第一步是去这个网站获取一个密钥: https://developers.google.com/maps/documentation/javascript/get-api-key

访问 API 的代码是修改以下内容,然后将其添加到 HTML 文档中:

<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
  type="text/javascript"></script>

注意

本文的第一版使用了被称为“无密钥”的 API。虽然我的原始代码仍然可以在我的领域中使用,但是 Google now 更加严格了。这些特性的使用是有限额的,尽管限额看起来很大,但是如果您计划在生产中使用,您需要研究文档。

下一步——如果你只想引入一个谷歌地图,这可能就是你所需要的——是建立一个对Map构造函数方法的调用。这方面的伪代码是

map = new google.maps.Map(place you are going to put the map, associative array with options);

请注意,将变量命名为map并没有什么坏处。

让我们一次一个地讨论这两个参数。放置地图的地方可以是 HTML 文档主体中定义的一个div。然而,我选择动态创建div。我是通过在body语句中设置onLoad属性,在一个以通常方式调用的init函数中使用代码来实现的。我还编写了代码来在div中创建一个canvas元素。代码是

        candiv = document.createElement("div");
        candiv.innerHTML = ("<canvas id="canvas" width="600" height="400">No canvas
        </canvas>");
        document.body.appendChild(candiv);
        can = document.getElementById("canvas");
                pl = document.getElementById("place");
                ctx = can.getContext("2d");

canplctx是全局变量,每个变量都可供其他函数使用。

注意

尽管我试图使用“在 HTML 文档中引入对谷歌地图的访问”这样的语言,但我对描述一个“制作”地图的功能感到内疚。Google Maps 连接是一个动态连接,其中 Google Maps 创建了所谓的“要显示的图块”。

Map方法的第二个参数是一个关联数组。关联数组有命名元素,没有索引元素。用于Map方法的数组可以指示缩放级别、地图中心和地图类型等。缩放级别可以从 0 到 18。0 级如图 4-7 所示。第 18 层可以显示建筑物。地图的类型有路线图、卫星图、混合图和地形图。这些都是用谷歌地图 API 中的常量来表示的。中心由一个类型为LatLng的值给出,如您所料,该值是使用代表纬度和经度值的十进制数构造的。使用关联数组意味着我们不必遵循参数的固定顺序,默认设置将应用于我们忽略的任何参数。

下面是我的makemap函数的开始。调用该函数时使用了两个数字来表示地图的中心纬度和经度。我的代码构造了一个名为blatlngLatLng对象,设置了保存地图规范的数组,然后构造了地图——也就是说,构造了 Google Maps 的门户。

function makemap(mylat,mylong) {
        var marker;
        blatlng = new google.maps.LatLng(mylat,mylong);

myOptions = {
          zoom: 12,
           center: blatlng,
           mapTypeId: google.maps.MapTypeId.ROADMAP
  };
map = new google.maps.Map(document.getElementById("place"), myOptions);

Map方法构造对 Google Maps 的访问,从 ID 为place的 div 中带有指定选项的地图开始。makemap功能继续,在地图中心放置一个标记。这是通过设置一个关联数组作为Marker方法的参数来实现的。图标标记将是我创建的图像,命名为rxmarker,使用我自己设计的图像,绘制成红色 x

marker = new google.maps.Marker({
 position: blatlng,
 title: "center",
 icon: rxmarker,
 map: map });

makemap函数中还有一个语句,但我将在后面解释其余的。

画布图形

我们希望用鼠标在地图上移动的图形类似于第三章中使用的蒙版,将矩形视频剪辑变成圆形视频剪辑。这两个面具都可以描述为类似于一个长方形的甜甜圈:一个带圆孔的长方形。我们使用两条路径绘制阴影/聚光灯的图形,就像上一章中视频的遮罩一样。然而,这两种情况有两个明显的不同:

  • 这个面具的确切形状各不相同。外部边界是整个画布,孔的位置与鼠标的当前位置对齐。这个洞会四处移动。

  • 面具的颜色不是纯色颜料,而是透明的灰色。

画布从谷歌地图的顶部开始。我通过编写设置 z 索引值的样式指令来实现这一点:

canvas {position:absolute; top: 165px; left: 0px; z-index:100;}
#place {position:absolute; top: 165px; left: 0px; z-index:1;}

第一个指令引用所有画布元素。这个 HTML 文档里只有一个。回想一下,z 轴从屏幕出来朝向观察者,所以较高的值在较低的值之上。还要注意,我们在 JavaScript 代码中使用了zIndex,在 CSS 中使用了z-index。JavaScript 解析器会将–符号视为减号操作符,因此对zIndex的更改是必要的。我需要编写代码来改变zIndex以获得我想要的这个项目的事件处理。

图 4-16 显示了在画布上绘制的阴影遮罩的一个例子。我已经使用单选按钮将基本位置设置为京都。然后,我使用谷歌地图控件缩小,平移到东京,然后放大。就 z 索引而言,画布位于地图上方,遮罩是用透明的灰色绘制的,因此下方的地图是可见的。

img/272384_2_En_4_Fig16_HTML.jpg

图 4-16

地图上一个地方的阴影/聚光灯

图 4-17 显示了在同一地图上绘制的阴影掩膜的另一个例子。这是因为 Google Maps 处理用户的鼠标移动,然后 JavaScript 代码处理鼠标移动来恢复阴影。

img/272384_2_En_4_Fig17_HTML.jpg

图 4-17

地图上另一个位置的阴影遮罩

这里有几个主题是相互关联的。让我们假设变量mxmy保存鼠标光标在画布上的位置。我将在本章后面解释如何做到这一点。函数drawshadowmask将绘制阴影遮罩。透明的灰色是遮罩的颜色,在名为grayshadow的变量 I 中定义,并使用内置函数rgba构建。rgba代表红绿蓝阿尔法。alpha 指的是透明度/不透明度。alpha 值为 1 表示颜色完全不透明:纯色。值为 0 表示完全透明,颜色不可见。还记得红色、绿色和蓝色值从 0 到 255,255、255 和 255 的组合是白色。这是一个实验的时代。我决定为灰色/浅灰色/幽灵般的阴影设置如下:

var grayshadow = "rgba(250,250,250,.8)";

函数drawshadowmask使用了几个常量变量——它们从不改变。指示这些值的示意图如图 4-18 所示。

img/272384_2_En_4_Fig18_HTML.jpg

图 4-18

为掩模指示变量值的示意图

遮罩分为两部分,就像对弹跳视频的遮罩所做的那样。你可以回头看图 3-8 和图 3-9 。编码是相似的:

function drawshadowmask(mx,my) {
   ctx.clearRect(0,0,600,400);
   ctx.fillStyle = grayshadow;
   ctx.beginPath();
   ctx.moveTo(canvasAx,canvasAy);
   ctx.lineTo(canvasBx,canvasBy);
   ctx.lineTo(canvasBx,my);
   ctx.lineTo(mx+holerad,my);
   ctx.arc(mx,my,holerad,0,Math.PI,true);
   ctx.lineTo(canvasAx,my);
   ctx.lineTo(canvasAx,canvasAy);
   ctx.closePath();
   ctx.fill();
   ctx.beginPath();
   ctx.moveTo(canvasAx,my);
   ctx.lineTo(canvasDx,canvasDy);
   ctx.lineTo(canvasCx,canvasCy);
   ctx.lineTo(canvasBx,my);
   ctx.lineTo(mx+holerad,my);
   ctx.arc(mx,my,holerad,0,Math.PI,false);
   ctx.lineTo(canvasAx,my);
   ctx.closePath();
   ctx.fill();
}

现在我们继续看红色灯泡。

光标

光标——移动鼠标时在屏幕上移动的小图形——可以在 style 元素或 JavaScript 中设置。图形有几个内置的选择(例如,十字准线和指针),我们也可以参考我们自己的设计来定制光标,这就是我在这个项目中演示的。我加入了声明

can.onmousedown = function () { return false; } ;

init功能中,防止按下鼠标时改变默认光标。这可能是不必要的,因为默认可能不会被触发。

为了将移动鼠标的光标更改为传递聚光灯的东西,我创建了一个红色紧凑型荧光灯泡的图片,并将其保存在文件light.gif中。然后,我在函数showshadow中使用了下面的语句。showshadow函数已经被设置为mousemove的事件处理程序

can.style.cursor = "url('light.gif'), pointer";

指示 JavaScript 应该在光标位于can元素顶部时使用图像的地址。此外,如果light.gif文件不可用,该语句会指示 JavaScript 使用内置指针图标。这类似于用优先选择列表来指定字体的方式。变量can已经被设置为引用画布元素。当画布被推到谷歌地图下时,光标将不被使用,这将在下一节讨论。

JavaScript 事件

当我开始从事这个项目时,对事件的处理——即鼠标事件,但也包括改变 Google 地图缩放比例或单击单选按钮的事件——似乎是最令人生畏的。关键的考虑是事件是由 Google Maps 处理还是由我的 JavaScript 代码处理。然而,实际的实现结果很简单。在init函数和makemap函数中,我编写代码来设置鼠标移动、按下鼠标按钮和抬起鼠标按钮的事件处理,所有这些都与canvas元素有关。例如,在init函数中,有

  can.addEventListener('mousemove',showshadow);
  can.addEventListener('mousedown',pushcanvasunder);
  can.addEventListener("mouseout",clearshadow);

如前所述,showshadow函数调用drawshadowmask函数。我可以将这两个功能结合起来,但是将任务分成更小的任务通常是一个好的实践。showshadow功能确定鼠标位置,进行调整,使灯泡底座位于聚光灯的中心,然后调用drawshadowmask:

function showshadow(ev) {
   var mx;
   var my;
   if ( ev.layerX ||  ev.layerX == 0) {
        mx= ev.layerX;
        my = ev.layerY;
        }
  else if (ev.offsetX || ev.offsetX == 0) {
        mx = ev.offsetX;
        my = ev.offsetY;
         }
    can.style.cursor = "url('light.gif'), pointer";
    mx = mx+10;
    my = my + 12;
    drawshadowmask(mx,my);
}

提到ev.layerXev.layerYif声明是针对老款火狐浏览器的。它可能会被移除。

现在我需要确定当用户按下鼠标时我想做什么。我决定让阴影消失,让地图以最大亮度显示。除了事情的表象,我还想让谷歌地图 API 恢复控制。希望 Google Maps API 接管的一个关键原因是,我想在地图上放置一个标记,而不是在画布上,来标记一个位置。这是因为我想让标记随着地图移动,而这很难通过在画布上绘制来实现。我需要将画布上的标记与地图的平移和缩放同步。相反,API 为我完成了所有这些工作。此外,我需要 Google Maps API 来生成该位置的纬度和经度值。

可以说,重新控制谷歌地图的方法是“把画布压下去”。该功能是

function pushcanvasunder(ev) {
        can.style.zIndex = 1;
        pl.style.zIndex = 100;
}

将画布压到下面或放回上面的操作不是瞬间完成的。我愿意接受关于(1)如何定义接口和(2)如何实现你所定义的建议。这里有改进的余地。

另一个需要注意的情况是,当用户将鼠标从画布上移开时,我希望发生什么?mouseout事件是可以监听的,所以我编写了设置事件的代码(参见前面显示的can.addEventListener语句)由clearshadow函数处理。clearshadow函数正好完成了这一点——它清除了整个画布,包括阴影:

function clearshadow(ev) {
        ctx.clearRect(0,0,600,400);
}

在引入 Google 地图的函数中,我为地图的mouseup设置了一个事件处理程序。

listener = google.maps.event.addListener(map, 'mouseup', function(event) {
                        checkit(event.latLng);
                        });

addListener的调用是 Google Maps API 的一部分,而不是 JavaScript 本身,它设置了对checkit函数的调用。用一种更非正式的方式重复一下已经说过的话:这个对google.maps.event.addListener的调用设置了 Google API 来监听地图上的mouseup事件。以下语句使 JavaScript 监听can(画布)上的mouseout事件。

can.addEventListener("mouseout",clearshadow);

使用event对象的属性作为参数来调用checkit函数。正如您所猜测的,event.latLng是在map对象上释放鼠标按钮时鼠标所在位置的经纬度值。checkit功能将使用这些值来计算离基准位置的距离,并将这些值和距离一起打印在屏幕上。这段代码调用了我编写的对值进行舍入的函数。我这样做是为了避免显示一个有很多有效数字的值,超过了适合这个项目的数字。Google Maps API marker方法提供了一种使用我选择的图像作为标记的方法,这次是黑色手绘的 x ,并在标记中包含一个标题。推荐的标题是让使用屏幕阅读器的人可以访问应用程序,尽管我不能说这个项目在可访问性方面会让任何人满意。可以产生如图 4-19 所示的屏幕。注意我住的基斯科山附近的 x。顶部的信息显示了我通勤的英里数。可以更改代码来计算英里或公里。

img/272384_2_En_4_Fig19_HTML.jpg

图 4-19

指示地图上显示的距离的标题

用保存纬度和经度值的参数调用的checkit函数如下:

function checkit(clatlng) {
   var distance = dist(clatlng,blatlng);
   distance = round(distance,2);
   var distanceString = String(distance)+" km";
   marker = new google.maps.Marker({
   position: clatlng,
   title: distanceString,
   icon: bxmarker,
   map: map });
   var clat = clatlng.lat();
   var clng = clatlng.lng();
   clat = round(clat,4);
   clng = round(clng,4);
   document.getElementById("answer").innerHTML  =
"The distance from base to most recent marker ("+clat+", "+clng+") is "+String(distance) +" miles.";
//change miles to km depending on value used for R in the dist function
    can.style.zIndex = 100;
    pl.style.zIndex = 1;
}

尽管我省略了文本中的大部分注释,但我觉得有必要保留关于英里和公里的注释。我建议你在工作中也这样做。

请注意,该函数做的最后一件事是将画布放回地图的顶部。

CHANGE 按钮和单选按钮是使用标准 HTML 和 JavaScript 实现的。该表单是使用以下 HTML 代码生成的:

<form name="f" onSubmit=" return changebase();">
    <input type="radio" name="loc" /> Springer Nature (Apress Publishers) London, UK<br/>
    <input id="first" type="radio" name="loc" /> Purchase College/SUNY, NY, USA<br/>
    <input type="radio" name="loc" /> Kyoto, Japan<br/>
    <input type="submit" value="CHANGE">
</form>

当点击标记为 CHANGE 的提交按钮时,调用函数changebasechangebase函数确定选中了哪个单选按钮,并使用 Locations 表获取纬度和经度值。然后它使用这些参数值调用makemap。这种组织数据的方式叫做并行结构:数组元素locations对应单选按钮。最后一条语句将 header 元素的innerHTML设置为显示文本,包括所选基本位置的名称。

function changebase() {
        var mylat;
        var mylong;
        for(var i=0;i<locations.length;i++) {
                if (document.f.loc[i].checked) {
                        mylat = locations[i][0];
                        mylong = locations[i][1];
                        makemap(mylat,mylong);
                        document.getElementById("header").innerHTML =
                                   "Base location (small red x) is "+locations[i][2];
                }
        }
        return false;
}

计算显示的距离和舍入值

正如我们许多人所知,谷歌地图提供距离信息,甚至区分步行和驾驶。对于这个应用程序,我需要更多的控制来指定我想要计算距离的两个位置,所以我决定用 JavaScript 开发一个函数。确定两个点之间的距离,每个点代表纬度和经度值,是使用余弦球面定律来完成的。我的消息来源是 http://www.movable-type.co.uk/scripts/latlong.html 。这是代码。请注意,为了生成以公里为单位的值,您使用一个 R 值和一个被注释的 miles 值。如果当你切换到英里,你需要确保显示的信息说英里。

function dist(point1, point2) {
   var R = 6371; // km  Need to make sure this syncs with the message displayed re: distance.
   // var R =  3959; // miles
   var lat1 = point1.lat()*Math.PI/180;
   var lat2 = point2.lat()*Math.PI/180 ;
   var lon1 = point1.lng()*Math.PI/180;
   var lon2 = point2.lng()*Math.PI/180;
   var d = Math.acos(Math.sin(lat1)*Math.sin(lat2) +
   Math.cos(lat1)*Math.cos(lat2) *
   Math.cos(lon2-lon1)) * R;
    return d;
  }

警告

我没有在代码中包含很多注释,因为我在本章的表格中注释了每一行。但是,注释很重要。我强烈建议在dist函数中留下对kmmiles的注释,这样你就可以适当地调整你的程序。或者,您可以显示这两个值,或者给用户一个选择。

最后一个函数用于舍入值。当一个量依赖于一个人移动鼠标时,你不应该显示一个有很多小数位的值。但是,请记住,纬度和经度代表大单位。我决定用两位小数显示距离,用四位小数显示纬度和经度。

我写的函数挺一般的。它有两个参数,一个是数字num,另一个是places,表示取值的小数位数。你可以在其他情况下使用它。它通过添加我称为增量的值,然后计算不大于该值的最大整数,适当地向上或向下舍入。因此

  • round(9.147,2)将产生 9.15

  • round(9.143, 2)将产生 9.14

代码的工作方式是首先确定我称之为的因子,10 的预期位数。对于 2,这将是 100。然后我计算增量。对于两个地方,这将是 5 / 100 * 10,也就是 5/1000,也就是. 005。我的代码执行以下操作:

  1. 将增量加到原始数字上。

  2. 将结果乘以系数。

  3. 计算不大于结果的最大整数(这称为下限)—产生一个整数。

  4. 将结果除以因子。

代码如下:

function round (num,places) {
        var factor = Math.pow(10,places);
        var increment = 5/(factor*10);
        return Math.floor((num+increment)*factor)/factor;
}

我使用round函数将距离四舍五入到两位小数,将纬度和经度四舍五入到四位小数。

小费

JavaScript 有一个名为toFixed的方法,本质上执行我这一轮的任务。如果num持有一个数字,比如说 51.5621,那么num.toFixed()将产生 51,而num.toFixed(2)将产生 51.56。我了解到这种方法可能会有误差,所以我选择创建自己的函数。不过,你可能很乐意在自己的应用程序中使用toFixed()

随着相关 HTML5 和谷歌地图 API 特性的解释,我们现在可以把它们放在一起。

构建应用程序并使之成为您自己的应用程序

map spotlight 应用程序将 Google Maps 功能与 HTML5 编码结合起来。该应用程序的简要概述如下:

  1. init:初始化,包括带入地图(makemap)和用处理程序设置鼠标事件:showshadowpushcanvasunderclearshadow

  2. makemap:引入一个地图并设置事件处理,包括对checkit的调用

  3. showshadow:调用drawshadowmask

  4. pushcanvasunder:启用地图上的事件

  5. checkit:计算距离,添加自定义标记,显示距离和四舍五入后的经纬度

描述被调用/被调用和调用关系的函数表(表 4-1 )对于所有的应用程序都是相同的。

表 4-1

功能 在地图制作项目

|

功能

|

调用/调用

|

来电

|
| --- | --- | --- |
| init | 由<body>标签中的onLoad属性的动作调用 | makemap |
| pushcanvasunder | 由在init中调用的addEventListener的动作调用 |   |
| clearshadow | 由在init中调用的addEventListener的动作调用 |   |
| showshadow | 由在init中调用的addEventListener的动作调用 | drawshadowmask |
| drawshadowmask | 由showshadow调用 |   |
| makemap | 由init调用 |   |
| checkit | 由在makemap中调用的addEventListener的动作调用 | rounddist |
| round | 由checkit调用(三次) |   |
| dist | 由checkit调用 |   |
| changebase | 由<form>中的onSubmit动作调用 | makemap |

表 4-2 显示了名为mapspotlight.html的地图制作应用程序的代码。

表 4-2

mapspotlight.html 应用程序的完整代码

|

代码行

|

描述

|
| --- | --- |
| <!DOCTYPE html> | 页眉 |
| <html> | 开始html标签 |
| <head> | 开始head标签 |
| <title>Spotlight </title> | 完整标题 |
| <meta charset="UTF-8"> | 什么时候 |
| <style> | 打开style元件 |
| header {font-family:Georgia,"Times New Roman",serif; | 设置标题的字体 |
| font-size:16px; | 字体大小 |
| display:block;} | 前后换行 |
| canvas {position:absolute; top: 165px; left:0px; | 单个画布元素的样式指令;在页面上稍微向下放置 |
| z-index:100;} | 画布的初始设置在地图的顶部 |
| #place {position:absolute; top: 165px; left: 0px; | 用于持有谷歌地图的div的样式指令;位置与画布完全相同 |
| z-index:1;} | 初始设置在帆布下 |
| </style> | 关闭style元素 |
| <script async defer src="``https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap``type="text/javascript"></script> | 引入包含 Google Maps API 的外部脚本元素;注意:您需要获得自己的密钥来运行该程序 |
| <script type="text/javascript" charset="UTF-8"> | 开始script标签 |
| var locations = [ | 定义一组基本位置;注意:您需要与正文中的单选按钮相协调 |
| [51.534467,-0.121631, "Springer Nature (Apress Publishers) London, UK"], | 伦敦的纬度、经度名称出版商办公室 |
| [41.04796,-73.70539,"Purchase College/SUNY, NY, USA"], | 。。。采购学院 |
| [35.085136,135.776585,"Kyoto, Japan"] | 。。。京都 |
| ]; | 封闭位置阵列 |
| var candiv; | 用于握持div握持画布 |
| var can; | 参考画布元素 |
| var ctx; | 画布的参考上下文;用于所有绘图 |
| var pl; | 参考拿着谷歌地图的div |
| function init() { | init的功能头 |
| var mylat; | 将保存纬度值 |
| var mylong; | 将保存经度值 |
| candiv = document.createElement("div"); | 创建一个div |
| candiv.innerHTML = ("<canvas id="canvas" width="600" height="400">No canvas </canvas>"); | 将其内容设置为一个canvas元素 |
| document.body.appendChild(candiv); | 添加到正文 |
| can = document.getElementById("canvas"); | 设置对画布的引用 |
| pl = document.getElementById("place"); | 设置对持有谷歌地图的div的引用 |
| ctx = can.getContext("2d"); | 设置上下文 |
| can.onmousedown = function () { return false; } ; | 防止将光标更改为默认值 |
| can.addEventListener('mousemove',showshadow); | 为鼠标移动设置事件处理 |
| can.addEventListener('mousedown',pushcanvasunder); | 设置按下鼠标按钮的事件处理 |
| can.addEventListener("mouseout",clearshadow); | 设置将鼠标移出画布的事件处理 |
| mylat = locations[1][0]; | 将纬度设置为第一个(中间)位置的纬度 |
| mylong = locations[1][1]; | 将经度设置为第一个(中间)位置的经度 |
| document.getElementById("first").checked="checked"; | 将中间单选按钮设置为显示为选中状态 |
| makemap(mylat,mylong); | 调用函数来制作地图(在指定位置引入谷歌地图) |
| } | 关闭init功能 |
| function pushcanvasunder(ev) { | pushcanvas函数的头,用引用事件的参数调用 |
| can.style.zIndex = 1; | 向下推帆布 |
| pl.style.zIndex = 100; | 设置地图div向上 |
| } | 关闭pushcanvasunder功能 |
| function clearshadow(ev) { | clearshadow函数的头,用引用事件的参数调用 |
| ctx.clearRect(0,0,600,400); | 清除画布(擦除阴影遮罩) |
| } | 关闭clearshadow功能 |
| function showshadow(ev) { | showshadow 函数的标头,用引用事件的参数调用 |
| var mx; | 将用于保持鼠标的水平位置 |
| var my; | 将用于保持鼠标的垂直位置 |
| if ( ev.layerX &#124;&#124;  ev.layerX == 0) { | 这个浏览器用layerX吗?注意:这是针对旧浏览器的 |
| mx = ev.layerX; | 如果是,用它来设置mx。。。 |
| my = ev.layerY; | 。。。和my |
| } else if (ev.offsetX &#124;&#124; ev.offsetX == 0) { | 试试offset。注意:这适用于当前的浏览器 |
| mx = ev.offsetX; | 如果是,用它来设置mx。。。 |
| my = ev.offsetY; | 。。。和my |
| } | 关闭条款 |
| can.style.cursor = "url('light.gif'),pointer"; | 如果可用,将光标设置为light.gif;否则使用pointer |
| mx = mx+10; | 进行粗略的校正,使光的中心水平地位于灯泡的底部。。。 |
| my = my + 12; | 。。。垂直地 |
| drawshadowmask(mx,my); | 在修改后的(mx,my)调用drawshadowmask功能 |
| } | 关闭showshadow功能 |
| var canvasAx = 0; | 遮罩常数:左上 x |
| var canvasAy = 0; | 左上 y |
| var canvasBx = 600; | 右上 x |
| var canvasBy = 0; | 右上 y |
| var canvasCx = 600; | 右下 x |
| var canvasCy = 400; | 右下 y |
| var canvasDx = 0; | 左下 x |
| var canvasDy = 400; | 左下 y |
| var holerad = 50; | 阴影中孔洞的恒定半径(聚光灯的半径) |
| var grayshadow = "rgba(250,250,250,.8)"; | 暗淡阴影的颜色;注意 0.8 的α值 |
| function drawshadowmask(mx,my) { | drawshadowmask功能的表头;参数保持环形孔的中心 |
| ctx.clearRect(0,0,600,400); | 擦除整个画布 |
| ctx.fillStyle = grayshadow; | 设置颜色 |
| ctx.beginPath(); | 开始第一个(顶部)路径 |
| ctx.moveTo(canvasAx,canvasAy); | 移动到左上角 |
| ctx.lineTo(canvasBx,canvasBy); | 画到右上角 |
| ctx.lineTo(canvasBx,my); | 绘制到由我的参数指定的垂直点 |
| ctx.lineTo(mx+holerad,my); | 向左画到洞的边缘 |
| ctx.arc(mx,my,holerad,0,Math.PI,true); | 画半圆弧 |
| ctx.lineTo(canvasAx,my); | 向左侧绘制 |
| ctx.lineTo(canvasAx,canvasAy); | 退回起点 |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 填写 |
| ctx.beginPath(); | 第二(较低)路径的起点 |
| ctx.moveTo(canvasAx,my); | 从我的参数指示的左侧点开始 |
| ctx.lineTo(canvasDx,canvasDy); | 绘制到左下角 |
| ctx.lineTo(canvasCx,canvasCy); | 绘制到右下角 |
| ctx.lineTo(canvasBx,my); | 绘制到右边缘的点 |
| ctx.lineTo(mx+holerad,my); | 向左画到洞的边缘 |
| ctx.arc(mx,my,holerad,0,Math.PI,false); | 画半圆弧 |
| ctx.lineTo(canvasAx,my); | 向左边缘绘制 |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 填写 |
| } | 关闭drawshadowmask功能 |
| var listener; | 通过addListener调用设置的变量;没有再次使用 |
| var map; | 持有地图 |
| var blatlng; | 保存基本经纬度对象 |
| var myOptions; | 保存用于映射的关联数组 |
| var rxmarker = "rx1.png"; | 保存红色 x 图像的文件名 |
| var bxmarker = "bx1.png"; | 保存黑色 x 图像的文件名 |
| function makemap(mylat,mylong) { | makemap功能的表头;参数保存地图中心的位置 |
| var marker; | 将保留为中心创建的标记 |
| blatlng = new google.maps.LatLng(mylat,mylong); | 构建一个LatLng对象(API 的特殊数据类型) |
| myOptions = { | 集合关联数组 |
| zoom: 12, | 缩放设置(可以是 0 到 18) |
| center: blatlng, | 中心 |
| mapTypeId: google.maps.MapTypeId.ROADMAP | 地图类型 |
| }; | 关闭myOptions阵列 |
| map = new google.maps.Map(document.getElementById("place"), myOptions); | 调用 API 在指定位置引入地图 |
| marker = new google.maps.Marker( | 在地图中心放置标记;marker方法采用一个关联数组作为其参数;注意:也可以使用标记对象的setMap方法 |
| { | 关联数组的开始 |
| position: blatlng, | 设置位置 |
| title: "center", | 设置标题 |
| icon: rxmarker, | 设置图标 |
| map: map | 将 map named 参数设置为名为 map 的变量 |
| } | 关闭关联数组,它是调用Marker的参数 |
| ); | 关闭对Marker方法的调用 |
| listener = google.maps.event.addListener( | 设置事件处理(以下三个参数);这是一个谷歌地图活动 |
| map, | 对象,即地图 |
| 'mouseup', | 特定事件 |
| function(event) { | 自主功能(直接定义为addListener中的参数) |
| checkit(event.latLng); | 用指定的经纬度对象调用checkit |
| } | 关闭函数定义 |
| ); | 关闭对addListener的呼叫 |
| } | 关闭makemap功能 |
| function checkit(clatlng) { | checkit的功能头;用纬度-经度对象调用 |
| var distance = dist(clatlng,blatlng); | 调用dist函数,计算点击位置与底部之间的距离 |
| var marker; | 将保存新创建的标记 |
| distance = round(distance,2); | 四舍五入数值 |
| var distanceString = String(distance)+" km"; | 设置distanceString为显示 |
| marker = new google.maps.Marker( | 调用Marker方法,该方法将一个关联数组作为其参数 |
| { | 关联数组的开始 |
| position: clatlng, | 预备姿势 |
| title: distanceString, | 设置标题 |
| icon: bxmarker, | 将图标设置为黑色 x |
| map: map | 将关联数组的 map 元素设置为名为 map 的变量的值 |
| } | 紧密关联数组 |
| ); | 关闭对Marker方法的调用 |
| var clat = clatlng.lat(); | 提取纬度值 |
| var clng = clatlng.lng(); | 提取经度值 |
| clat = round(clat,4); | 将值四舍五入到小数点后四位 |
| clng = round(clng,4); | 将值四舍五入到小数点后四位 |
| document.getElementById("answer").innerHTML  = | 在屏幕上设置文本。。。 |
| "The distance from base to most recent marker ("``+ clat+", "+clng+") is "+String(distance) +" km."; | 。。。要计算和格式化的信息 |
| can.style.zIndex = 100; | 将画布设置在顶部 |
| pl.style.zIndex = 1; | 将pl(手持地图)设置在下方 |
| } | 关闭checkit功能 |
| function round (num,places) { | 舍入值的函数的标题 |
| var factor = Math.pow(10,places); | 根据位置数量确定系数 |
| var increment = 5/(factor*10); | 确定向上或向下舍入的增量 |
| return Math.floor((num+increment)*factor)/factor; | 进行计算 |
| } | 关闭round功能 |
| function dist(point1, point2) { | dist(距离)功能的功能头 |
| // spherical law of cosines,``// from``//http://www.movable-type.co.uk/scripts/latlong.html | 我的来源的归属;这是标准数学 |
| var R = 6371; // km | 用于生成答案的因子,以公里为单位 |
| // var R =  3959; // miles | 注释掉,但是保留以防万一你想用英里给出答案,我在图 4-19 中就是这么做的 |
| var lat1 = point1.lat()*Math.PI/180; | 将值转换为弧度 |
| var lat2 = point2.lat()*Math.PI/180 ; | 将值转换为弧度 |
| var lon1 = point1.lng()*Math.PI/180; | 将值转换为弧度 |
| var lon2 = point2.lng()*Math.PI/180; | 将值转换为弧度 |
| var d = | 计算。。。 |
| Math.acos(Math.sin(lat1)*Math.sin(lat2) + Math.cos(lat1)*Math.cos(lat2) * Math.cos(lon2-lon1)) * R; | 用三角学确定距离 |
| return d; | 回送结果 |
| } | 关闭dist功能 |
| function changebase() { | changebase功能的标题 |
| var mylat; | 将持有新的基准位置纬度 |
| var mylong; | 将保存新的基准位置经度 |
| for(var i=0;i<locations.length;i++) { | for循环确定哪个单选按钮被选中 |
| if (document.f.loc[i].checked) { | 这个检查过了吗? |
| mylat = locations[i][0]; | 如果是,设置mylat |
| mylong = locations[i][1]; | 设置mylong |
| makemap(mylat,mylong); | 调用makemap |
| document.getElementById("header").``innerHTML = "Base location (small red x) is "+locations[i][2]; | 更改标题中的文本以显示名称 |
| } | 关闭if true子句 |
| } | 关闭for回路 |
| return false; | 返回false进行当前刷新 |
| } | 关闭功能 |
| </script> | 结束script标签 |
| </head> | 结束head标签 |
| <body onLoad="init();"> | 开始body标签;包括onLoad来调用init |
| <header id="header">Base location (small red x) </header> | 语义头元素 |
| <div id="place" style="width:600px; height:400px"></div> | div持有谷歌地图 |
| <div id="answer"></div> | div保存点击位置的信息 |
| Change base location: <br/> | 文本 |
| <form name="f" onSubmit=" return changebase();"> | 改变基底的形式的开始;注意:您需要与script元素中的 locations 数组协调 |
| <input type="radio" name="loc" /> Springer Nature (Apress Publishers) London, UK<br/> | 单选按钮选择 |
| <input id=”first” type="radio" name="loc" /> Purchase College<br/> | 单选按钮选择;给定 ID 设置为打开时检查 |
| <input type="radio" name="loc" /> Kyoto, Japan<br/> | 单选按钮选择 |
| <input type="submit" value="CHANGE"> | 进行更改的按钮 |
| </form> | 结束form标签 |
| </body> | 结束body标签 |
| </html> | 结束html标签 |

你需要决定你的基地位置。还是那句话,三没什么特别的。你的选择可能会更接近。如果你的基本列表太大,你可以考虑使用<optgroup>产生一个下拉列表。无论如何,您都需要定义一组位置。每个地点都有两个数字——纬度和经度——以及一串包含名称的文本。一些文本在 HTML 中以 body 元素的形式重复出现。

测试和上传应用程序

这个项目由 HTML 文件和三个图像文件组成。对于我的项目版本,图像文件是灯泡(light.gif)、红色的 x ( rx1.png)和黑色的 x ( bx1.png)。这些图像文件类型没有什么特别的。你喜欢什么就用什么。有人可能会说,我的 x 标记太小了,所以在决定怎么做时,要考虑你的客户。

这个应用程序需要你在线测试,因为这是联系谷歌地图的唯一方式。

摘要

在本章中,您学习了如何执行以下操作:

  • 使用谷歌地图应用编程接口。

  • 使用画布图形将 Google Maps API 的使用与您自己的 JavaScript 编码结合起来。也就是说,生成一个包含 Google Maps 事件和 HTML5 事件的 GUI。

  • 使用控制透明度/不透明度的 alpha 设置进行绘制。

  • 改为自定义光标。

  • 计算地理点之间的距离。

  • 四舍五入十进制数值,以便适当显示。

下一章描述了另一个使用谷歌地图的项目。您将学习如何构建一个应用程序,在该应用程序中,您可以将图片、视频剪辑或图片和音频剪辑的组合与特定的地理位置相关联,然后您将看到当用户在地图上的位置处或附近单击时,如何显示和播放指定的媒体。

五、地图门户(MapPortal):使用谷歌地图访问您的媒体

在本章中,您将学习以下内容:

  • 使用 Google Maps API 播放和显示视频、音频和图像

  • 动态创建 HTML5 标记

  • 从内容描述中分离出程序

  • 构建地理游戏

介绍

本章中的项目使用 Google Maps API 作为播放视频、显示图像或播放音频的方式显示图像,所有这些都基于地理位置。您可以使用此项目作为模型来构建地理区域的研究或商务或度假旅行的报告,或者您可以将其发展为更复杂的地理测验。正如第四章的情况一样,主要的课程是关于将谷歌地图 API 与你自己的 JavaScript 结合使用,特别是呈现图像、音频和视频。本章的示例是一个测验应用程序。我已经获得了媒体,例如视频文件、音频文件和图像文件,并且我已经在代码中定义了媒体和特定地理位置之间的关联。为了让你了解我的意思,在我的项目中,目标位置(在代码中以经纬度坐标给出)和媒体之间的关联如表 5-1 所示。

表 5-1

内容概要

|

位置描述

|

媒体

|
| --- | --- |
| 美国纽约采购学院(学生服务大楼) | 开始位置:无媒体 |
| 基斯科山, NY, 美国 | 埃丝特的照片和她弹钢琴的音频文件 |
| 美国纽约采购学院(自然科学大楼) | 乐高机器人视频 |
| 美国纽约州自由女神像市 | 烟花视频 |
| 日本宫浜 | 大鸟居的照片 |

应用程序可以顺利处理不同类型的媒体。这要归功于 HTML5 的特性,我谦虚地说,还要归功于我的编程。(事实上,谦虚是需要的:我需要对程序做一个小的修改,因为当我从较小的图片更改为较大的图片时,图像尺寸有很大的差异。)媒体信息以及问题和位置存储在单独的文件中。

仍然建议您提供多种视频和音频格式,以确保您的应用程序可以在不同的浏览器中工作。浏览器识别的媒体类型可能会发生变化,因此需要的类型会减少,但目前情况并非如此。

该应用程序是一个简单的测验。它由两个文件组成:mapmediaquiz.htmlmediaquizcontent.jsmediaquizcontent.js文件包含连接媒体和位置的信息,也包含问题的文本。

图 5-1 显示了测验的开始屏幕。

img/272384_2_En_5_Fig1_HTML.jpg

图 5-1

测验的开始屏幕

玩家现在试图通过确定位置并点击地图来回答这个问题。图 5-2 显示了当我点击购买校园时会发生什么。这不是一个好的答案,这是程序检测到的。

img/272384_2_En_5_Fig2_HTML.jpg

图 5-2

点击购买学院的结果

注意,在我点击的地方出现了一个小 x,但是它离正确的位置还不够近。我移动地图并再次尝试,图 5-3 显示了点击屏幕的结果,但没有足够接近目标位置。请注意,我已经平移了地图,将它移动到了北方。玩家可以选择得到提示。当我点击提示按钮时,图 5-3 出现了。这是一个非常强烈的暗示,鼓励读者想出一种方法来帮助玩家而不给出答案。

img/272384_2_En_5_Fig3_HTML.jpg

图 5-3

点击提示按钮的结果

当我按照指示点击红色 x 时,图 5-4 显示了结果。还要注意音频控制,它提供了暂停和恢复播放以及改变扬声器音量的方法。在不同的浏览器中,对音频(和视频)的控制会有所不同,但功能是相同的。音频会立即开始播放。还要注意,下一个问题出现了。

img/272384_2_En_5_Fig4_HTML.jpg

图 5-4

图像和音频组合

因为我知道位置在哪里,所以我知道缩小到下一个位置。图 5-5 显示了使用谷歌地图界面实现这一点的结果。音轨继续播放,我仍然可以看到图片。

img/272384_2_En_5_Fig5_HTML.jpg

图 5-5

缩小以准备向南平移

图 5-6 显示了将地图移动到南方,然后放大到采购园区的结果,在采购园区,学生制作的视频显示了一个乐高 Mindstorms 机器人正在穿越一个迷宫。

img/272384_2_En_5_Fig6_HTML.jpg

图 5-6

正确定位(足够接近)乐高机器人,播放视频

下一个地点是自由女神像。请注意,当我单击该位置附近时,会出现一个由 Google 设置的弹出标签。

img/272384_2_En_5_Fig7_HTML.jpg

图 5-7

缩小、向南平移,然后放大以单击自由女神像

最后一个问题需要穿越整个国家,穿越太平洋才能找到 Miyajama(照片的供应商 Takashi 告诉过我)。图 5-8 显示了第一步的结果。

img/272384_2_En_5_Fig8_HTML.jpg

图 5-8

缩小然后放大日本

前一个问题的结果仍然出现。我按下提示按钮,看到图 5-9 所示的内容。

img/272384_2_En_5_Fig9_HTML.jpg

图 5-9

点击提示按钮的结果

当我点击屏幕上提示的位置时,图 5-10 出现。这是日本主要的本地和全球旅游景点。

img/272384_2_En_5_Fig10_HTML.jpg

图 5-10

伟大的鸟居

在这一点上,我需要承认我的原始代码不能处理 Takashi 提供的非常好和非常大的图像。它对于我使用的编码来说太大了,而我使用的编码对于小图像来说已经足够好了。图 5-11 显示了当我将大鸟居问题和宫间的照片包含在我的原始代码中时会发生什么。这确实令人失望。这只是图像的左上角。

img/272384_2_En_5_Fig11_HTML.jpg

图 5-11

显示图像的原始编码结果

我最初的声明:

         ctx.drawImage(img1,0,0);

只在画布上画了画的上角。相反,我需要编写 JavaScript 来确定如何将图片缩放到 400x400 的画布上,在执行缩放的同时保持纵横比。下面的方法可以解决这个问题:

            var iw = img1.width;
            var ih = img1.height;
            var aspect = iw/ih;
            if (iw>=ih) {
                if (iw>400){
                tw = 400;
                th = 400/aspect;
                }
                else {
             tw = iw;
             th = ih;
                 }
              }
              else {
      if (ih>400){
                       th = 400;
           tw = 400*aspect;
        }
      else {
                       th = ih;
            tw = iw;
      }
              }
              ctx.drawImage(img1,0,0,iw,ih,0,0,tw,th);

图 5-12 表示代码的动作。原始宽度等于iw且高度等于ih的图像被缩小以适合 400 乘 400 的画布。最终尺寸由twth表示。这段代码产生了如图 5-10 所示的内容。

img/272384_2_En_5_Fig12_HTML.jpg

图 5-12

显示源和目标宽度和高度值关系的图表

有了这个介绍,我将继续讨论项目历史和关键需求。

项目历史和关键要求

Purchase 学院的一名大四学生收集并制作了关于纽约皇后区少数民族社区的视频剪辑和照片,并想找到一种展示这项工作的方式。谷歌地图 API 和 HTML5 中的新工具似乎非常适合这项任务。请记住,该学生只需要在她在高级项目展示中设置的计算机上演示作品,因此不兼容浏览器的问题不是问题。关键需求包括 Google Maps API 所提供的内容。正如您在上一章中所了解的,我们可以编写代码来访问以指定地理位置为中心的地图,设置初始缩放级别,并显示道路或卫星或地形或混合的视图。此外,API 还提供了一种方法来响应查看者单击地图的事件。我们需要一种方法来定义特定的位置,以便与查看者点击对应的位置进行比较。

我为学生设计的第一个系统只使用了视频和图像。我后来决定添加图像和音频组合。应用程序的关键要求是在正确的时间显示和播放指定的媒体,并在适当的时候停止和移除媒体,例如到了下一次演示的时间。

在帮助学生项目后,我想到了改变。第一个是添加了图像和音频组合。我决定不要音频本身。下一个变化是将特定内容从一般编码中分离出来。这反过来需要一种为视频和音频元素动态创建标记的方法。

我一直喜欢游戏和课程,为观众——现在最好描述为玩家或学生——构建一个带有问题或提示的应用程序似乎是一个自然的步骤。玩家通过在地图上找到正确的位置给出答案。像这样的任何应用程序都需要定义一个关于答案的容差。不能期望观众/玩家/学生准确地点击正确的点。

在测试小测验时,我意识到我需要一些方法来帮助玩家通过一个特别难的问题。因为我是老师,所以我决定给玩家看答案,而不是直接跳过问题。然而,正如我前面指出的,您也许能够设计出一种更好的方法来产生提示。

虽然在玩游戏时这一点并不明显,但是问题、地点(答案)和媒体的分离使得我们可以很容易地组织一个完全不同的测验。然而,正如我所指出的,当我决定加入不同大小和形状的图片时,我确实需要做一些调整。

描述了关键需求之后,下一节包含了对可用于构建项目的特定 HTML5 特性的解释。

HTML5、CSS 和 JavaScript 特性

像第四章中的 map maker 项目一样,这些项目是通过结合使用 Google Maps API 和 HTML5 的特性来实现的。这个项目的组合并不复杂。地图停留在窗口的左侧,媒体显示在右侧。我将快速回顾如何访问地图以及如何设置事件处理,然后继续讨论 HTML5、CSS 和 JavaScript 特性,以满足其余的关键需求。

用于地图访问和事件处理的谷歌地图 API

访问 Google Maps API 需要一个引用外部文件的脚本元素。正如在第四章中提到的,使用谷歌地图 API 的第一步是去这个网站获取一个密钥: https://developers.google.com/maps/documentation/javascript/get-api-key

访问 API 的代码是修改以下内容,然后将其添加到 HTML 文档中:

<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
  type="text/javascript"></script>

这个外部脚本元素引入了对象的定义,比如地图和标记,您现在可以使用这些定义将 Google Maps 的功能包含到您的 HTML 和 JavaScript 项目中。

我使用一个名为makemap的函数建立了到映射的连接。它有两个参数:代表纬度和经度值的两个十进制数字:

function makemap(mylat, mylong)

保存从 0 到 18 的数字的全局变量zoomlevel和保存图像文件地址的bxmarker and rxmarker在函数makemap被调用之前被设置。

引入地图的代码是对google.maps.Map构造函数方法的调用。它需要两个参数。第一个是 HTML 文档中地图出现的位置。我在文档体中设置了一个 ID 为placediv:

<div id="place" style="float: left; width:50%; height:400px"></div>

第二个参数是一个关联数组。以下三个语句将地图的中心位置设置为 Google Maps 经纬度对象,创建关联数组myOptions,并调用Map构造函数:

blatlng = new google.maps.LatLng(mylat,mylong);
myOptions = {
          zoom: zoomlevel,
          center: blatlng,
                 mapTypeId: google.maps.MapTypeId.ROADMAP
            };
map = new google.maps.Map(document.getElementById("place"), myOptions);

为了完整起见,这里是地图类型的其他设置的截图。这些是地形、混合和卫星。mapTypeId可以用简单的字符串设置,例如'roadmap'。图 5-13 显示了请求显示地形的设置的结果——即指示海拔、水、公园和人工建筑区域的颜色:

img/272384_2_En_5_Fig13_HTML.jpg

图 5-13

地形图类型

mapTypeId: google.maps.MapTypeId.TERRAIN

图 5-14 显示了结合卫星和道路地图图像请求混合视图的结果。

img/272384_2_En_5_Fig14_HTML.jpg

图 5-14

混合贴图类型

mapTypeId: google.maps.MapTypeId.HYBRID

对了,混合地图是点击界面上的卫星选项产生的。

图 5-15 显示请求卫星图像的结果。我们可以认为这是纯卫星图像。请注意,主要的高速公路是可见的。

img/272384_2_En_5_Fig15_HTML.jpg

图 5-15

卫星地图类型

mapTypeId: google.maps.MapTypeId.SATELLITE

最后,在您的应用程序中,您可能不希望查看者直接更改地图。您可以通过使用myOptions数组中的附加选项禁用默认界面来防止用户更改地图。我已经包含了我放在disableDefaultUI之前的语句,以表明关联数组属性由逗号分隔,最后一个逗号后没有逗号。

mapTypeId: google.maps.MapTypeId.ROADMAP,
disableDefaultUI: true

图 5-16 显示了结果。用户仍然可以平移地图,即移动地图,但是+和–缩放控件以及地图和卫星按钮已被移除。

img/272384_2_En_5_Fig16_HTML.jpg

图 5-16

地图接口已移除

还有两个操作需要makemap执行。在地图上指定的中心位置放置一个自定义标记,并为单击地图设置事件处理:

marker = new google.maps.Marker({
         position: blatlng,
         title: "center",
         icon: rxmarker,
         map: map });
listener = google.maps.event.addListener(map, 'click', function(event) {
                        checkit(event.latLng);
                        });

rxmarker值引用了一个图像对象,它的src被设置为一个名为rx1.png的外部文件。这就是在地图中心产生红色小 x 的原因。提醒一下:addListener是为 Google Maps API 设置事件的方法。addEventListener是一个为 JavaScript 设置事件的方法。

外部文件中的项目内容

测验使用了三种媒体:videopicture和我称之为pictureaudio。注意:这些是我选择包含在项目中的三种类型的术语。测验的内容是用两个数组指定的,我命名为precontentquestionsprecontent数组的每个元素本身是一个五或六个元素的数组。前四个元素对于所有类型都是相同的:纬度、经度、标题和类型。第五或第五和第六指向特定的媒体元素。当前测验的数据,即外部文件的内容是:

var base=
            [41.04796,-73.70539,"Purchase College/SUNY"];
var zoomlevel = 13;

var precontent = [
   [41.19991,-73.72353,"Esther at home","pictureaudio","estherT","esther.jpg"],
   [41.05079,-73.70448,"Lego robot","video","maze"],
   [40.68992,-74.04460,"Fire works","video","sfire3"],
   [34.298846,132.318359,"Miyajima","picture","miyajima0.JPG"]
   ];

var questions = [
  "Where did Grandma Esther live?",
  "Show the Lego robot navigating a maze.",
  "Where are great fireworks?",
  "Where is the Great Torii?"

];

var maxdistance = 10;

basezoomlevelmaxdistance变量都是它们看起来的样子。base是地图的初始中心点。zoomlevel指定初始缩放。我说初始是因为用户可以使用谷歌地图控件来平移或放大或缩小。maxdistance是我用来检查用户点击是否足够接近其中一个位置的数字。您需要为您的应用确定合适的距离。

precontent数组指定了四个位置,以一个图片/音频组合开始,接着是两个视频,再接着是一个图片。如您所料,图片/音频组合数组中的元素包括两条附加信息。仅仅从这段代码来看并不明显,但是esther.jpg指的是一个图像元素,而estherT指的是一个音频元素。同样,mazesfire3指的是视频元素,miyajima0.JPG指的是另一个图像元素。使用两个或更多阵列的布置,如我使用的precontentquestions被称为并行结构。我的代码产生了一个名为content的数组,它被checkit函数引用(将在下面描述),适当的媒体被呈现。

使用一个script元素将外部脚本引入主文档。对于mapmediaquiz,这是

<script type="text/javascript" src="mediaquizcontent.js"> </script>

距离和公差

两个经纬度点之间距离的计算在前一章中已有描述。这里要解释的问题是关于如何进行距离的比较。对于测验应用程序,我需要编写代码来确定 Google 事件处理程序返回的位置是否足够接近指定问题的正确位置。变量maxdistance保存值,有时称为容差。这里是我的checkit函数的大部分代码。我已经忽略了switch语句,一旦确定玩家的猜测足够接近,它会对每种问题类型做不同的处理。

function checkit(clatlng) {
      var marker;
      var latlnga =new google.maps.LatLng(content[nextquestion][0],content[nextquestion][1]);

      var distance = dist(clatlng,latlnga);
                  eraseold();
                  marker = new google.maps.Marker({
            position: clatlng,
                                   title: "Your answer",
                        icon: bxmarker,
            map: map });

                   if (distance<maxdistance) {

           switch (content[nextquestion][3]) { ...
                        }  // end switch
                   asknewquestion();
      }  // end if (distance<maxdistance)
      else {
            answer.innerHTML= "Not close enough to the location.";
      }
}

用于创建 HTML 的正则表达式

正则表达式是描述用于检查和操作的字符串(文本)模式的强大工具。它是一种用于指定模式的完整语言。例如,为了让您对这个大主题有所了解,模式

/⁵[1-5]\d{2}-?\d{4}-?\d{4}-?\d{4}$/

可用于检测万事达卡号码。这些数字从 51 到 55 开始,后面是两个以上的数字,然后是三组四位数。该模式接受破折号,但不要求破折号。^符号意味着模式必须出现在字符串的开头,而$意味着它必须到达字符串的结尾。正斜杠(/)是模式的分隔符,反斜杠是转义符。从头开始解释这种模式如下:

  • ^:从字符串的开头开始。

  • 5:图案必须包含一个 5。

  • [1-5]:图案必须包含数字 1、2、3、4 或 5 中的一个。

  • \d{2}:模式必须正好包含两位数字。

  • -?:模式必须包含 0 或 1 -。

  • \d{4}:模式必须正好包含四位数字。

  • -?:模式必须包含 0 或 1 -。

  • \d{4}:模式必须正好包含四位数字。

  • -?:模式必须包含 0 或 1 -。

  • \d{4}:模式必须正好包含四位数字。

  • $:字符串结束。

万事达卡号码也必须遵守其他规则,你可以研究一下如何进一步验证它们。不要担心,我们将使用比这简单得多的正则表达式(也称为 regex )。

正则表达式的使用早于 HTML。可以在表单中使用正则表达式来指定输入的格式。对于这个应用程序,我们将对字符串使用replace方法,在一个长字符串中找到一小段特定文本的所有实例,并用其他内容替换它。我使用的一种说法是

videomarkup = videomarkup.replace(/XXXX/g,name);

这样做的是找到字符串XXXX的所有出现(这就是g所做的),并用变量name的值替换它们。

我可以并且可能应该更多地使用正则表达式来验证定义应用程序内容的数据。也许您想在自己的应用程序中尝试一下。

注意

在某种程度上,正确的决定可能是停止使用直接的 JavaScript 数组,包括使用并行结构,而使用 XML 或数据库。我不认为这是在这个应用程序中要求的,但我可能是错的。注意,使用 PHP 之类的语言进行服务器端编程,不管有没有数据库,都提供了一种隐藏数据的方法。

HTML5 标记和定位的动态创建

外部脚本语句引入测验应用程序的信息。现在是解释如何使用这些信息的时候了。init函数将调用一个名为loadcontent的函数。该函数调用makemap在指定的基准位置制作地图。

makemap(base[0],base[1]);

content数组从一个空数组开始。

var content = [];

顺便说一下,这不同于

var content;

您的代码需要使content成为一个数组。

然后,它使用一个for循环来迭代precontent的所有元素。for循环的开始将precontent的第 i 个元素添加到content数组中。

for (var i=0;i<precontent.length;i++) {
                content.push(precontent[i]);
                name = precontent[i][4];

下一行是一个switch语句的头,它使用内部数组中指示类型的元素作为条件。

switch (precontent[i][3]) {

对于videopictureaudio,代码创建一个div元素并定位它,使其向右浮动。然后,它在div元素中放置视频或音频的正确标记。那是什么标记?我有一些我称之为虚拟字符串的东西,它们有XXXX,视频或音频文件的实际名称将放在那里。把这些当做模板。我本来可以只用一个字符串来播放视频,但是它太复杂了,所以我决定用三个和两个来播放音频。这些字符串是

var videotext1 = "<video id=\"XXXX\" loop=\"loop\" preload=\"auto\" controls=\"controls\" width=\"400\"><source src=\"XXXX.webmpv8.webm\" type=\'video/webm\'>";

var videotext2="<source src=\"XXXX.theora.ogv\" type=\'video/ogg\'>  <source src=\"XXXX.mp4\"  type=\'video/mp4\'>";

var videotext3="Your browser does not accept the video tag.</video>";
var audiotext1="<audio id=\"XXXX\" controls=\"controls\" preload=\"preload\"><source
 src=\"XXXX.ogg\" type=\"audio/ogg\" />";
var audiotext2="<source src=\"XXXX.mp3\" type=\"audio/mpeg\" /><source src=\"XXXX.wav\"
 type=\"audio/wav\" /></audio>";

注意反斜杠(\)的使用。它告诉 JavaScript 按原样使用下一个符号,不要将其解释为正则表达式的特殊运算符。这就是屏幕中的引号如何成为 HTML 的一部分。

我的方法要求我确保视频和音频文件的名称遵循这种模式。这意味着 MP4 文件都需要只包含名字,没有内部点。

我使用正则表达式函数 replace 编写代码,从precontent数组中取出信息,并根据需要将它放入字符串中的任意位置。完整的switch语句是

switch (precontent[i][3]) {
                case "video":
                        divelement= document.createElement("div");
                        divelement.style = "float: right;width:30%;";
                        videomarkup = videotext1+videotext2+videotext3;
                        videomarkup = videomarkup.replace(/XXXX/g,name);
                        divelement.innerHTML = videomarkup;
                        document.body.appendChild(divelement);
                        videoreference = document.getElementById(name);
                        content[i][4] = videoreference;
                        break;
                case "pictureaudio":
                        divelement = document.createElement("div");
                        divelement.style = "float: right;width:30%;";
                        audiomarkup = audiotext1+audiotext2;
                        audiomarkup = audiomarkup.replace(/XXXX/g,name);
                        divelement.innerHTML = audiomarkup;
                        document.body.appendChild(divelement);
                        audioreference = document.getElementById(name);
                        savedimagefilename = content[i][5];
                        content[i][5] = audioreference;
                        imageobj = new Image();
                        imageobj.src= savedimagefilename;
                        content[i][4] = imageobj;
                        break;
                case "picture":
                        imageobj = new Image();
                        imageobj.src= precontent[i][4];
                        content[i][4] = imageobj;
                        break;
                }

注意,pictureaudio案例做了一些杂耍来创建引用新创建的音频元素和图像元素的内容元素。

然而,这还不足以确保视频和音频在所有浏览器上都显示在正确的位置。也就是说,它对一些人有效,但对另一些人无效。我决定准确定位音频和视频——也就是说,绝对定位。这需要在所有视频和音频元素的style元素中使用以下 CSS:

video {display:none; position:absolute; top: 60px; right: 20px;}
audio {display:none; position:absolute; top: 60px; right: 20px;}

音频的位置是用于音频控制的。

动态创建这些 HTML 元素有一个潜在的问题。你可能还记得,在家庭剪贴画的第二章中,有一段代码确保在对视频做任何事情之前加载了视频。我没有发现测验有任何问题,可能是因为回答问题需要足够的时间。尽管如此,我还是敦促你记住这个问题,并回头参考第二章。

提示按钮

你可以从我的代码中看出,我对于是提供一个提示还是帮助一个已经放弃的玩家很矛盾。在body元素中,我包括了

<button onClick="giveup();">Hint? </button>

giveup函数创建一个新的地图。也就是说,它使用makemap函数在同一个地方构造对不同 Google 地图的访问。它还删除了旧媒体,并将方向放入answer元素。

function giveup() {
        makemap(content[nextquestion][0],content[nextquestion][1]);
        eraseold();
        answer.innerHTML="Click at red x to finish this question.";
}

构建应用程序并使之成为您自己的应用程序

让应用程序成为你自己的第一步也是关键的一步是决定内容。使用各种媒体内容和各种图片尺寸(以及视频尺寸)有很多好处,但是有一个更简单的设计还是有好处的。

测验应用程序

下面是测验应用程序的快速摘要:

  1. init:执行初始化,包括调用loadcontent

  2. loadcontent:使用变量,最重要的是包含在外部脚本元素中的precontent数组,为媒体创建新的标记。它还援引了makemapquestions数组不需要更多的工作。

  3. makemap:引入地图并设置事件处理,包括对checkit的调用。

  4. asknewquestion:显示问题。

  5. checkit:将点击的位置与该问题的位置进行比较。

  6. dist:计算两个位置之间的距离。

  7. giveup:这是点击提示按钮的响应。一张新地图被带了进来。擦除所有媒体,并引导玩家点击显示的红色 x 附近。

  8. eraseold:删除当前正在播放的视频、音频或图片。

表 5-2 概述了测验应用程序中的功能。描述mapmediabase.html应用程序的调用/被调用和调用关系的函数表对所有应用程序都是相似的。

表 5-2

功能 在问答应用

|

功能

|

调用/调用者

|

打电话

|
| --- | --- | --- |
| init | 由<body>标签中的onLoad属性的动作调用 | loadcontentasknewquestion |
| makemap | 由loadcontent and giveup调用 |   |
| checkit | 由makemap中的addListener调用调用 | distasknewquestion, eraseold |
| dist | 由checkit调用 |   |
| loadcontent | 由init调用 | makemap |
| asknewquestion | 由initcheckit调用 |   |
| eraseold | 由checkitgiveup调用 |   |
| giveup | 通过按钮的动作调用 | eraseold, makemap |

表 5-3 显示了测验应用程序的代码。

表 5-3

地图问答程序的完整代码

|

代码行

|

描述

|
| --- | --- |
| <!DOCTYPE html> | HTML5 的 Doctype |
| <html> | html标签 |
| <head> | head标签 |
| <title>Map Quiz </title> | 完整的标题元素 |
| <meta charset="UTF-8"> | Meta 标签,HTML5 的标准 |
| <style> | style标签 |
| header {font-family:Georgia,"Times New Roman",serif; | 为语义元素 header 设置样式;字体家族将 Georgia 作为第一选择,Times New Roman 作为后备选择,默认 serif 作为下一个后备选择 |
| font-size:20px; | 相当大的字体 |
| display:block; | 在前后设置换行符 |
| } | 关闭样式指令 |
| video {display:none; position:absolute; top: 60px;``right: 20px; | 视频的样式指令;最初不显示 |
| } | 关闭视频指令 |
| audio {display:none; position:absolute; top: 60px;``right: 20px;} | 音频的样式指令;请注意,这是针对控件的;最初不显示 |
| canvas {position:relative; top:60px} | canvas元素的样式指令 |
| #answer {position:relative; font-family:Georgia,``"Times New Roman", Times, serif; font-size:16px;} | 右上方消息的样式指令 |
| </style> | 结束样式标签 |
| <script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"``type="text/javascript"></script> | Google Maps API 中引入的脚本元素;注意:您需要获得并使用自己的 API 密钥 |
| <script type="text/javascript" src="mediaquizcontent.js">``</script> | 带入mediaquizcontent.js中的内容 |
| <script type="text/javascript" charset="UTF-8"> | 开始脚本标记 |
| var listener; | 用于设置点击地图的谷歌地图事件 |
| var map; | 用来装地图 |
| var myOptions; | 保存地图规范的选项数组 |
| var ctx; | 画布的上下文 |
| var blatlng; | 基础latlng对象 |
| var content = []; | 一个空数组,将由loadcontent填充 |
| var answer; | 参考答案、说明 |
| var v; | 将保存对视频元素的引用 |
| var audioel; | 将保存对音频元素的引用(本测验只有一个) |
| var videotext1 = "<video id=\"XXXX\" preload=\"auto\" controls=\"controls\" width=\"400\"><source src=\"XXXX.mp4\" type=\'video/mp4; codecs=\"avc1.42E01E, mp4a.40.2\"\'>"; | 视频模板的第一部分 |
| var videotext2="<source src=\"XXXX.theora.ogv\" type=\'video/ogg; codecs=\"theora, vorbis\"\'><source src=\"XXXX.webmvp8.webm\" type=\'video/webm; codec=\"vp8, vorbis\"\'>"; | 视频模板的第二部分 |
| var videotext3="Your browser does not accept the video tag.</video>"; | 视频模板的第三部分 |
| var audiotext1="<audio id=\"XXXX\" controls=\"controls\" preload=\"preload\"><source src=\"XXXX.ogg\" type=\"audio/ogg\" />"; | 音频模板的第一部分 |
| var audiotext2="<source src=\"XXXX.mp3\" type=\"audio/mpeg\" /><source src=\"XXXX.wav\" type=\"audio/wav\" /></audio>"; | 音频模板的第二部分 |
| var nextquestion = -1; | 问题计数器需要在第 0 个之前开始 |
| function init() { | init功能的标题 |
| ctx = document.getElementById("canvas").getContext('2d'); | 将参考设置为canvas |
| answer = document.getElementById("answer"); | 将参考设置为answer |
| header = document.getElementById("header"); | 将参考设置为header(显示问题的位置) |
| loadcontent(); | 使用precontent数组创建内容 |
| asknewquestion(); | 调用函数来提问,从而开始测验 |
| } | 关闭init功能 |
| function asknewquestion() { | asknewquestion功能的标题 |
| nextquestion++; | 递增计数器 |
| if (nextquestion<questions.length) { | 如果还有更多问题 |
| header.innerHTML=questions[nextquestion]; | 显示问题 |
| } | 关闭if-still-more-questions子句 |
| else { | 其他 |
| header.innerHTML="No more questions."; | 不再显示问题 |
| } | 关闭else子句 |
| } | 关闭asknewquestion功能 |
| function loadcontent() { | loadcontent功能的标题 |
| var divelement; | 将保存对新创建的div元素的引用 |
| makemap(base[0],base[1]); | 为基准位置调用makemap |
| var videomarkup; | 视频元素的完整模板 |
| var videoreference; | 引用每个新创建的视频元素 |
| var audiomarkup; | 音频元素的完整模板 |
| var audioreference; | 引用每个新创建的音频元素 |
| var imageobj; | 图像对象 |
| var name; | 从 precontent 获得的名称,用于替换模板中的XXXX |
| var savedimagefilename; | 保存的图像文件 |
| for (var i=0;i<precontent.length;i++) { | 对于循环头,通过precontent |
| content.push(precontent[i]); | 添加到内容 |
| name = precontent[i][4]; | 提取名字 |
| switch (precontent[i][3]) { | 根据类型做switch |
| case "video": | 视频案例 |
| divelement= document.createElement("div"); | 创建一个div |
| divelement.style = "float: right;width:30%;"; | 将媒体放在右边 |
| videomarkup = videotext1+videotext2+videotext3; | 创建完整的模板 |
| videomarkup = videomarkup.replace(/XXXX/g,name); | 使用name进行更换 |
| divelement.innerHTML = videomarkup; | 将结果放入div |
| document.body.appendChild(divelement); | 将div添加到主体中(这样它就可以被访问),但是请注意,在它变得可见之前,它是不可见的 |
| videoreference = document.getElementById(name); | 对象引用 |
| content[i][4] = videoreference; | …并使其成为子数组的第四个元素 |
| break; | 离开switch(视频案例结束) |
| case "pictureaudio": | Pictureaudio案例 |
| divelement = document.createElement("div"); | 创建一个div |
| divelement.style = "float: right;width:30%;"; | 将媒体放在右边 |
| audiomarkup = audiotext1+audiotext2; | 创建完整的模板 |
| audiomarkup = audiomarkup.replace(/XXXX/g,name); | 使用name进行更换 |
| divelement.innerHTML = audiomarkup; | 将结果放入div |
| document.body.appendChild(divelement); | 将div添加到主体中(这样它就可以被访问),但是请注意,在它变得可见之前,它是不可见的 |
| audioreference = document.getElementById(name); | 对象引用 |
| savedimagefilename = content[i][5]; | 将当前第五元素放入savedimagefilename |
| content[i][5] = audioreference; | 使audioreference成为子数组的第五个元素 |
| imageobj = new Image(); | 创建图像对象 |
| imageobj.src= savedimagefilename; | 使其来源于savedimagefilename |
| content[i][4] = imageobj; | 使其成为子数组的第四个元素 |
| break; | 离开switch ( pictureaudio完成) |
| case "picture": | 相框 |
| imageobj = new Image(); | 创建图像对象 |
| imageobj.src= precontent[i][4]; | 设置其src |
| content[i][4] = imageobj; | 设置子数组的第四个元素指向图像 |
| break; | 离开switch(图片案例完成) |
| } | 关闭switch |
| } | 关闭for回路 |
| } | 关闭loadcontent功能 |
| var rxmarker = "rx1.png"; | 小红 x |
| var bxmarker = “bx1.png”; | 小黑 x |
| function makemap(mylat,mylong) { | makemap功能的标题 |
| var marker; | 将保存标记对象 |
| blatlng = new google.maps.LatLng(mylat,mylong); | 使用函数参数创建latlng对象 |
| myOptions = {  zoom: zoomlevel,      center: blatlng,    mapTypeId: google.maps.MapTypeId.ROADMAP    }; | 设置myOptions数组 |
| map = new google.maps.Map(document.getElementById("place"), myOptions); | 把地图拿进来 |
| marker = new google.maps.Marker({``position: blatlng, title: "center", icon: rxmarker, map: map }); | 创建标记 |
| listener = google.maps.event.addListener(map, 'click', function(event) { | 设置单击地图的事件 |
| checkit(event.latLng); | …事件处理程序是一个调用checkit的匿名函数 |
| }); | 失去功能并关闭对addListener的呼叫 |
| } | 关闭makemap |
| function eraseold() { | eraseold函数的头(代码与前面的例子相同,但现在在一个函数中) |
| if (v != undefined) { | 有没有一个古老的v定义? |
| v.pause(); | 暂停一下 |
| v.style.display = "none"; | 从显示中移除 |
| } | 关闭条款 |
| if (audioel != undefined) { | 有没有一个古老的audioel定义? |
| audioel.pause(); | 暂停一下 |
| audioel.style.display = "none"; | 抹掉上次播放的音频的控制 |
| } | 关闭条款 |
| ctx.clearRect(0,0,300,300); | 透明画布 |
| } | 关闭eraseold功能 |
| function checkit(clatlng) { | checkit的标题 |
| var marker; | 将在玩家设定的位置保持标记(黑色 x) |
| var latlnga =new google.maps.LatLng(content[nextquestion][0],content[nextquestion][1]); | 为这个问题的答案构建纬度-经度对象 |
| var distance = dist(clatlng,latlnga); | 计算距离 |
| eraseold(); | 调用该功能擦除当前显示的任何媒体 |
| var marker = new google.maps.Marker({``position: clatlng,``title: "Your answer",``icon: bxmarker,``map: map }); | 放置标记 |
| if (distance<maxdistance) { | 用户的点击是否足够接近? |
| switch (content[nextquestion][3]) { | 打开与此问题相关的类型 |
| case "video": | 视频案例 |
| answer.innerHTML=content[nextquestion][2]; | 显示答案(标题) |
| ctx.clearRect(0,0,400,400); | 清理画布 |
| v = content[nextquestion][4]; | 获取视频参考 |
| v.style.display="block"; | 让它可见 |
| v.currentTime = 0; | 在开始时设置 |
| v.play(); | 播放视频 |
| break; | 离开switch(视频案例完成) |
| case "picture": | 图片案例(将对图片音频案例使用一些编码) |
| case "pictureaudio": | Pictureaudio案例 |
| answer.innerHTML=content[nextquestion][2]; | 显示答案 |
| ctx.clearRect(0,0,400,400); | 清理画布 |
| var img1 = content[nextquestion][4]; | 获取图像 |
| var iw = img1.width; | 确定宽度 |
| var ih = img1.height; | 确定高度 |
| var aspect = iw/ih; | 计算方面 |
| if (iw>=ih) { | 如果宽度大于高度,那么宽度将是适合的因素 |
| if (iw>400){``tw = 400;``th = 400/aspect;``} | 如果宽度大于画布,计算目标尺寸 |
| else {``tw = iw;``th = ih;``} | 如果宽度不大于 400,目标是原始的 |
| } | 宽度较大时结束 |
| else { | 否则(高度是关键尺寸) |
| if (ih>400){``th = 400;``tw = 400*aspect;``} | 如果高度大于 400,计算目标尺寸 |
| else {``th = ih;``tw = iw;``} | 否则目标尺寸是原始尺寸 |
| } | 结束外部 else |
| ctx.drawImage(img1,0,0,iw,ih,0,0,tw,th); | 绘制从整个源到计算目标的图像 |
| if (content[nextquestion][3]=="picture") { | 如果这是图片… |
| break;} | 离开switch |
| else { | 否则需要显示和播放音频 |
| audioel = content[nextquestion][5]; | 提取元素 |
| audioel.style.display="block"; | 显示控件 |
| audioel.currentTime = 0; | 在开始时设置 |
| audioel.play(); | 玩 |
| break; | 离开开关 |
| } | 关闭其他未显示的图片 |
| } | 关闭开关 |
| asknewquestion(); | 问一个新问题(仅当用户的猜测足够接近时) |
| } | 在maxdistance内关闭 |
| else { | 其他 |
| answer.innerHTML= "Not close enough to the answer."; | 显示消息 |
| } | 关闭else |
| } | 关闭checkit |
| function dist(point1, point2) { | dist功能的标题 |
| var R = 6371; // km | 用于km的值 |
| // var R =  3959; // miles | 在代码中保留注释,以便轻松切换到英里 |
| var lat1 = point1.lat()*Math.PI/180; | 计算弧度 |
| var lat2 = point2.lat()*Math.PI/180 ; | 计算弧度 |
| var lon1 = point1.lng()*Math.PI/180; | 计算弧度 |
| var lon2 = point2.lng()*Math.PI/180; | 计算弧度 |
| var d = Math.acos(Math.sin(lat1)*Math.sin(lat2) +``Math.cos(lat1)*Math.cos(lat2) *``Math.cos(lon2-lon1)) * R; | 使用余弦定律的标准计算 |
| return d; | 返回距离 |
| } | 关闭功能 |
| function giveup() { | giveup函数的标题(用于提示) |
| makemap(content[nextquestion][0],content[nextquestion][1]); | 引入以答案为中心的新地图 |
| eraseold(); | 擦除任何旧媒体 |
| answer.innerHTML="Click at red x to finish this question."; | 显示说明,因为玩家需要点击进行;这给了玩家一种方式来表明他们有新的地图 |
| } | 关闭giveup |
| </script> | 关闭script元素 |
| </head> | 关闭head元素 |
| <body onLoad="init();"> | 正文标签;加载时调用init |
| <header id="header">Click</header> | 标题元素 |
| <div id="place" style="float: left;width:50%; height:400px"></div> | 地图的位置 |
| <button onClick="giveup();">Hint? </button> | 按钮表示需要帮助 |
| <div style="float: right;width:30%;height:400px"> | 保留其余元素 |
| <div id="answer">Starting  location</div> | 有答案,那是位置的标题 |
| <p>  </p> | 间隔 |
| <canvas id="canvas" width="400" height="400" > | 帆布 |
| Your browser doesn't recognize canvas | 旧浏览器的标准 |
| </canvas> | 关闭canvas元素 |
| </div> | 关闭div |
| </body> | 关闭body |
| </html> | 关闭html |

测试和上传应用程序

这一章有一个应用,一个地理测验。它由两个文件组成,一个(mapmediaquiz.html)包含 HTML、CSS 和大部分代码,另一个(mediaquizcontent.js)包含表示内容的 JavaScript。中的编码。js 文件引用了媒体。我包含了两个视频剪辑的标准视频文件集、单个音频剪辑的标准音频文件和两个图像文件。我用一个手绘的红色小 x和一个手绘的黑色小 x 来标记地图上的位置,而不是谷歌地图中默认的泪珠形状。我再重复一遍:如果不获取自己的 API 密匙并更改* script 元素,你将无法运行源代码。您可以并且应该替换您自己的问题、答案(位置)和媒体,但一定要注意大小和形状问题,并检查我的处理以适应任何大的图像文件。*

摘要

在本章中,您继续使用 Google Maps API。您学习了如何执行以下操作:

  • 管理地理测验。

  • 使用问题、位置和媒体的规范来动态创建 HTML 元素。

  • 编写 Google Maps API 事件处理程序,以检测用户是否靠近有视频、音频和图像或者只有图像的位置。

  • 将媒体内容的定义与节目本身分开。

  • 使用正则表达式生成正确的标记。

  • 开始和停止媒体的显示和播放。

在下一章中,你将会读到一个叫做添加到 15 的游戏的实现。主要是一个使用数组和字符串的练习。

六、相加到 15(AddTo15)游戏

在本章中,您将学习以下内容:

  • 将一个已知的真实世界游戏实现为一个数字程序,其中“计算机”是玩家之一

  • 为“计算机”制定战略

  • 插入暂停

  • 使用数组和字符串

介绍

两人游戏加到 15 要求玩家轮流从数字 1 到 9 中选择,目标是获得三个加起来等于 15 的数字。我第一次看到这个游戏是在纽约市的数学博物馆,在那里它被实现为一个装置,杆上的数字可以从中心移动到玩家这边。如果玩家赢了,就会有灯光和响亮快乐的声音。如果没人赢,会有更短更安静的声音。这个游戏也可以用一副牌中的 1 到 9 张牌或者用纸笔来玩。这个游戏可以被描述为完美知识之一:过去移动的结果和当前的可能性都是可见的。这个游戏相当于一个众所周知的儿童游戏,我将识别这个游戏并向读者证明它的等价性。(你可以在本章的源代码中找到对此的解释。)

对于本章的例子,我选择让程序管理游戏并且扮演一个玩家的角色。这意味着我需要为“计算机”制定一个策略。我的策略很好,但是玩家仍然有可能赢。后来,在我的工作中,我决定我需要在“计算机”移动之前插入一个停顿,以便人类玩家能够像与对手游戏一样体验这个程序。

图 6-1 显示游戏的开启窗口。

img/272384_2_En_6_Fig1_HTML.jpg

图 6-1

打开窗户

图 6-2 显示了玩家和“计算机”各自移动后的结果。

img/272384_2_En_6_Fig2_HTML.jpg

图 6-2

玩家和电脑移动后

最后,我展示了一个截图,图 6-3 ,这似乎是最常见的结果:数字用尽,没有人赢。在我家,这被描述为“猫赢了”,所以我用这个词来描述告诉结果的消息。

img/272384_2_En_6_Fig3_HTML.jpg

图 6-3

比赛以平局结束

这一章是游戏实现的案例研究,包括用户界面和策略的实现。使用数组以及数组间的引用是非常重要的。

游戏的一般要求

Add to 15 程序和其他类似程序的要求是为玩家提供一个相当直观的界面。对手,我称之为“计算机”,尽管我不喜欢拟人化一台机器,需要有一个策略。我在本章中描述的程序有一个相当强硬的策略。我想我战胜了它,但不是经常。这个程序可能的改进是开发出最佳的策略,使“计算机”永远不会输,尽管可能会打成平手,以及其他不太熟练的策略。有了一组选项,一个增强将是给人类玩家一个为他们的对手挑选技能水平的选择。这需要制定一系列策略,也许包括随机行动。

我的这个程序的第一个版本让“计算机”的移动几乎与玩家的移动同时出现。我插入了一个暂停来给游戏一个我认为更好的“触感”。这种做法适用于许多游戏。当我们在现实世界中玩游戏时,我们不会有意识地暂停,但在数字世界中实现游戏可能需要明确地关注时间。

加到 15 非常简单,因此可以列出所有加到 15 的可能组合。事实上,有 8 个,所以我的程序有一个数组,它的元素是保存有效组的数字的字符串,例如“3 5 7”。(实际上,该数组有九个元素,第一个是空的占位符,因此索引可以从 1 开始,而不是从 0 开始。)游戏的管理和“计算机”策略的实施可以使用该数组中的信息来构建。您将不会看到任何将数字相加的代码!

我的程序有另一个有九个元素的数组,每个数组都有指向该数字属于八个列表中的哪些组的元素。我的程序有一个棋盘数组,从所有九个数字开始;玩家的数组,最初为空;和“计算机”的数组,最初也是空的。玩家和计算机的数组有九个元素,第一个元素没有使用,它表示八种组合中每种组合有多少个元素。

程序有两个不变的数组:groupsoccupied。它也有一个数组,numbers,在开始时创建,但之后不再更改。有五个数组会发生变化:boardcomputerplayerpgroupcountcgroupcount。在下一节中,您将看到这些工具的使用。数组的使用和前后指向是这类应用程序的典型特征。有冗余,但它简化了编码。

HTML5、CSS 和 JavaScript

在这一节中,我将解释用于完成 Add to 15 项目需求的特性。

CSS 中的样式

包含九个数字的椭圆形、红色边框、黄色背景元素被动态创建为span元素。设置外观的 CSS 是

span {
   position:absolute;
   top:180px;
   border-style: solid;
   color: red;
   border-radius: 25px;
   background-color: yellow;
   padding: 5px;
   cursor: pointer;
}

用绝对定位动态创建这些元素意味着它们可以很容易地从棋盘上移到玩家或“计算机”的部分。使类型spandiv相反意味着没有强制换行,它们可以彼此相邻。顺便说一下,我区分填充(元素内部)和边距(元素外部)的技巧是考虑填充的单元格。

JavaScript 数组

正如已经讨论过的,一组数组用于游戏的操作。一些数组在 0 索引位置有一个未使用的槽,只是为了使编码更容易。groups数组保存了总共 15 种可能的组合:

var groups = [
  "  ",               //placeholder, not used
  "3 4 8",
  "1 5 9",
  "2 6 7",
  "1 6 8",
  "3 5 7",
  "2 4 9",
  "2 5 8",
  "4 5 6"
];

可以被视为冗余信息的occupied数组使得某些计算变得更加容易。我确实决定忍受零基索引。被占用的数组用于指示从 0 到 9 的每个值属于哪个组。更具体地,第 N 个子阵列中的值对应于保持 N+1 的组的索引。这是被占用的数组,我稍后会给出一些例子。

var occupied = [  //indexed subtracting 1
  [2, 4],
  [3, 6, 7],
  [1, 5],
  [1, 6, 8],
  [2, 5, 7, 8],
  [3, 4, 8],
  [3, 5],
  [1, 4, 7],
  [2, 6]
];

所以数字 1 与数组[2,4]相关联。这表明 1 属于第二组“1 5 9”,第四组“1 6 8”。数字 5 属于第二、第五、第七和第八组。groupsoccupied数组不变。

boardplayercomputer数组保存棋盘上的数字,由玩家选择,或者为“计算机”选择。所以最初的声明是

var player = [];
var computer = [];
var board = [1,2,3,4,5,6,7,8,9];

最后两个数组记录玩家和“计算机”距离完成八个组合中的每一个有多近。所以最初的声明是

var pgroupcount = [0,0,0,0,0,0,0,0,0];  //unused first slot
var cgroupcount = [0,0,0,0,0,0,0,0,0];  //unused first slot

在图 6-2 所示的游戏中,“计算机”持有一个 2。查看occupied数组,2 出现在组 3、6 和 7 中。cgroupcount应该是[0,0,0,1,0,0,1,1,0]。玩家先选了 5,那么pgrounpcount数组就是[0,0,1,0,0,1,0,1,1]。

如果玩家然后选择 6,pgroupcount将是[0,0,1,1,1,1,0,1,2]。在任一组的计数数组中出现 2 表示有机会获胜——如果玩家有 2 个组成员,获得第三个意味着获胜——或者需要阻挡——如果“计算机”有 2 个组成员,它可以在下一步中获胜。我的代码必须确定丢失数字的身份,并检查它是否还在棋盘上(在board数组中)。

有了这些阵列的基础设施,我可以解释如何响应玩家的移动,生成“计算机”的移动,并确定游戏是赢了还是结束了。

设置游戏

setUpBoard函数创建代表九个数字的九个span元素。对这九个元素的引用保存在一个名为numbers的数组中。为元素设置了一个额外的属性,名为n,用于保存具体的数字。作为创建过程的一部分,使用一个for循环来实现,为“点击”事件调用addEventListener方法,并设置为当玩家点击数字时调用addToPlayer函数。

一旦创建,该数组就不会改变。改变的是每个元素的位置,由style.leftstyle.top属性表示。

响应玩家的移动

响应玩家移动的关键功能是addToPlayer。您可以将addToPlayer函数视为执行内务处理类型的操作,更新各种数组。选择的数字被添加到player数组中。调用函数take,从board数组中移除元素。通过改变style.top属性来重新定位对应于该数字的span元素。需要更改playerboard数组,但不会改变窗口中数字元素的位置。

本地变量holder被设置为保存包含数字的组。回想一下,occupied数组是包含该信息的数组的数组。我使用一个for循环来遍历holder并更新pgroupcount。我的代码检查是否有三个计数。这将表明玩家获胜。如果不是这样,addToPlayer函数在调用computerMove之前执行一个setTimeout语句来暂停。

addToPlayer函数有一行,其中点击一个块的事件被停止:

ev.target.removeEventListener("click",addToPlayer);

这防止了玩家点击已经被玩家拿走的棋子的不良行为。我必须承认,我最初确实注意到了这个问题。

生成计算机移动

暂停后调用computerMove功能。我在computerMovesmartChoice之间分配了任务。computerMove函数调用smartChoice函数。computerMove功能主要执行与addToPlayer功能类似的内务处理任务。我注意到,虽然我的程序让玩家先玩,但是computerMove代码确实会检查棋盘是否是空的。

smartChoice程序使用数组进行以下操作:

  1. 棋盘上还有哪个数字(在board数组中)能让电脑赢得游戏吗?

  2. 假设不可能立即获胜,棋盘上是否有任何数字意味着玩家可以立即获胜?如果是,播放该号码以阻止玩家。

  3. 假设不需要立即块,是否有任何一个组的一个元素已经被计算机播放,而其他两个元素都没有被播放器播放?如果是这样,请从两个可用号码中选择一个。

  4. 假设前面的情况都不适用,而 5 是可用的,就拿它。

  5. 假设前面的情况都不适用,取偶数。

  6. 从剩下的数字中随机选择一个。

因此,通过提供更好和/或更多的策略来增强计划将涉及到改变smartChoice

类似于addToPlayer中的动作,computermove函数有一行代码用于删除点击已经播放过的片段的事件处理:

numbers[n-1].removeEventListener("click",addToPlayer);

这防止了玩家点击已经由计算机播放的片段的不良行为。

addToPlayer函数一样,computerMove函数可以确定游戏是以计算机获胜还是平局结束。

构建应用程序并使之成为您自己的应用程序

您可以通过改进策略和/或添加不同的策略来使该应用成为您自己的应用。你可以向前看第九章,在那里描述了名为localStorage的 HTML5 设施,并思考如何将它融入到游戏中。本章的主要目的是提供使用交叉引用数组的经验。另一个挑战是提供一种无需重新加载就能重复游戏的方法。你可以在第八章中看到拼图变成视频的例子。另一个增强是记录移动的顺序,可能使用localStorage,这样你可以尝试不同的策略。

表 6-1 列出了所有的功能,并指出它们是如何被调用的以及它们调用了什么功能。

表 6-1

功能 中添加到 15 个项目

|

功能

|

调用/调用者

|

打电话

|
| --- | --- | --- |
| init | 由<body>标签中的onLoad属性的动作调用 | setUpBoard |
| setUpBoard | init |   |
| computerMove | 由setTimeout的动作调用,在addToPlayer中调用 | smartChoicetake |
| smartChoice | computerMove |   |
| take | addToPlayercomputerMove |   |
| addToPlayer | 由点击事件的动作addEventListener调用 | take |

表 6-2 显示了 Add to 15 游戏的代码,每一行都有注释。

表 6-2

添加到 15 应用程序的完整代码

| 代码行 | 描述 | | `` | 页眉 | | `` | 开始`html`标签 | | `` | 开始`head`标签 | | `Add to 15` | 完整标题 | | `` | 什么时候 | | `` | 关闭样式元素 | | `` | 关闭`script`标签 | | `` | 正文标签 | | `

Player against Computer


` | 页眉 | | `Player goes first: click on number. First to have a set of 3 adding to 15 wins. Reload for new game.` | 说明 | | `

` | 间隔 | | `Computer` | 计算机区域 | | `


` | 间隔 | | `

` | 间隔 | | `
` | 水平标尺 | | `Board` | 电路板区域 | | `



` | 间隔 | | `
` | 水平规则 | | `Player` | 玩家区 | | `




` | 间隔 | | `
` | 水平规则 | | `
` | `Div`为状态 | | `
` | 关闭`div` | | `` | 关闭`body`标签 | | `` | 关闭`html`标签 |

测试和上传应用程序

这个应用程序的源材料只包含一个 HTML 文档。原始资料包含一个关于 Add to 15 游戏问题的 Word 文档。

摘要

在这一章中,你研究了如何通过给单人游戏者提供一个对手并管理游戏来实现双人游戏。您了解并获得了以下经验:

  • 定义和操作数组

  • 如何为玩家建立一个用户界面,包括为点击“棋盘”上的物体设置事件和编程暂停

  • 对运动员的不良行为采取预防措施

在下一章,我们将进入空间迷人的折纸世界。我们探索如何使用线条画、视频剪辑和在画布上绘制照片来制作一个会说话的鱼的折纸模型。这些技术可以应用于不同类型的方向。

七、折纸方向(OrigamiDirection):使用基于数学的线条画、照片和视频

在本章中,您将学习以下内容:

  • 如何利用数学编写 JavaScript 函数产生精确的线条画

  • 一种将线条画、照片、视频以及用于顺序说明的文本结合起来的方法

  • 一种通过让您逐步进行,甚至返回并插入或更改以前的工作来促进开发的方法

介绍

本章的项目是折叠一个折纸模型,一条会说话的鱼的一系列指导。但是,您可以阅读任何主题,只要您想向查看者呈现一系列图表,包括向前和向后移动的能力,以及由线条画或来自文件或视频剪辑的图像组成的图表。

注意

折纸指的是折纸艺术。它通常与日本联系在一起,但也起源于中国和西班牙。传统的褶皱包括水弹、鹤和振翅鸟。莉莲·奥本海默被认为是在美国普及折纸艺术的功臣,并创立了后来成为美国国家组织“折纸美国”的组织。她在 1972 年亲自教我名片蛙。本章的下载中包含了一个用于名片青蛙的 HTML5 程序。折纸是一种活跃在世界各地的艺术形式,也是数学、工程和计算复杂性研究的焦点。

图 7-1 显示了会说话的鱼应用程序origamifish.html的打开屏幕。屏幕上显示的是折纸图的标准惯例,我修改后加入了颜色。标准的折纸叫做 kami,一面是白色的,另一面是非白色的。

img/272384_2_En_7_Fig1_HTML.jpg

图 7-1

打开屏幕

注意

我减少了折纸动作。例如,我省略了反向折叠的表示,它用于将嘴唇翻过来。这些褶皱之前通常是所谓的预备褶皱,这是我为会说话的鱼描述的。

文件夹可以点击下一步(在序列的这一点上,返回不做任何事情)到达指令的第一个实际步骤,如图 7-2 所示。当然,可以添加编程来删除开始时的返回按钮和结束时的下一步按钮。

img/272384_2_En_7_Fig2_HTML.jpg

图 7-2

第一步,展示纸的正方形。说明书上说要翻纸。

跳过前面,图 7-3 显示了折叠的后续步骤。请注意,纸张的彩色面显示出来了。未折叠的折叠线由细垂直线表示,接下来要进行的折叠(向下折叠拐角)由右上角的彩色虚线对角线表示。

img/272384_2_En_7_Fig3_HTML.jpg

图 7-3

将一个角向下折叠到折叠线

在模型构建的后期,文件夹必须执行汇折叠。这被认为是一个困难的举动。图 7-4 显示了在下沉之前所谓的褶皱模式:褶皱显示为山褶皱或谷褶皱。

img/272384_2_En_7_Fig4_HTML.jpg

图 7-4

水槽标准图步骤

我决定用一个展示水槽步骤的视频剪辑来补充线条画。图 7-5 显示了视频中的一帧。我(文件夹)使用了视频控件来暂停动作。文件夹可以重放视频剪辑,并重复多次回到折痕模式。

img/272384_2_En_7_Fig5_HTML.jpg

图 7-5

显示下沉步骤的暂停视频

下沉仍然是一个挑战,但观看视频剪辑可以有所帮助。文件夹可以重放和暂停视频剪辑。图 7-6 显示了水槽后的下一步。从线条画到视频剪辑再到线条画对用户/文件夹来说很容易,对开发者来说也很简单。

img/272384_2_En_7_Fig6_HTML.jpg

图 7-6

水槽后的步骤(第一个视频剪辑)

下一步要求折叠器向后折叠右边的三角形口盖,分开角度。请注意,角度由一条弧线表示。

在折叠过程中,有一个步骤,我认为一两张照片是传达需要做的事情的最佳方式。图 7-7 显示了从上方(从鱼的喉咙向下看嘴)看到的正在制作的模型的图片。

img/272384_2_En_7_Fig7_HTML.jpg

图 7-7

显示鱼喉咙的照片

图 7-8 显示了按照图 7-7 所示方向将折叠好的材料移动到一边的结果。

img/272384_2_En_7_Fig8_HTML.jpg

图 7-8

喉咙固定的鱼的照片

说明以另一个视频剪辑结束,这个视频剪辑显示了鱼在说话,通过轻轻按下文件夹的顶部和底部来完成。图 7-9 显示了视频中的一帧。

img/272384_2_En_7_Fig9_HTML.jpg

图 7-9

显示会说话的鱼的视频

关键要求

折纸方向有一个标准格式,通常被称为图表,我是在这个标准的基础上建立的。在这种方法中,每一步都显示了下一次使用排版制作的折叠。最基本的褶皱在展开时要么呈山谷状,要么呈山形,这用虚线或点划线表示。通常,折叠是在制作折纸模型的过程中展开的。有时有褶皱的地方用细线表示,有时用虚线表示山谷褶皱,用点划线表示山脉褶皱。

我的目标是绘制线条画,就像在书上看到的那样,计算临界点和线的坐标位置。我不想手工绘制图纸并扫描它们,也不想使用典型的工程 CAD 程序。我不想测量和记录长度或角度,而是让 JavaScript 替我完成这项任务。就像折纸行话所说的那样,这甚至适用于为“品味”而做的折叠,因为我可以确定我选择使用的确切位置。使用基本的代数、几何和三角学提供了一种通过计算线端点的坐标来获得线条画的精确位置的方法。

折纸的步骤通常带有文字说明。此外,有时会使用箭头。我想在遵循标准的同时,利用这些指令将在计算机上传递的优势,并为其他媒体提供颜色和机会。

考虑到会说话的鱼和其他一些褶皱,我决定使用照片和视频来进行线条画可能对你来说不够好的操作。

注意

我给自己设定的折纸图的挑战是既要遵循标准,又要利用 HTML5 的新技术。当转向新的媒体和技术时,这是典型的。你不想放弃一个你的读者可能认为是必要的标准,但是你也想使用那些可以解决实际问题的标准。

一个更微妙的需求是,我希望在开发应用程序时对其进行测试。这意味着一种灵活但健壮的方式来指定步骤。

HTML5、CSS、JavaScript 特性和数学

我现在将描述 HTML5 的特性和用于解决 origami directions 项目需求的编程技术。最好的方法是从表示步骤的整体机制开始,然后解释我是如何导出位置的第一组值的。然后我将解释用于绘制山谷、山脉和箭头,以及用于计算交点和比例的实用函数。最后,我将简要回顾图像的显示和视频的播放。

步骤的总体机制

折纸方向的步骤由一个名为steps的数组指定。数组的每个元素本身是一个两元素数组,包含一个函数名和一段将出现在屏幕上的文本。origamifish.htmlsteps数组的最终值如下:

var steps= [
   [directions,"Diagram conventions"],
                [showkami,"Make quarter turn."],
   [diamond1,"Fold top point to bottom point."],
   [triangleM,"Divide line into thirds and make valley folds and unfold "],
   [thirds,"Fold in half to the left."],
   [rttriangle,"Fold down the right corner to the fold marking a third. "],
   [cornerdown,"Unfold everything."],
   [unfolded,"Prepare to sink middle square by reversing folds as indicated ..."],
   [changedfolds,"note middle square sides all valley folds, some other folds changed.

 Flip over."],
   [precollapse,"Push sides to sink middle square."],
   [playsink,"Sink square, collapse model."],
   [littleguy,"Now fold back the right flap to center valley fold. You are bisecting the

 indicated angle."],
   [oneflapup,"Do the same thing to the flap on the left"],
   [bothflapsup,"Make fins by wrapping top of right flap around 1 layer and left around

 back layer"],
   [finsp,"Now make lips...make preparation folds"],
   [preparelips,"and turn lips inside out. Turn corners in..."],
   [showcleftlip,"...making cleft lips."],
   [lips,"Pick up fish and look down throat..."],
   [showthroat1,"Stick your finger in its mouth and move the inner folded material to one

    side"],
   [showthroat2,"Throat fixed."],
   [rotatefish,"Squeeze & release top and bottom to make fish's mouth close and open"],
   [playtalk,"Talking fish."]
   ];

当我开始构建应用程序时,我没有想到steps数组。取而代之的是,我在进行的过程中添加了steps数组,包括插入新条目和更改内容和/或函数名。我从下面的steps数组的定义开始:

var steps= [
                     [showkami,"Make quarter turn"],
                     [diamond,"Fold top point to bottom point."]
                  ];

我花了一些时间进入展示折叠最后阶段的节奏,并为下一步添加了标记。最终结果是一个使用单个 HTML 页面的演示文稿,该页面包含 21 个步骤,包含矢量图、照片和视频,遵循与 PowerPoint 演示文稿类似的格式,即能够前进或后退。

前进和后退由功能donextgoback完成。但首先我需要解释整件事是如何开始的。就像到目前为止所有项目的情况一样,名为init的函数由<body>标签中的onLoad属性的动作调用。代码设置全局变量并调用函数来呈现下一步donextinit的功能是

function init() {
   canvas1 = document.getElementById("canvas");
   ctx = canvas1.getContext("2d");
   cwidth = canvas1.width;
   cheight = canvas1.height;
   ta = document.getElementById("directions");
   nextstep = 0;
   ctx.fillStyle = "white";
   ctx.lineWidth = origwidth;
   origstyle = ctx.strokeStyle;
   ctx.font = "15px Georgia, Times, serif";
   donext();
}

变量nextstep可以说是指向steps数组的指针。我从零开始。

donext函数的任务是展示制作折纸模型的步骤中的下一步。该功能从检查它是否在范围内开始;也就是说,如果它已经递增到超过了steps数组的末尾,则nextstep的值被设置为最后一个索引。接下来,该功能暂停并删除显示的最后一个视频。它将画布恢复到其最大高度,在播放视频剪辑时,我的代码可能会改变这一高度。该函数还将video变量设置为undefined,因此不必为该视频再次执行删除语句。在所有情况下,donext清除画布并重置linewidth。然后donext功能显示下一步。显示器包括多个部分:由线条画、视频或图像组成的图形部分和由说明组成的文本部分。donext函数调用内部数组第一个(即第 0 个)元素指示的绘图函数:

steps[nextstep][0]();

并使用内部数组的第二个(即第一个)元素显示文本:

ta.innerHTML = steps[nextstep][1];

donext函数中的最后一条语句是递增指针。整个donext功能是

function donext() {
   if (nextstep>=steps.length) {
      nextstep=steps.length-1;
   }
   if (v) {
     v.pause();
     v.style.display = "none";
     v = undefined;
     canvas1.height = 480;
   }
   ctx.clearRect(0,0,cwidth,cheight);
   ctx.lineWidth = origwidth;
   steps[nextstep][0]();
   ta.innerHTML = steps[nextstep][1];
   nextstep++;
}

编写goback函数所花费的思考时间比它的大小所暗示的要长得多。nextstep变量保存下一步的索引。这意味着返回需要变量递减 2。必须检查指针是否太低,即小于零。最后,goback函数调用donext来显示已经被设置为nextstep的内容。代码是

function goback() {
   nextstep = nextstep -2;
   if (nextstep<0) {
        nextstep = 0;
   }
   donext();
}

用户界面

我称之为文件夹的用户有两个按钮,标记为下一步和返回。它们是使用 HTML5 按钮元素实现的,并分别调用gobackdonext函数。我选择两种不同颜色的按钮——红色代表返回,绿色代表下一步——可以讨论,因为措辞不一致。然而,它确实给了我一个机会来提醒您在名称层叠样式表层叠这个词的意义。我在head元素的style元素中使用了一个指令,然后我还在 body 元素中使用了以下标记:最后一个style指令是控制按钮并赋予按钮颜色的指令。

<button onClick="goback();" style="color: #F00">Go back </button>
<button onClick="donext();" style="color: #03F">Next step </button>

颜色名称,每个只有三个字符,相当于#FF0000#0033FF

这两节已经描述了顺序方向的基本机制。它假设每一步都由一个函数和文本来表示。下一节将展示坐标值是如何设置的。

坐标值

线条绘制是使用 HTML5 canvas 函数和变量完成的,主要指示 x 和 y 值。变量在代码中以带有初始化的var语句出现。我是在一步一步地创建模型的过程中写下这些语句的,尽管就 JavaScript 而言,它们是常量,值是在程序加载时设置的。图 7-10 显示了顺序的第三步,并标注了点 a、b、c 和 d

img/272384_2_En_7_Fig10_HTML.jpg

图 7-10

角落标签

我如何确定这四个点的坐标?作为基础,我指定了点 a 的位置。我还指定了纸张的宽度和高度为四英寸,英寸到像素的转换为 72。变量声明是

var kamiw = 4;
var kamih = 4;
var i2p = 72;
var ax = 10;
var ay = 220;

变量名kamiwkamih是指折纸用的标准方形纸的宽度和高度。从现在开始,一切都是计算好的。所需的第一个值是纸张对角线的大小。对于正方形,使用勾股定理,对角线是边长乘以 2 的平方根。以下设置变量diag的语句将边(kamiw)乘以 2 的平方根和表示英寸到像素转换的因子。

var diag = kamiw* Math.sqrt(2.0)*i2p;

大多数其他编程语言包含许多标准数学函数的内置代码,因此程序员不必重新发明轮子。在 JavaScript 中,这些通常作为Math类的方法提供。你可以在网上搜索以确定确切的名称和用法。

这样,位置 b、c 和 d 的值是使用现有值的表达式。

var bx = ax+ .5*diag;
var by = ay - .5*diag;
var cx = ax + diag;
var cy = ay;
var dx = bx;
var dy = ay + .5*diag;

我通过建立模型并确定新头寸如何基于旧头寸,开发了变量的表达式。这些变量被在steps数组中指定的函数用来绘制表示模型边缘、折叠线、箭头和角度的线条。一些计算使用了通用的数学公式。接下来的两节介绍了实用函数:阶跃函数使用的函数。

显示器的实用功能

如图 7-1 所示,山谷褶皱由虚线组成。山脉褶皱是由点和虚线组成的线来表示的。这是折纸方向的标准惯例,并使文件夹能够遵循不同语言书籍中的方向。其中一种可以是默认颜色(黑色)或另一种颜色。我需要为基础设置变量:破折号长度、点长度、两个破折号之间的间隔、点之间的间隔以及最后一个点和破折号之间的间隔。通过首先查看函数,然后定义必要的值,最容易理解需要什么。valley功能定义如下:

function valley(x1,y1,x2,y2,color) {
   var px=x2-x1;
   var py = y2-y1;
   var len = dist(x1,y1,x2,y2);
   var nd = Math.floor(len/(dashlen+dgap));
   var xs = px/nd;
   var ys = py/nd;
   if (color) ctx.strokeStyle = color;
   ctx.beginPath();
   for (var n=0;n<nd;n++) {
      ctx.moveTo(x1+n*xs,y1+n*ys);
      ctx.lineTo(x1+n*xs+dratio*xs,y1+n*ys+dratio*ys);
   }
   ctx.closePath();
   ctx.stroke();
   ctx.strokeStyle = origstyle;
}

valley功能决定有多少个破折号。这是通过将谷线的长度除以破折号的总长度和破折号之间的间隙来实现的。如果这不是一个整数,则最后的部分破折号组合将被删除。Math.floor方法为我们完成了这个任务。Math.floor(4.3)回报 4。

变量xsys分别是 x 和 y 的增量。color参数可能存在,也可能不存在。如果参数存在,if (color)语句改变笔画颜色。该函数的核心是绘制每个破折号的for循环。

mountain功能类似,但更复杂,因为山形褶皱排版的性质:破折号的组合后跟一个等于点的间隙,然后是一个点,然后是另一个间隙。mountain的功能如下:

function mountain(x1,y1,x2,y2,color) {
   var px=x2-x1;
   var py = y2-y1;
   var len = dist(x1,y1,x2,y2);
   var nd = Math.floor(len/ddtotal);
   var xs = px/nd;
   var ys = py/nd;
   if (color) ctx.strokeStyle = color;
   ctx.beginPath();
   for (var n=0;n<nd;n++) {
      ctx.moveTo(x1+n*xs,y1+n*ys);
      ctx.lineTo(x1+n*xs+ddratio1*xs,y1+n*ys+ddratio1*ys);
      ctx.moveTo(x1+n*xs+ddratio2*xs,y1+n*ys+ddratio2*ys);
      ctx.lineTo(x1+n*xs+ddratio3*xs,y1+n*ys+ddratio3*ys);
   }
   ctx.closePath();
   ctx.stroke();
   ctx.strokeStyle = origstyle;
}

记住函数的语句,下面是我如何定义两个函数使用的变量:

var dashlen = 8;
var dgap = 2.0;
var ddashlen = 6.0;
var ddot = 2.0;
var dratio = dashlen/(dashlen+dgap);
var ddtotal = ddashlen+3*ddot;
var ddratio1 = ddashlen/ddtotal;
var ddratio2 = (ddashlen+ddot)/ddtotal;
var ddratio3 = (ddashlen+2*ddot)/ddtotal;

线条用来显示纸的边缘。我将这些线条的宽度设置为 2。对于纸被折叠然后展开的地方,我使用一条更细的线:线宽设置为 1。我写了一个函数来制作细线:

function skinnyline(x1,y1,x2,y2) {
   ctx.lineWidth = 1;
   ctx.beginPath();
   ctx.moveTo(x1,y1);
   ctx.lineTo(x2,y2);
   ctx.closePath();
   ctx.stroke();
   ctx.lineWidth = origwidth;
}

在折纸鱼的方向上,我决定使用短的向下的箭头。我为它编写了一个通用函数,您可以在“构建应用程序并使之成为您自己的应用程序”一节中的注释代码中研究这个函数。有两个地方,当我决定显示一个长的弯曲箭头,无论是水平的还是垂直的。这被证明是项目中最长的函数,我在这里就不赘述了。您可以在完整的注释代码清单中研究该函数。用你选择的饮料来增强体力。这是一个复杂的函数,因为许多情况需要分别处理:向上或向下的垂直箭头,或者从左到右或从右到左的水平箭头。箭头是一个圆的圆弧,其圆心被计算为远离该圆弧,两条小直线指示箭头。

用于计算的效用函数

在前面的章节中,你已经看到了这个项目所需的第一个数学计算。它叫做dist,它计算两点之间的距离:

function dist(x1,y1,x2,y2) {
   var x = x2-x1;
   var y = y2-y1;
   return Math.sqrt(x*x+y*y);
}

下一个要讨论的函数是确定两条线的交点。交点是满足两条直线方程的点。在折纸鱼的例子中,请看图 7-14 。我(在我的程序中)将需要计算从 k 到 n 的直线和从 s 到 q 的直线的交点。在本章中进一步查看图 7-17 。xx点就是交点。该程序的代码是

var xxa = intersect(sx,sy,qx,qy,kx,ky,nx,ny);
var xxx = xxa[0];
var xxy = xxa[1];

线由两个点定义,每个点由两个数字定义。这意味着intersect函数有 2 × 2 × 2 个输入参数。我的功能不一般;只有当线不垂直并且确实有交叉点时,它才起作用。这对于我使用折纸鱼来说是可以接受的,但是如果你把它用于另一个应用,你可能需要做更多的工作。

现在让我们来关注线的数学表示。有不同的方程式,但我用的这个叫做点斜率形式。直线的斜率是任意两点之间 y 的变化除以 x 的变化。按照惯例,斜率被命名为 m。斜率为 m 的直线通过点(x1,y1)的公式为

  • y–y1 = m *(x–x1)

注意这行是数学,不是 JavaScript。现在回到编程,我确定了传递给intersect函数的每条线的斜率和方程。

intersect函数将 m12 设置为从(x1,y1)到(x2,y2)的直线的斜率,将 m34 设置为从(x3,y3)到(x4,y4)的直线的斜率。该代码主要设置两个 y 值:

  • y = M12 *(x–x1)+y1,y = m34 *(x–x3)+y3

下一步是将这两个表达式设置为相等,并求解 x。这样做的目的是计算位于两条线上的 x 的值。有了 x 的值,我用两个等式中的一个来得到相应的 y,对 x,y 表示一个点——实际上是唯一的点——在两条线上。这就是所谓的路口。我编写了返回数组[x,y]的函数代码。以下是完整的代码:

function intersect(x1,y1,x2,y2,x3,y3,x4,y4) {
   // only works on line segments that do intersect and
   // are not vertical
   var m12 = (y2-y1)/(x2-x1);
   var m34 = (y4-y3)/(x4-x3);
   var m = m34/m12;
   var x = (x1-y1/m12-m*x3+y3/m12)/(1-m);
   var y = m12*(x-x1)+y1;
   return ([x,y]);
}

在这一点上,你可能会突然失去信心,因为屏幕的坐标系统是颠倒的,所以你在高中数学课上记得的东西可能都不适用。垂直值随着屏幕向下移动而增加。事实证明,这些方程仍然有效(尽管我们的解释可能不同)。例如,从(0,0)开始到(100,100)的直线的计算斜率为正 1,尽管我们可能认为它是向下倾斜的。在颠倒的世界里,它有正斜率。

折纸鱼需要的另一个计算是我命名为的比例。这个函数有五个输入参数。(x1,y1)和(x2,y2)定义一条线段。第五个参数是 p,表示比例。该函数的任务是计算从(x1,y1)到(x2,y2)的 p 线段上的(x,y)位置。

function proportion(x1,y1,x2,y2,p) {
   var xs = x2-x1;
   var ys = y2-y1;
   var x = x1+ p*xs;
   var y = y1 + p* ys;
   return ([x,y]);
}

这涵盖了我所称的折纸项目的效用函数。这三个计算函数将适用于其他应用。

步进线绘制功能

为序列中的一个步骤生成图表的函数使用 HTML5 的路径绘制工具和变量,这些变量已使用计算实用函数或内置的Math方法进行了设置。在这一节中,我不会一一介绍,但会解释几个。例如,函数triangleM(下面将详细介绍该函数)的任务是为图 7-11 所示的步骤生成图表。

img/272384_2_En_7_Fig11_HTML.jpg

图 7-11

分成三部分的步骤

注意

我的指示并没有建议这样做的方法。文件夹这样做的一个常见方法是从一端(比如左边)的三分之一处猜测点。将正确的点折叠到那个点,然后轻轻捏一下。然后将左端折叠到捏痕处,重复直到你看不到捏痕的变化。这个方法展示了一些很好的数学,即极限。无论你在最初的猜测中犯了什么样的错误,它都会减少到原来的四分之一。如果你坚持这样做,你很快就会得到可以接受的东西。

图 7-12 显示了标注有临界点 e、f、g 和 h 标签的图片。

img/272384_2_En_7_Fig12_HTML.jpg

图 7-12

将一条线分成三份并折叠

定义这四个点的变量是

var e = proportion(ax,ay,cx,cy,.333333);
var ex = e[0];
var ey = e[1];
var f = proportion(ax,ay,cx,cy,.666666);
var fx = f[0];
var fy = f[1];
var g = proportion(ax,ay,dx,dy,.666666);
var gx = g[0];
var gy = g[1];
var h = proportion(cx,cy,dx,dy,.666666);
var hx = h[0];
var hy = h[1];

功能triangleM定义如下:

function triangleM() {
   triangle();
   shortdownarrow(ex,ey);
   shortdownarrow(fx,fy);
   valley(ex,ey,gx,gy,"orange");
   valley(fx,fy,hx,hy,"orange");
}

该函数绘制一个三角形,然后在 e 和 f 上方绘制两个向下的短箭头,然后绘制两条橙色的山谷线。

triangle功能被定义为

function triangle() {
  ctx.fillStyle="teal";
  ctx.beginPath();
  ctx.moveTo(ax,ay);
  ctx.lineTo(cx,cy);
  ctx.lineTo(dx,dy);
  ctx.lineTo(ax,ay);
  ctx.closePath();
  ctx.fill();
  ctx.stroke();
}

triangle函数不是一般的,而是画出这个特定的三角形。一般功能是

function generaltriangle(px,py, qx,qy, rx,ry, scolor, fcolor) {
  ctx.fillStyle=fcolor;
  ctx.strokeStyle = scolor;
  ctx.beginPath();
  ctx.moveTo(px,py);
  ctx.lineTo(qx,qy);
  ctx.lineTo(rx,ry);
  ctx.lineTo(px,py);
  ctx.closePath();
  ctx.fill();
  ctx.stroke();
}

另外,不要假设我知道如何编写这个函数。我可能将这些代码放入第一个函数中,然后当我进入模型的下一步时,意识到我又需要一个三角形了。我提取了我写的代码,并将第一个函数重命名为triangleM(表示“三角形标记”)。我让triangleM函数和thirds函数分别调用名为triangle的函数。

图 7-13 显示了模型中的一个步骤,我将用一个我命名为littleguy的函数来说明,因为在我看来就是这样。

img/272384_2_En_7_Fig13_HTML.jpg

图 7-13

在水槽之后,我称之为小家伙

图 7-14 显示了关键点的标注。

img/272384_2_En_7_Fig14_HTML.jpg

图 7-14

littleguy 临界点的标记

相应变量的定义如下

var kx = ax+diag/3;
var ky = ay;
var lx = kx + diag/3;
var ly = ay;
var mx = ax + diag/6;
var innersq = Math.sqrt(2)*diag/6;
var my = ay + innersq*Math.sin(Math.PI/4);
var nx = ax+diag/3+diag/6;
var ny = my;
var px = mx;
var py = dy;
var rx = nx;
var ry = py;
var qx = kx;
var qy = hy;
var dkq = qy-ky;
var sx = kx + (dkq/Math.cos(Math.PI/8))*Math.sin(Math.PI/8);
var sy = ay;

请注意,我没有试图节省变量。是的,rxnx是同一个值,但是我更容易把它们想成截然不同的东西。

littleguy的代码如下:

function littleguy() {
   ctx.fillStyle="teal";
   ctx.beginPath();
   ctx.moveTo(ax,ay);
   ctx.lineTo(kx,ky);
   ctx.lineTo(mx,my);
   ctx.lineTo(ax,ay);
   ctx.moveTo(kx,ky);
   ctx.lineTo(lx,ly);
   ctx.lineTo(px,py);
   ctx.lineTo(mx,my);
   ctx.lineTo(kx,ky);
   ctx.moveTo(nx,ny);
   ctx.lineTo(rx,ry);
   ctx.lineTo(qx,qy);
   ctx.lineTo(nx,ny);
   ctx.closePath();
   ctx.fill();
   ctx.stroke();
   skinnyline(qx,qy,kx,ky);
   ctx.beginPath();
   ctx.arc(qx,qy,30,-.5*Math.PI,-.25*Math.PI,false);
   ctx.stroke();
   mountain(qx,qy,sx,sy,"orange")
}

用度数表示的圆弧是从-90 度到-45 度。请注意,零度是水平方向,正度是顺时针方向。

图 7-15 、 7-16 、 7-17 和 7-18 显示了该模型剩余关键位置的位置。

img/272384_2_En_7_Fig18_HTML.jpg

图 7-18

做好嘴唇后

img/272384_2_En_7_Fig17_HTML.jpg

图 7-17

在绕回步骤之后

img/272384_2_En_7_Fig16_HTML.jpg

图 7-16

准备下沉中心

img/272384_2_En_7_Fig15_HTML.jpg

图 7-15

半步折叠处贴标

使用图来帮助理解设置变量值的代码。比如我在描述intersect函数的时候提到过,看图 7-14 和 7-17 ,可以看到由 xxx 和 xxy 表示的点 xx 是 s 到 q 和 k 到 n 的直线的交点

还有一个阶跃函数值得解释。结尾前的说明让鱼的头部指向屏幕下方。我想在最后一个视频剪辑之前制作图表,使其水平方向与将要显示的视频剪辑相匹配。这是使用 HTML5 的画布坐标转换完成的。之前的函数命名为lipsrotatefish功能保存当前的原始坐标系。然后,它转换为鱼上的一个点,调用旋转(逆时针旋转 90 度),然后撤销转换。然后,rotatefish函数调用lips函数,绘制鱼,但现在是水平方向。代码如下:

function rotatefish() {
   ctx.save();
   ctx.translate(kx,my);
   ctx.rotate(-Math.PI/2);
   ctx.translate(-kx,-my);
   lips();
   ctx.restore();
}

展示照片

显示照片的步骤与生成线条画的步骤具有相同的结构。对于应用程序所需的每个图像,我需要定义一个Image对象,并将src属性设置为图像文件的名称。以下陈述与图 7-7 中所示的图片相关:

var throat1 = new Image();
throat1.src = "throat1.jpg";

function showthroat1() {
  ctx.drawImage(throat1,40,40);
}

第五章中介绍的创建一个定义媒体的独立文件并自动生成代码(包括 HTML 标记)的技术在这里可能是合适的。我为每张照片和每个视频剪辑编写了函数,正如我在下一节中解释的那样。

展示和移除视频

origamifish.html文件包含两个视频剪辑的视频元素,一个 ID 为sink,另一个 ID 为talk。style 元素有一个所有视频都不显示的指令:

video {display: none;}

函数playsinkplaytalk分别制作视频显示,设置当前时间为零,播放视频,调整画布高度。playsink的定义如下:

function playsink() {
   v = document.getElementById("sink");
   v.style.display="block";
   v.currentTime = 0;
   v.play();
   canvas1.height = 178;
}

在讨论了 origami directions 项目要使用的编程技术和 HTML5 特性之后,我们现在可以从整体上来看这个应用程序了。

构建应用程序并使之成为您自己的应用程序

在本章所学的基础上,最快速的方法是为另一个类似折纸的工艺项目创建方向,有线条画和一些照片和视频剪辑的好处。您可以一步一步地构建它,创建您需要的功能。可能原来有些函数就是我所说的效用函数:其他函数使用的函数。您也可以根据需要构建指示定位的变量。下面是对 origami fish 应用程序的非正式总结/概述:

  • init用于初始化

  • donextgoback用于在步骤中前后移动

  • 用于绘制特定类型线条的实用函数

  • 用于计算的效用函数

  • 阶跃函数(steps数组中引用的函数)

表 7-1 列出了功能和功能组,并指出它们是如何被调用的以及它们调用了什么功能。

表 7-1

折纸方向项目中的 功能

|

功能

|

调用/调用者

|

打电话

|
| --- | --- | --- |
| init | 由<body>标签中的onLoad属性的动作调用 | donext |
| donext | 由按钮标签中的initgobackonClick属性调用 |   |
| goback | 由按钮标签中的onClick属性调用 | donext |
| 绘图实用函数(shortdownarrowvalleymountainskinnylinecurvedarrow) | 由步骤函数调用 |   |
| 用于计算的效用函数(distintersectproportion) | 主要在var语句中调用,以设置代表模型中关键位置的变量 |   |
| 阶跃函数 | 作为donext中的steps数组中的元素调用;一些(finstrianglediamondrttrianglediamondclips)被其他步骤函数调用 | 实用绘图功能,指示的其他步骤功能 |

表 7-2 显示了基本应用程序的代码,每一行都有注释。这些代码中的大部分你已经在前面的章节中看到过了。

表 7-2

折纸方向项目的完整代码

|

代码行

|

描述

|
| --- | --- |
| <!DOCTYPE html> | 页眉 |
| <html> | html标签 |
| <head> | head标签 |
| <title>Origami fish</title> | 完整标题 |
| <style> | style标签 |
| button {font-size:large; font-family:Georgia, "Times New Roman", Times, serif;} | 格式化按钮的指令;注意,颜色是为body元素中的每个按钮指定的 |
| #directions {font-family:"Comic Sans MS", cursive;} | 用于格式化所有方向的指令 |
| video {display:none;} | 关闭所有视频元素的显示,直到被调用 |
| </style> | 结束style标签 |
| <script> | 开始script标签 |
| var ctx; | 将为所有绘图保存画布上下文 |
| var cwidth; | 画布宽度 |
| var cheight; | 画布高度 |
| var ta; | 将保存每个步骤的文本部分的元素 |
| var kamiw = 4; | 设置纸张宽度 |
| var kamih = 4; | 设置纸张高度 |
| var i2p = 72; | 将英寸设置为像素 |
| var dashlen = 8; | 设置山谷褶皱中的虚线长度 |
| var dgap = 2.0; | 设置破折号之间的间隙 |
| var ddashlen = 6.0; | 在山地褶皱中设置虚线长度 |
| var ddot = 2.0; | 设置山峰褶皱中的点长度 |
| var dratio = dashlen/(dashlen+dgap); | 用于山区线路 |
| var ddtotal = ddashlen+3*ddot; | 用于山区线路 |
| var ddratio1 = ddashlen/ddtotal; | 用于山区线路 |
| var ddratio2 = (ddashlen+ddot)/ddtotal; | 用于山区线路 |
| var ddratio3 = (ddashlen+2*ddot)/ddtotal; | 用于山地线;用于计算虚线和点的数量以及虚线和点的起点和范围的所有值 |
| var kamix = 10; | 第一步中纸张的 x 位置 |
| var kamiy = 10; | 第一步中纸张的 y 位置 |
| var nextstep; | 指向steps数组的指针 |
| function dist(x1,y1,x2,y2) { | dist功能的标题 |
| var x = x2-x1; | 在x中设置差值 |
| var y = y2-y1; | 在y中设置差值 |
| return Math.sqrt(x*x+y*y); | 返回平方和的平方根 |
| } | 关闭dist功能 |
| function intersect(x1,y1,x2,y2,x3,y3,x4,y4) { | 两行之间的intersect功能标题,用 2 × 2 点表示 |
| // only works on line segments that do intersection and | 代码中应该保留的好注释:假设有交集。。。 |
| // are not vertical | 。。。并假设线条不是垂直的;如果是,代码将被零除,这将产生一个错误 |
| var m12 = (y2-y1)/(x2-x1); | 计算斜率 |
| var m34 = (y4-y3)/(x4-x3); | 计算斜率 |
| var m = m34/m12; | 用于计算 |
| var x = (x1-y1/m12-m*x3+y3/m12)/(1-m); | 求解x |
| var y = m12*(x-x1)+y1; | 求解y |
| return ([x,y]); | 返回对 |
| } | 关闭intersect功能 |
| function init() { | init功能的标题 |
| canvas1 = document.getElementById("canvas"); | 设置canvas1 |
| ctx = canvas1.getContext("2d"); | 设置上下文 |
| cwidth = canvas1.width; | 设置cwidth |
| cheight = canvas1.height; | 设置cheight |
| ta = document.getElementById("directions"); | 设置ta为文本方向按住柠檬 |
| nextstep = 0; | 初始化nextstep |
| ctx.fillStyle = "white"; | 设置填充样式;将用于擦除 |
| ctx.lineWidth = origwidth; | 设置线宽(之前设置) |
| origstyle = ctx.strokeStyle; | 保存笔画颜色 |
| ctx.font = "15px Georgia, Times, serif"; | 设置字体 |
| donext(); | 从第 0 步开始 |
| } | 关闭init功能 |
| function directions() { | 标题为方向,第一个“步骤”显示 |
| ctx.fillStyle = "black"; | 更改填充样式,用于文本 |
| ctx.font = "15px Georgia, Times, serif"; | 设置字体 |
| ctx.fillText("Make valley fold", 10,20); | 输出说明 |
| valley(200,18,300,18,"orange"); | 制作橙色山谷线样本 |
| ctx.fillText("Make mountain fold",10,50); | 输出说明 |
| mountain(200,48,300,48,"orange"); | 制作样品橙山线 |
| ctx.fillText("unfolded fold line",10,100); | 输出说明 |
| skinnyline(200,98,300,98); | 为展开的折叠线制作样本细线 |
| ctx.fillText("When sense of fold matters:",10,150); | 输出说明 |
| ctx.fillText("unfolded valley fold", 10,180); | 继续 |
| valley(200,178,300,178); | 做样老谷 |
| ctx.fillText("unfolded mountain fold",10,210); | 输出说明 |
| mountain(200,208,300,208); | 弄样旧山 |
| ctx.fillStyle = "white"; | 改回填充样式 |
| } | 关闭方向功能 |
| function donext() { | donext功能的标题 |
| if (nextstep>=steps.length) { | 检查nextstep是否过大 |
| nextstep=steps.length-1; | 重置 |
| } | 关闭条款 |
| if (v) { | 检查是否设置了v |
| v.pause(); | 暂停视频 |
| v.style.display = "none"; | 使其不显示 |
| v = undefined; | 将v设置为未定义 |
| canvas1.height = 480; | 恢复高度 |
| } | 关闭条款 |
| ctx.clearRect(0,0,cwidth,cheight); | 透明画布 |
| ctx.lineWidth = origwidth; | 重置线条宽度 |
| steps[nextstep][0](); | 调用适当的阶跃函数 |
| ta.innerHTML = steps[nextstep][1]; | 显示附带的文本 |
| nextstep++; | 增量nextstep |
| } | 关闭donext功能 |
| function goback() { | goback的标题 |
| nextstep = nextstep -2; | 将nextstep减 2(因为它已经领先一位) |
| if (nextstep<0) { | 检查nextstep现在是否太低 |
| nextstep = 0; | 重置 |
| } | 关闭条款 |
| donext(); | 调用donext |
| } | 关闭goback功能 |
| function shortdownarrow(x,y) { | 短向下箭头功能的标题 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(x,y-20) | 移动到(x,y)位置的正上方 |
| ctx.lineTo(x,y-7); | 在(x,y)的正上方画线 |
| ctx.moveTo(x-5,y-12); | 向左上方移动 |
| ctx.lineTo(x,y-7); | 画对角线 |
| ctx.moveTo(x+5,y-12); | 向右上方移动 |
| ctx.lineTo(x,y-7); | 画对角线 |
| ctx.closePath(); | 关闭路径 |
| ctx.stroke(); | 画出完整的路径:一个短箭头 |
| } | 关闭shortdownarrow功能 |
| function proportion(x1,y1,x2,y2,p) { | proportion功能的标题 |
| var xs = x2-x1; | 在x中设置差值 |
| var ys = y2-y1; | 在y中设置差值 |
| var x = x1+ p*xs; | 计算新的x |
| var y = y1 + p* ys; | 计算新的y |
| return ([x,y]); | 返回对 |
| } | 关闭proportion功能 |
| function skinnyline(x1,y1,x2,y2) { | skinnyline功能的标题 |
| ctx.lineWidth = 1; | 设置线条宽度 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(x1,y1); | 移动到开始 |
| ctx.lineTo(x2,y2); | 要结束的行 |
| ctx.closePath(); | 关闭路径 |
| ctx.stroke(); | 画出笔划 |
| ctx.lineWidth = origwidth; | 重置线条宽度 |
| } | 关闭skinnyline |
| var origstyle; | 将保持原始颜色 |
| var origwidth = 2; | 为大多数线条设置线条宽度 |
| function valley(x1,y1,x2,y2,color) { | valley功能的标题 |
| var px=x2-x1; | 在x中设置差值 |
| var py = y2-y1; | 在y中设置差值 |
| var len = dist(x1,y1,x2,y2); | 确定长度 |
| var nd = Math.floor(len/(dashlen+dgap)); | 多少破折号和缺口 |
| var xs = px/nd; | 称之为 x 因素 |
| var ys = py/nd; | 称之为 y 因素 |
| if (color) ctx.strokeStyle = color; | 如果给定了color参数,将描边颜色设置为该值 |
| ctx.beginPath(); | 开始路径 |
| for (var n=0;n<nd;n++) { | 虚线数量的循环 |
| ctx.moveTo(x1+n*xs,y1+n*ys); | 移动到下一个位置 |
| ctx.lineTo(x1+n*xs+dratio*xs,y1+n*ys+dratio*ys); | 绘制虚线 |
| } | 关闭for回路 |
| ctx.closePath(); | 关闭路径 |
| ctx.stroke(); | 画出路径 |
| ctx.strokeStyle = origstyle; | 重置笔画样式 |
| } | 关闭valley功能 |
| function mountain(x1,y1,x2,y2,color) { | 山地功能的标题 |
| var px=x2-x1; | 设置 x 方向的差异 |
| var py = y2-y1; | 设置 y 方向的差异 |
| var len = dist(x1,y1,x2,y2); | 确定长度 |
| var nd = Math.floor(len/ddtotal); | 确定虚线和圆点组合的数量 |
| var xs = px/nd; | 设置 x 因子 |
| var ys = py/nd; | 设置 y 因子 |
| if (color) ctx.strokeStyle = color; | 如果给定了color参数,将描边颜色设置为该值 |
| ctx.beginPath(); | 开始路径 |
| for (var n=0;n<nd;n++) { | 组合数循环 |
| ctx.moveTo(x1+n*xs,y1+n*ys); | 移到下一个 |
| ctx.lineTo(x1+n*xs+ddratio1*xs,y1+n*ys+ddratio1*ys); | 画破折号 |
| ctx.moveTo(x1+n*xs+ddratio2*xs,y1+n*ys+ddratio2*ys); | 移动到点的开头 |
| ctx.lineTo(x1+n*xs+ddratio3*xs,y1+n*ys+ ddratio3*ys); | 画点 |
| } | 闭环 |
| ctx.closePath(); | 关闭路径 |
| ctx.stroke(); | 画出路径 |
| ctx.strokeStyle = origstyle; | 重置笔画样式 |
| } | 关闭mountain功能 |
| function curvedarrow(x1,y1,x2,y2,px,py){ | 从(x1,y1)到(x2,y2)curvedarrow的标题偏移(px,py) |
| var arrowanglestart; | 开始角度 |
| var arrowanglefinish; | 完成角度 |
| var d = dist(x1,y1,x2,y2); | 距离 |
| var rad=Math.sqrt(4.25*d*d); | 值 4.25 是通过实验得出的,以获得箭头的吸引曲线 |
| var ctrx; | 弯曲箭头的圆弧中心的 x 坐标 |
| var ctry; | y 坐标 |
| var ex; | 组成箭头的两条线 |
| var ey; | 组成箭头的两条线 |
| var angdel = Math.atan2(d/2,2*d); | 弧的角度 |
| var fromhorizontal; | 弧开始的角度 |
| ctx.strokeStyle = "red"; | 设置颜色 |
| ctx.beginPath(); | 开始路径 |
| if (y1==y2) { | 水平箭头案例 |
| arrowanglestart = 1.5*Math.PI-angdel; | 设置起始角度 |
| arrowanglefinish = 1.5*Math.PI+angdel; | 设置结束角度 |
| ctrx = .5*(x1+x2) +px; | 计算中心x |
| ctry = y1+2*d +py; | 计算中心y |
| if (x1<x2) { | 对于从左向右的箭头 |
| ctx.arc(ctrx,ctry, rad,arrowanglestart,arrowanglefinish, false); | 画弧线 |
| fromhorizontal=2*Math.PI- arrowanglefinish; | 用于计算 |
| ex = ctrx+rad*Math.cos(fromhorizontal); | 设置 x 增量 |
| ey = ctry - rad*Math.sin(fromhorizontal); | 设置 y 增量 |
| ctx.lineTo(ex-8,ey+8); | 画第一条小线 |
| ctx.moveTo(ex,ey); | 移动到另一端 |
| ctx.lineTo(ex-8,ey-8); | 画直线 |
| } | 从左到右关闭箭头 |
| else { | 从右到左 |
| ctx.arc(ctrx,ctry, rad,arrowanglefinish,arrowanglestart,``true); | 画弧线 |
| fromhorizontal=2*Math.PI- arrowanglestart; | 计算线条 |
| ex = ctrx+rad*Math.cos(fromhorizontal); | 为小线条设置 x |
| ey = ctry - rad*Math.sin(fromhorizontal); | 为小线条设置 y |
| ctx.lineTo(ex+8,ey+8); | 绘制第一条线 |
| ctx.moveTo(ex,ey); | 移动到另一行的末尾 |
| ctx.lineTo(ex+8,ey-8); | 画直线 |
| } | 结束子句 |
| ctx.stroke(); | 为两种情况画一张图 |
| } | 结束水平情况 |
| else if (x1==x2) { | 垂直线 |
| arrowanglestart = -angdel; | 设置起始角度 |
| arrowanglefinish = angdel; | 设置完成角度 |
| ctrx = x1-2*d+px; | 计算中心 x |
| ctry = .5*(y1+y2) + py; | 计算 y 中心 |
| if (y1<y2) { | 如果向下箭头 |
| ctx.arc(ctrx,ctry,rad,arrowanglestart,``arrowanglefinish,false); | 画弧线 |
| fromhorizontal=- arrowanglefinish; | 用于计算 |
| ex = ctrx+rad*Math.cos(fromhorizontal); | 计算小线条的 x |
| ey = ctry - rad*Math.sin(fromhorizontal); | 计算小线条的 y 值 |
| ctx.lineTo(ex-8,ey-8); | 画第一条小线 |
| ctx.moveTo(ex,ey); | 移动到结尾 |
| ctx.lineTo(ex+8,ey-8); | 画第二条小线 |
| } | 结束向下子句 |
| else { | 向上子句 |
| ctx.arc(ctrx,ctry, rad,arrowanglefinish,arrowanglestart,``true); | 画弧线 |
| fromhorizontal=- arrowanglestart; | 用于计算 |
| ex = ctrx+rad*Math.cos(fromhorizontal); | 计算小线条的 x |
| ey = ctry - rad*Math.sin(fromhorizontal); | 计算小线条的 y 值 |
| ctx.lineTo(ex-8,ey+8); | 画第一条小线 |
| ctx.moveTo(ex,ey); | 移动到第二行末尾 |
| ctx.lineTo(ex+8,ey+8); | 画一条线 |
| } | 结束子句 |
| ctx.stroke(); | 画弧线 |
| } | 关闭垂直机箱 |
| ctx.strokeStyle = "black"; | 重置颜色 |
| } |   |
| // specific to fish | 接下来的内容是特定于鱼模型的 |
| var steps= [ | 说明步骤:函数名和附带文本 |
| [directions,"Diagram conventions"], |   |
| [showkami,"Make quarter turn."], |   |
| [diamond1,"Fold top point to bottom point."], |   |
| [triangleM,"Divide line into thirds and make valley folds and unfold "], |   |
| [thirds,"Fold in half to the left."], |   |
| [rttriangle,"Fold down the right corner to the fold marking a third. "], |   |
| [cornerdown,"Unfold everything."], |   |
| [unfolded,"Prepare to sink middle square by reversing folds as indicated ..."], |   |
| [changedfolds,"note middle square sides all valley folds, some other folds changed. Flip over."], |   |
| [precollapse,"Push sides to sink middle square."], |   |
| [playsink,"Sink square, collapse model."], |   |
| [littleguy,"Now fold back the right flap to center valley fold. You are bisecting the indicated angle."], |   |
| [oneflapup,"Do the same thing to the flap on the left"], |   |
| [bothflapsup,"Make fins by wrapping top of right flap around 1 layer and left around back layer"], |   |
| [finsp,"Now make lips...make preparation folds"], |   |
| [preparelips,"and turn lips inside out. Turn corners in..."], |   |
| [showcleftlip,"...making cleft lips."], |   |
| [lips,"Pick up fish and look down throat..."], |   |
| [showthroat1,"Stick your finger in its mouth and move the inner folded material to one side"], |   |
| [showthroat2,"Throat fixed."], |   |
| [rotatefish,"Squeeze & release top and bottom to make fish's mouth close and open"], |   |
| [playtalk,"Talking fish."] |   |
| ]; |   |
| var diag = kamiw* Math.sqrt(2.0)*i2p; | 对角线长度 |
| var ax = 10; | 为左角设置 x |
| var ay = 220; | 为左角设置 y |
| var bx = ax+ .5*diag; | 计算 b(顶角) |
| var by = ay - .5*diag; |   |
| var cx = ax + diag; | 计算 c(右) |
| var cy = ay; |   |
| var dx = bx; | 计算 d(底部) |
| var dy = ay + .5*diag; |   |
| var e = proportion(ax,ay,cx,cy,.333333); | e 至 h 见图 7-12 |
| var ex = e[0]; |   |
| var ey = e[1]; |   |
| var f = proportion(ax,ay,cx,cy,.666666); |   |
| var fx = f[0]; |   |
| var fy = f[1]; |   |
| var g = proportion(ax,ay,dx,dy,.666666); |   |
| var gx = g[0]; |   |
| var gy = g[1]; |   |
| var h = proportion(cx,cy,dx,dy,.666666); |   |
| var hx = h[0]; |   |
| var hy = h[1]; |   |
| var jx = ax + .5*diag; | 参见图 7-15 和 7-16 |
| var jy = ay; |   |
| var diag6 = diag/6; |   |
| var gry = ay-(gy-ay); |   |
| var kx = ax+diag/3; | k 至 s 见图 7-14 |
| var ky = ay; |   |
| var lx = kx + diag/3; |   |
| var ly = ay; |   |
| var mx = ax + diag/6; |   |
| var innersq = Math.sqrt(2)*diag/6; |   |
| var my = ay + innersq*Math.sin(Math.PI/4); |   |
| var nx = ax+diag/3+diag/6; |   |
| var ny = my; |   |
| var px = mx; |   |
| var py = dy; |   |
| var rx = nx; |   |
| var ry = py; |   |
| var qx = kx; |   |
| var qy = hy; |   |
| var dkq = qy-ky; |   |
| var sx = kx + (dkq/Math.cos(Math.PI/8))*Math.sin(Math.PI/8); |   |
| var sy = ay; |   |
| var tx = kx; | 参见图 7-17 |
| var ty = qy-dist(qx,qy,lx,ly); |   |
| var xxa = intersect(sx,sy,qx,qy,kx,ky,nx,ny); |   |
| var xxx = xxa[0]; |   |
| var xxy = xxa[1]; |   |
| var xxlx = kx-(xxx-kx); |   |
| var xxly = xxy; |   |
| var slx = kx- (sx-kx); |   |
| var sly = sy; |   |
| var tlx = tx-5; |   |
| var tly = ty; |   |
| var dkt=ky-ty; |   |
| var finlx = kx-dkt; | 参见图 7-18 |
| var finly = ky; |   |
| var finrx = kx+dkt; |   |
| var finry = ky; |   |
| var w = Math.cos(Math.PI/4)*dkt; |   |
| var wx = kx-.5*dkt; |   |
| var wy = w*Math.sin(Math.PI/4)+ky; |   |
| var zx = kx+.5*dkt; |   |
| var zy = wy; |   |
| var plipx = px; |   |
| var plipy = py-10; |   |
| var rlipx = rx; |   |
| var rlipy = ry-10; |   |
| var plipex = px - 10; |   |
| var plipey = plipy; |   |
| var rlipex = rx + 10; |   |
| var rlipey = rlipy; |   |
| var rclipcleft1 = proportion(rlipex,rlipey,rlipx,rlipy,.5); |   |
| var pclipcleft1 = proportion(plipex,plipey,plipx,plipy,.5); |   |
| var rclipcleft2 = proportion(rlipex,rlipey,qx,qy,.1); |   |
| var pclipcleft2 = proportion(plipex,plipey,qx,qy,.1); |   |
| var rcx1 = rclipcleft1[0]; |   |
| var rcy1 = rclipcleft1[1]; |   |
| var rcx2 = rclipcleft2[0]; |   |
| var rcy2 = rclipcleft2[1]; |   |
| var pcx1 = pclipcleft1[0]; |   |
| var pcy1 = pclipcleft1[1]; |   |
| var pcx2 = pclipcleft2[0]; |   |
| var pcy2 = pclipcleft2[1]; |   |
| var v; | 将保存视频元素 |
| var throat1 = new Image(); | 定义Image对象 |
| throat1.src = "throat1.jpg"; | 设置src |
| var throat2 = new Image(); | 定义Image对象 |
| throat2.src = "throat2.jpg" | 设置src |
| var cleft = new Image(); | 定义Image对象 |
| cleft.src="cleftlip.jpg"; | 设置src |
| function showcleftlip() { | showcleftlip的标题 |
| ctx.drawImage(cleft,40,40); | 绘制图像 |
| } | 关闭showcleftlip |
| function showthroat1() { | showthroat1的标题 |
| ctx.drawImage(throat1,40,40); | 绘制图像 |
| } | 关闭showthroat1 |
| function showthroat2() { | showthroat2的标题 |
| ctx.drawImage(throat2,40,40); | 绘制图像 |
| } | 关闭showthroat2 |
| function playtalk() { | playtalk的标题 |
| v = document.getElementById("talk"); | 设置为谈话视频 |
| v.style.display="block"; | 使可见 |
| v.currentTime = 0; | 开始时设置 |
| v.play(); | 玩 |
| canvas1.height = 126; | 调整视频的高度 |
| } | 关闭playtalk |
| function playsink() { | playsink的标题 |
| v = document.getElementById("sink"); | 设置为水槽视频 |
| v.style.display="block"; | 使可见 |
| v.currentTime = 0; | 开始时设置 |
| v.play(); | 玩 |
| canvas1.height = 178; | 调整视频的高度 |
| } | 关闭playsink |
| function lips() { | lips的标题 |
| ctx.fillStyle = "teal"; | 设置颜色 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(finlx,finly); | 移动到左手手指的左上角 |
| ctx.lineTo(kx,ky); | 居中绘制 |
| ctx.lineTo(wx,wy); | 向后向下画 |
| ctx.lineTo(finlx,finly); | 拉起开始(左角,左鳍) |
| ctx.moveTo(finrx,finry); | 移至右侧鳍 |
| ctx.lineTo(kx,ky); | 居中绘制 |
| ctx.lineTo(zx,zy); | 向下向右画 |
| ctx.lineTo(finrx,finry); | 画到右角,右鳍 |
| ctx.moveTo(mx,my); | 移动到 m |
| ctx.lineTo(kx,ky); | 画到 k |
| ctx.lineTo(xxx,xxy); | 画到 xx |
| ctx.lineTo(qx,qy); | 向下画,中心到 q |
| ctx.lineTo(plipx,plipy); | 向下画,对吗 |
| ctx.lineTo(mx,my); | 一直画到 m |
| ctx.moveTo(xxx,xxy); | 移动到 xx |
| ctx.lineTo(nx,ny); | 向右下画 |
| ctx.lineTo(rlipx,rlipy); | 提取到 rlip |
| ctx.lineTo(qx,qy); | 绘制到中心 q |
| ctx.lineTo(xxx,xxy); | 抽回 xx |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 填充形状 |
| ctx.stroke(); | 轮廓形状 |
| ctx.fillStyle="white"; | 设置为白色 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(qx,qy); | 从较低的中心开始 |
| ctx.lineTo(pcx2,pcy2); | 画到嘴唇的左上方 |
| ctx.lineTo(pcx1,pcy1); | 绘制到左外唇 |
| ctx.lineTo(plipx,plipy); | 向右轻轻画到底角 plip |
| ctx.lineTo(qx,qy); | 画回中心 |
| ctx.lineTo(rcx2,rcy2); | 画到嘴唇的右上方 |
| ctx.lineTo(rcx1,rcy1); | 向右外唇绘制 |
| ctx.lineTo(rlipx,rlipy); | 画到底角 rlip |
| ctx.lineTo(qx,qy); | 画回中心 |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 填充白色形状(两部分) |
| ctx.stroke(); | 轮廓形状 |
| skinnyline(kx,ky,qx,qy); | 画垂直中心线 |
| ctx.fillStyle="teal"; | 重置为彩色 |
| } | 紧闭双唇 |
| function rotatefish() { | rotatefish的标题 |
| ctx.save(); | 保存当前坐标系 |
| ctx.translate(kx,my); | 移动到中心点 |
| ctx.rotate(-Math.PI/2); | 旋转 90 度 |
| ctx.translate(-kx,-my); | 撤消翻译 |
| lips(); | 画嘴唇(到目前为止的模型) |
| ctx.restore(); | 恢复旧坐标系 |
| } | 关闭rotatefish |
| function preparelips() { | preparelips的标题 |
| ctx.fillStyle="teal"; | 设置颜色 |
| fins(); | 最多绘制 |
| valley(qx,qy,rlipx,rlipy); | 标记山谷线 |
| valley(qx,qy,plipx,plipy); | 标记山谷线 |
| } | 关闭preparelips |
| function finsp() { | finsp的标题 |
| ctx.fillStyle="teal"; | 设置颜色 |
| fins(); | 最多绘制 |
| valley(qx,qy,rlipx,rlipy,"orange"); | 绘制山谷褶皱 |
| valley(qx,qy,plipx,plipy,"orange"); | 绘制山谷褶皱 |
| } | 关闭finsp |
| function fins() { | 翅片的集管 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(finlx,finly); | 移动到左鳍 |
| ctx.lineTo(kx,ky); | 向中心画线 |
| ctx.lineTo(wx,wy); | 向左下方画线 |
| ctx.lineTo(finlx,finly); | 向左鳍画 |
| ctx.moveTo(finrx,finry); | 移至右侧鳍 |
| ctx.lineTo(kx,ky); | 居中绘制 |
| ctx.lineTo(zx,zy); | 向右下画 |
| ctx.lineTo(finrx,finry); | 收回右手指 |
| ctx.moveTo(mx,my); | 移动到 m(向左和向下) |
| ctx.lineTo(kx,ky); | 居中绘制 |
| ctx.lineTo(xxx,xxy); | 画到 xx |
| ctx.lineTo(qx,qy); | 画到 q |
| ctx.lineTo(px,py); | 向 p 靠拢 |
| ctx.lineTo(mx,my); | 向左画到 m |
| ctx.moveTo(xxx,xxy); | 移动到 xx |
| ctx.lineTo(nx,ny); | 向右画到 n |
| ctx.lineTo(rx,ry); | 下降到 r |
| ctx.lineTo(qx,qy); | 向上画并向左居中 |
| ctx.lineTo(xxx,xxy); | 画到 xx |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 填充形状 |
| ctx.stroke(); | 画提纲 |
| skinnyline(kx,ky,qx,qy); | 画细线条表示中心折叠 |
| } | 关闭鳍 |
| function bothflapsup () { | bothflapsup的标题 |
| ctx.fillStyle="teal"; | 设置颜色 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(slx,sly); | 移动到角落 |
| ctx.lineTo(tlx,tly); | 将线拉到顶端 |
| ctx.lineTo(kx,ky); | 向中心画线 |
| ctx.lineTo(xxlx,xxly); | 向左下方画线 |
| ctx.lineTo(slx,sly); | 拉回到尖端 |
| ctx.moveTo(mx,my); | 向下移动(在左边) |
| ctx.lineTo(kx,ky); | 向中心画线 |
| ctx.lineTo(sx,sy); | 向右侧绘制 |
| ctx.lineTo(qx,qy); | 向左下划 |
| ctx.lineTo(px,py); | 绘制到底部,左顶端 |
| ctx.lineTo(mx,my); | 起草 |
| ctx.moveTo(tx,ty); | 起草 |
| ctx.lineTo(sx,sy); | 向右绘制 |
| ctx.lineTo(kx,ky); | 居中绘制 |
| ctx.lineTo(tx,ty); | 起草 |
| ctx.moveTo(xxx,xxy); | 向右绘制 |
| ctx.lineTo(nx,ny); | 向右绘制 |
| ctx.lineTo(rx,ry); | 向下拉到尖端 |
| ctx.lineTo(qx,qy); | 居中绘制 |
| ctx.lineTo(xxx,xxy); | 向右后退 |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 填充形状 |
| ctx.stroke(); | 轮廓形状 |
| skinnyline(kx,ky,qx,qy); | 添加指示折叠的线条 |
| } | 关闭bothflapsup |
| function oneflapup() { | oneflapup的标题 |
| ctx.fillStyle="teal"; | 设置颜色 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(ax,ay); | 移动到左角 |
| ctx.lineTo(kx,ky); | 画到中间 |
| ctx.lineTo(mx,my); | 向下并向左画 |
| ctx.lineTo(ax,ay); | 退回到左角 |
| ctx.moveTo(kx,ky); | 移到中间 |
| ctx.lineTo(sx,sy); | 向右绘制 |
| ctx.lineTo(qx,qy); | 向下画,中间 |
| ctx.lineTo(px,py); | 向左、向下绘制 |
| ctx.lineTo(mx,my); | 起草 |
| ctx.lineTo(kx,ky); | 绘制(回到)中间顶部 |
| ctx.moveTo(xxx,xxy); | 向右下画 |
| ctx.lineTo(nx,ny); | 招来 |
| ctx.lineTo(rx,ry); | 向右下方画 |
| ctx.lineTo(qx,qy); | 居中绘制 |
| ctx.lineTo(xxx,xxy); | 向右,向上画 |
| ctx.moveTo(kx,ky); | 移到中间 |
| ctx.lineTo(tx,ty); | 绘制到顶部 |
| ctx.lineTo(sx,sy); | 向下画,对吗 |
| ctx.lineTo(kx,ky); | 拉到(回到)顶端 |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 填充形状 |
| ctx.stroke(); | 轮廓形状 |
| skinnyline(qx,qy,kx,ky); | 绘制折叠线 |
| } | 关闭oneflapup |
| function littleguy() { | littleguy的标题 |
| ctx.fillStyle="teal"; | 设置颜色 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(ax,ay); | 移动到左角 |
| ctx.lineTo(kx,ky); | 居中绘制 |
| ctx.lineTo(mx,my); | 向左、向下绘制 |
| ctx.lineTo(ax,ay); | 退到角落 |
| ctx.moveTo(kx,ky); | 移到中间 |
| ctx.lineTo(lx,ly); | 画到右角 |
| ctx.lineTo(px,py); | 向下并向左画 |
| ctx.lineTo(mx,my); | 起草 |
| ctx.lineTo(kx,ky); | 画回中心 |
| ctx.moveTo(nx,ny); | 向右下移动 |
| ctx.lineTo(rx,ry); | 招来 |
| ctx.lineTo(qx,qy); | 向下方中央绘制 |
| ctx.lineTo(nx,ny); | 后退,对吗 |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 填充形状 |
| ctx.stroke(); | 轮廓形状 |
| skinnyline(qx,qy,kx,ky); | 绘制折叠线 |
| ctx.beginPath(); | 开始路径 |
| ctx.arc(qx,qy,30,-.5*Math.PI,``-.25*Math.PI,false); | 画弧线来代表角度 |
| ctx.stroke(); | 绘制为笔画 |
| mountain(qx,qy,sx,sy,"orange") | 指示山脉褶皱 |
| } | 关闭littleguy |
| function unfolded() { | unfolded的标题 |
| diamond(); | 画菱形 |
| valley(ax,ay,cx,cy); | 在纸上标出山谷 |
| valley(ex,ey,gx,gy); | 指示山谷、中间和左侧下方 |
| valley(fx,fy,hx,hy); | 指示山谷、中间和右侧下方 |
| mountain(ex,ey,gx,gry); | 指示左边的山、中间和上方 |
| mountain(fx,fy,hx,gry); | 指示山,中间,向上,对吗 |
| valley(jx,jy,dx,dy); | 从内部菱形到底部的山谷 |
| mountain(jx,jy,bx,by); | 从内部菱形到顶部的山 |
| valley(ex,ey,jx,jy+diag6); | 山谷左侧,内部菱形的上侧 |
| valley(jx,jy-diag6,fx,fy); | 山谷右侧,内部菱形的下侧 |
| mountain(ex,ey,jx,jy-diag6); | 山,左,内菱形的下侧 |
| mountain(jx,jy+diag6,fx,fy); | 内部菱形顶部右侧的山 |
| } | 关闭unfolded |
| function precollapse() { | precollapse的标题 |
| diamondc(); | 彩色钻石 |
| mountain(ax,ay,cx,cy); | 纸对面的山 |
| valley(ex,ey,gx,gy); | 山谷中心,在左边 |
| valley(fx,fy,hx,hy); | 山谷中心,在右边 |
| valley(ex,ey,gx,gry); | 山谷中心,在左边 |
| valley(fx,fy,hx,gry); | 山谷中心,在右边 |
| valley(jx,jy-diag6,jx,jy+diag6); | 纸张中间的山谷,垂直 |
| mountain(jx,jy-diag6,bx,by); | 从内部菱形向上的山 |
| mountain(jx,jy+diag6,dx,dy); | 从内部菱形向下的山 |
| mountain(ex,ey,jx,jy+diag6); | 山,底部,内部菱形左侧 |
| mountain(jx,jy-diag6,fx,fy); | 内部菱形右侧顶部的山 |
| mountain(ex,ey,jx,jy-diag6); | 内部菱形左侧顶部的山 |
| mountain(jx,jy+diag6,fx,fy); | 山,底部,内部菱形的右侧 |
| } | 关闭precollapse |
| function changedfolds() { | changedfolds的表头;请注意,这与展开相同,除了一些褶皱的感觉(山与谷) |
| diamond(); | 画菱形 |
| valley(ax,ay,cx,cy); | 纸谷 |
| mountain(ex,ey,gx,gy); | 山,在纸的中间,在左边 |
| mountain(fx,fy,hx,hy); | 山,中间,右下方 |
| mountain(ex,ey,gx,gry); | 山,中间,在左边 |
| mountain(fx,fy,hx,gry); | 山,中间,在右边 |
| mountain(jx,jy-diag6,jx,jy+diag6); | 山,中间,垂直 |
| valley(jx,jy-diag6,bx,by); | 山谷,内部菱形向上 |
| valley(jx,jy+diag6,dx,dy); | 山谷,内部菱形向下 |
| valley(ex,ey,jx,jy+diag6); | 谷,底部,内部菱形的左侧 |
| valley(jx,jy-diag6,fx,fy); | 山谷,顶部,内部菱形的右侧 |
| valley(ex,ey,jx,jy-diag6); | 山谷,顶部,内部菱形左侧 |
| valley(jx,jy+diag6,fx,fy); | 谷,底部,内部菱形的右侧 |
| } | 关闭changefolds |
| function triangleM() { | triangleM的标题 |
| triangle(); | 画三角形 |
| shortdownarrow(ex,ey); | 用箭头表示,三分之一点 |
| shortdownarrow(fx,fy); | 用箭头表示,三分之二点 |
| valley(ex,ey,gx,gy,"orange"); | 下一个山谷褶皱 |
| valley(fx,fy,hx,hy,"orange"); | 下一个山谷褶皱 |
| } | 关闭triangleM |
| function thirds() { | thirds的标题 |
| triangle(); | 画三角形 |
| skinnyline(ex,ey,gx,gy); | 指示折叠线 |
| skinnyline(fx,fy,hx,hy); | 指示折叠线 |
| curvedarrow(cx,cy,ax,ay,0,-20); | 从右向左绘制曲线,垂直偏移 |
| valley(jx,jy,dx,dy,"orange"); | 绘制(下一条)山谷线 |
| } | 关闭thirds |
| function cornerdown() { | cornerdown的标题 |
| rttriangle(); | 画直角三角形 |
| ctx.clearRect(ex,ey, diag6+5,diag6); | 擦除覆盖角的矩形 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(ex,ey); | 移动到开始 |
| ctx.lineTo(ex+diag6,ey+diag6); | 向右下画 |
| ctx.lineTo(ex,ey+diag6); | 一直往下画 |
| ctx.lineTo(ex,ey); | 退回起点 |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 填充三角形 |
| ctx.stroke(); | 轮廓三角形 |
| } | 关闭cornerdown |
| function showkami() { | showkami的标题 |
| ctx.strokeRect(kamix,kamiy,kamiw*i2p,kamih*i2p); | 画一个长方形 |
| } | 关闭showkami |
| function diamond1() { | diamond1的标题 |
| diamond(); | 画菱形 |
| valley(ax,ay,cx,cy,"orange"); | 添加橙谷 |
| curvedarrow(bx,by,dx,dy,10,0); | 添加垂直弯曲箭头 |
| } | 关闭diamond1 |
| function diamondc() { | diamondc的标题 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(ax,ay); | 移动到左角 |
| ctx.lineTo(bx,by); | 向右排成一行 |
| ctx.lineTo(cx,cy); | 向右下方排队 |
| ctx.lineTo(dx,dy); | 向下排到中间 |
| ctx.lineTo(ax,ay) | 开始行 |
| ctx.closePath(); | 关闭路径 |
| ctx.fillStyle="teal"; | 设置颜色 |
| ctx.fill(); | 填入菱形 |
| ctx.stroke(); | 画提纲 |
| } | 关闭diamondc |
| function diamond() { | diamond的标题 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(ax,ay); | 移动到左角 |
| ctx.lineTo(bx,by); | 向上画一条线 |
| ctx.lineTo(cx,cy); | 向下并向上画线 |
| ctx.lineTo(dx,dy); | 向下画线到中心 |
| ctx.lineTo(ax,ay) | 退回起点 |
| ctx.closePath(); | 关闭路径 |
| ctx.stroke(); | 画提纲 |
| } | 关闭diamond |
| function triangle() { | triangle功能的标题 |
| ctx.fillStyle="teal"; | 设置为彩色 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(ax,ay); | 移动到左角 |
| ctx.lineTo(cx,cy); | 划线穿过 |
| ctx.lineTo(dx,dy); | 向下画线 |
| ctx.lineTo(ax,ay); | 把线拉回来 |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 填充形状 |
| ctx.stroke(); | 画提纲 |
| } | 关闭triangle |
| function rttriangle() { | rttriangle的标题 |
| ctx.fillStyle="teal"; | 设置颜色 |
| ctx.beginPath(); | 开始路径 |
| ctx.moveTo(ax,ay); | 移动到左角 |
| ctx.lineTo(jx,jy); | 在中间画一条线 |
| ctx.lineTo(dx,dy); | 向下画线 |
| ctx.lineTo(ax,ay); | 把线拉回来 |
| ctx.closePath(); | 关闭路径 |
| ctx.fill(); | 填写直角三角形 |
| valley(ex,ey,ex+diag6,ey+diag6,"orange"); | 画对角线山谷 |
| skinnyline(ex,ey,gx,gy); | 在 ex,ey 和 gx,gy 之间画一条更窄的线 |
| } | 关闭rttriangle |
| </script> | 结束script标签 |
| </head> | 结束head标签 |
| <body onLoad="init();"> | body,调用init |
| <video id="sink" loop="loop" preload="auto" controls="controls" width="400"> | video标签 |
| <source src="``sink.mp4video.mp4 | MP4 |
| <source src="``sink.theora.ogv | OGG 型;我只有文件扩展名 |
| <source src="``sink.webmvp8.webm | WEBM;我只有文件扩展名 |
| Your browser does not accept the video tag. | 针对旧浏览器的消息 |
| </video> | 结束video标签 |
| <video id="talk" loop="loop" preload="auto" controls="controls"> | video标签 |
| <source src="``talk.mp4video.mp4 | MP4 类型 |
| <source src="``talk.theora.ogv | OGG 型;请注意,我只有文件扩展名 |
| <source src="``talk.webmvp8.webm | WEBM 型;请注意,我只有文件扩展名 |
| Your browser does not accept the video tag. | 针对旧浏览器的消息 |
| </video> | 结束video标签 |
| <canvas id="canvas" width="900" height="480"> | 设置画布 |
| Your browser does not recognize the canvas element | 针对旧浏览器的消息 |
| </canvas> | 结束canvas标签 |
| <br/> | 破裂 |
| <div id="directions"> Press buttons to advance or go back </div> | 放置方向的地方,带有结束标签div |
| <hr/> | 水平规则 |
| <button onClick="goback();" style="color: #F00">Go back </button> | 设置返回按钮 |
| <button onClick="donext();" style="color: #03F">Next step </button> | 设置下一步按钮 |
| </body> | 结束body标签 |
| </html> | 结束html标签 |

你可以将这种方法直接应用于为其他折纸模型或类似的建筑项目准备指导。但是,请更广泛地思考其他主题,在这些主题中,线条画将受益于数学计算,并且线条画、图像和视频可以一起使用。你不必一开始就什么都知道。准备好一步一步地完成这个项目。

测试和上传应用程序

假设你下载了照片和视频剪辑,这个应用程序可以在你自己的电脑上进行全面测试。如果你把它或者你自己的应用程序上传到服务器上,你需要上传 HTML 文件,所有的图像文件和所有的视频文件。请记住,要让一个应用程序在所有浏览器上工作,您可能需要为每个视频提供多种格式。

摘要

在这一章中,你学习了如何构建一个实际的应用程序来展示包含线条画、照片和视频剪辑的方向。编程技术包括以下内容:

  • 利用数学(代数、几何和三角学)绘制精确的图形

  • 使用数组保存对应于每个步骤的文本和函数名

  • 通过使用功能整合照片和视频剪辑

在下一章中,我们将处理另一个整合照片和视频剪辑的项目:构建一个拼图游戏,当玩家将拼图拼在一起时,它会变成一个视频。

八、拼图视频(JigsawVideo)

在本章中,您将学习以下内容:

  • 将图像分割成小块以制作拼图玩具的方法

  • 如何响应玩家移动棋子来解决难题

  • 如何计算水平和垂直坐标,并操作lefttop样式属性来重新定位屏幕上的元素

  • 关于容差或余量的概念,这样你的玩家就不必完美地解决这个难题

  • 如何让完成的拼图看起来变成一个运行的视频

介绍

本章的项目是一个拼图游戏,完成后会变成一个视频。它已经在配有鼠标的电脑上的 Chrome、Firefox、Opera 和 Safari 上进行了测试。每次加载程序或点击按钮重启程序时,拼图块会随机出现在屏幕上。图 8-1 显示了程序在运行 Firefox 浏览器的台式电脑上运行时的打开屏幕。

img/272384_2_En_8_Fig1_HTML.jpg

图 8-1

在计算机上打开屏幕

在电脑上,玩家使用鼠标移动和重新定位棋子。随机放置的碎片可能会叠放在一起。图 8-2 显示了展开的拼图块。我是用鼠标做的。我的例子有六个矩形的部分。

img/272384_2_En_8_Fig2_HTML.jpg

图 8-2

碎片散开

图 8-3 显示了我是如何把拼图拼在一起的。我可以把拼图放在屏幕上的任何地方。拼图的三块已经拼在一起了。

img/272384_2_En_8_Fig3_HTML.jpg

图 8-3

拼图的进展

请注意,带有标签反馈的框告诉继续工作。图 8-4 显示拼图接近完成。

img/272384_2_En_8_Fig4_HTML.jpg

图 8-4

只剩一片可以放进拼图玩具了

当把这些零件放在一起时,程序允许有误差,我称之为公差。你可以注意到白色的缺口,这说明拼图没有拼好。当我移动到最后一个棋子时,图 8-5 显示了我最后一次移动后不久的屏幕截图。

img/272384_2_En_8_Fig5_HTML.jpg

图 8-5

被视频取代的片段

请注意,反馈现在显示为“好!”一个视频已经开始播放,我停止它,并重置到开始,以获得这个截图。这幅画看起来很完美。事实上,这六块拼图已经被视频取代了。图 8-6 显示了控制显示的视频。这些控件不会自动显示,但是如果玩家将鼠标放在视频下部的顶部,就可以看到它们。不同浏览器的视频控制各不相同。

img/272384_2_En_8_Fig6_HTML.jpg

图 8-6

带控件的视频剪辑

我决定接受挑战,让这个项目适用于 iPhone、iPad 和 Android 手机。这意味着构建一个允许玩家使用手指触摸的用户界面。更准确地说,为了展示我的雄心,我想制作一个既能使用鼠标又能触摸的网站程序。我将把如何响应触摸的解释推迟到第十章,“响应性设计和可访问性”。在这一章中,我将讨论修改程序以适应不同尺寸的窗口的问题。这可以在桌面上通过改变窗口的宽度和/或高度来检查。

请注意,移动设备上的苹果操作系统可能要求用户点击播放按钮来启动所有视频。这被苹果认为是一个特性,而不是一个 bug。要求点击确实给了设备所有者阻止下载视频的机会,这需要时间和电池电量,并可能产生费用。对于拼图到视频的项目,我更希望它是无缝的,这就是在台式机或笔记本电脑上。该程序在我的 Mac 桌面上使用 Chrome 时确实表现出了无缝的行为,所以第二章中讨论的自动播放策略似乎得到了满足。

有了这个可以称为视频奖励拼图游戏项目的介绍,我们可以继续讨论项目的需求和实现。

背景和关键要求

三种截然不同的情况激发我想要建立这个特殊的项目。在我教的一门编程游戏课程中,我用 Adobe Flash 将拼图玩具制作成视频,许多学生很乐意将它们作为自己项目的模型。当我在做一个美国各州教育游戏时,这是第九章的主题,我决定用拼图游戏将各州拼在一起,这是对其他问题的一个很好的补充,比如让玩家在整个美国的地图上点击一个州来识别这个州。最后,我经常收到家庭成员的视频,并无耻地将它们融入我的教学实例中。这些情况是创作拼图变成视频项目的动机。

该项目的需求始于创建拼图块的挑战。一种方法是在这个程序之外创建——剪切——基本图片。如果你这样做,你必须记录每个拼图块的相对位置。你可以让碎片比这个例子更不规则。请参阅“如何构建应用程序并使之成为您自己的应用程序”一节。我在这里描述了一种不同的方法。我的程序剪切了基本图片。这些碎片都是同样的长方形。

在进行调整以适应窗口之后,主要的技术需求是构建用户界面。用户界面包括移动单个棋子的鼠标或手指触摸动作,以及重新拼图的按钮和文本字段中提供的反馈。

该程序在屏幕上显示随机放置的棋子。然后玩家移动棋子来构建图像。每次游戏结束后,程序都会进行一次计算,看看谜题是否已经解开。这个计算必须满足两个要求。拼图可以放在屏幕上的任何地方,所以计算必须根据相对位置进行。其次,在片段的定位中需要有一个公差,因为我们不能要求定位是完美的(例如,到像素)。

当拼图完成后,它会变成一个视频。更准确地说,一段视频出现在屏幕上棋子所在的位置。

HTML5、CSS、JavaScript 和编程特性

jigsaw video 项目使用的特性是 HTML5 结构和通用编程技术的混合。

创建基础图片

第一步是从视频的第一帧创建一个图像文件。如何做到这一点取决于您拥有的工具和您觉得使用起来舒服的东西。我在 Mac 上使用了抓取工具。其他可能的方法是按两次 PC Print Screen 键来捕捉屏幕,或者按 Command+Shift+4 在 Mac 上获得十字光标。还有 SnagIt。如果您有视频编辑工具,可以使用该工具访问第一帧。另一种方法是让拼图图片独立于视频。我永远不会想到这一点,但确实有人建议过。

动态创建的元素

在第二章中,你读到了家庭剪贴画项目,其中图像被重新放置在画布上。我在这里采用了一种稍微不同的方法。每一块都是自己的画布元素,标记是动态创建的,基本图像的各个部分都绘制在每块画布上。这些片段是在由init函数调用的名为makePieces的函数中创建的。游戏是使用一个叫做setupGame的函数来设置的,这个函数也是从init调用的。事实上,我有三个功能——initmakePiecessetupGame——部分是这个项目历史的产物。我重用了为美国各州游戏创建的代码,拼图只是其中的一部分。然而,将一个函数分解成更小的部分通常是一件好事。init函数做一些工作,调用makePieces,再做一些工作,然后调用setupGamesetupGame功能也从endjigsaw调用,这样玩家可以再次玩游戏。通常,我懒得让一个球员再玩一次,因为这很有挑战性。什么需要重置,什么不需要重置?然而,我决定在这种情况下做出努力。这些部分不会被重新创建,而是再次被随机放置在窗口中。我创建这个应用程序的方式并不是唯一可行的方式。在某些情况下,在这里和其他章节中,我选择编写一个比需要的更通用的函数,而在其他情况下我没有这样做。

基本图像(img)和视频元素都在 HTML 主体中指定。元素中的指令使得这些都不可见。基地img从来没有被公开过,但是它的内容被用来建造棋子。init功能由body标签中的onload属性的动作调用。这意味着在加载基础图像文件和视频文件之前,游戏不会开始。init函数执行一些内务处理任务,获取对元素的引用,并调用makePiecessetupGame函数。

makePieces负责确定适应窗口尺寸所需的调整。然后,它实际上切割拼图块。为了调整片段和视频到不同的窗口保持比例,我需要确定基础图像和窗口的关系。我决定让底部宽度不超过window.innerWidth的 80%,底部高度不超过window.innerHeight的 80%。我也不希望基地在尺寸上增长,如果宽度和高度小于这些数额。以下语句在变量ratio中产生关键因素:

origW = base.width;
origH = base.height;
var ratio =Math.min(1.0,.80*window.innerWidth/origW,.80*window.innerHeight/origH);

你可以这么想:如果origW值大于.80*window.innerWidth,说明我的代码需要缩小图片。在这种情况下,Math.min函数的第二个参数将小于 1。身高也是如此。将最小的因子分配给ratio变量。如果这两个因子都大于 1,那么ratio被设置为 1,片段和视频的大小不会增加。如果这些因子中的一个小于 1,那么ratio将被设置为小于 1。以下语句使用ratio设置关键变量。调用drawImage函数将使用opieceWopieceHpieceWpieceH从原始基础创建拼图块。拼图块和视频可能会改变原始尺寸。

baseImgW = origW*ratio;         //possibly modified
baseImgH = origH*ratio;         //possibly modified
v.width = baseImgW;             //possibly modified video width
v.height =baseImgH;             //possibly modified video height
opieceW = origW/numOfCols;      //width of the source for a jigsaw piece
opieceH = origH/numOfRows;      //height of the source for a jigsaw piece
pieceW = ratio*opieceW;         //jigsaw piece width
pieceH = ratio*opieceH;         //jigsaw piece height

makePieces函数调用drawImage来提取和缩放要绘制的基本图像片段,每个片段都放入它自己新创建的画布元素中。这个操作发生在嵌套的for循环中。基础图像分为numOfRowsnumOfCols。片段,即对画布元素的引用以及 x 值和 y 值分别存储在数组片段piecesxpiecesy中。将drawImage方法想象成执行拼图操作,尽管因为缩放的发生而更加复杂:

for(i=0.0; i<numOfRows; i++) {
  for (j=0.0; j<numOfCols; j++) {

//Some other tasks
sCTX.drawImage(base, j*opieceW, i*opieceH, opieceW, opieceH, 0, 0, pieceW, pieceH);
//Some other tasks

  }
}

基本映像未被更改。sCTX是为每件作品创建的画布的上下文。drawImage功能从j*opieceWi*opieceWopieceW宽、opieceH高开始提取—剪辑—基本图像的一部分。它将这部分图像绘制到 sCTX canvas 元素中,将其缩放为pieceWpieceH。这占据了整个画布。

您可以查看表 8-2 中的完整代码。通过将画布块附加到 body 元素并将样式属性 visibility 设置为visible,画布块变得可见。每个画布元素的addEventListener方法为每个画布设置对mousedown的响应。该代码排列这些片段,使它们类似于原始图片,即视频剪辑的第一帧。然而,setupGame功能很快就会被调用,因此玩家将看不到谜题的解答。在嵌套的for循环之后,执行另一个初始化。firstpkel变量指向新创建的元素,该元素包含第一个片段,即左上角的片段。这是代码用来定位视频剪辑的参考点。将各部分相对于彼此正确定位的计算与第一部分的位置无关。

设置游戏

设置拼图游戏的工作从停止视频并使其不显示开始。这在第一次没有必要,但是让代码总是执行这些操作会更容易。下一个任务是在屏幕上随机放置棋子。代码使用Math.randomMath.floor来完成这项工作。将display属性设置为inline以使片段可见,但不带有换行符,如果代码使用了block,就会出现这种情况。当发生播放视频的情况时,通过将显示设置为none来使所有的片段不可见,所以这段代码是必需的。注意,变量v已经在init函数中被设置为指向video元素。

function setupGame() {
  var i;
  var x;
  var y;
  var thingelem;
  v.pause();
  v.style.display = "none";
  doingjigsaw = true;
  for (i=0;i<nums;i++) {
             x = 10+Math.floor(Math.random()*baseImgW*.9);
             y = 50+Math.floor(Math.random()*baseImgH*.9);
             thingelem = pieces[i];
             thingelem.style.top = String(y)+"px";
             thingelem.style.left = String(x)+"px";
             thingelem.style.visibility='visible';
             thingelem.style.display="inline";
  }
  questionfel.feedback.value = "  ";
}

注意

如果您注意到在处理重玩拼图游戏的问题的编码中出现了一定的复杂性,这是典型的。重启、重新初始化等等比编程让某件事情只发生一次更具挑战性。

处理玩家动作

我的方法是首先实现鼠标事件并让它们工作。然后,当我的野心上升到为使用 iPhones 和 iPads 的某些家庭成员构建一个应用程序时,我通过让触摸事件模拟鼠标事件来实现手指触摸。我在本章解释了鼠标事件,在第十章解释了触摸的编码。

使用鼠标事件

移动拼图块的任务是

  • 认识到鼠标按钮是按下的,鼠标是在一块的上面

  • 当鼠标移动时,移动棋子,调整位置以确保棋子不会跳跃,而是保持如同光标附着在它的原始位置一样,可能在元素的中间。

  • 当玩家释放鼠标按钮时,释放或放下元素。

你可能还记得第二章中类似的操作。这个推理表明,我的代码将设置至少三个事件,这就是发生的情况。在makePieces函数中,下面的语句是在嵌套的for循环中执行的,该循环为每一块创建一个画布元素。变量s保存对canvas元素的引用。

s.addEventListener('mousedown',startdragging);

这为每个片段设置了mousedown的事件处理。startdragging函数将名为movingobj的变量设置为事件目标,也就是特定的拼图块。该函数还将全局变量oldxoldy设置为鼠标的位置。该函数为mousemovemouseup设置事件处理。

movingobj.addEventListener("mousemove",moving);
movingobj.addEventListener("mouseup",release);

请注意,当玩家按下鼠标按钮时,如果鼠标不在某个棋子上,则不会发生任何事情,因为“监听”的唯一事件是画布元素上的事件。移动功能是:

function moving(ev)
{
 if((movingobj!=null) &&(mouseDown)){
   newx = parseInt(ev.pageX);
   newy = parseInt(ev.pageY);
   delx = newx-oldx;
   dely = newy-oldy;
   oldx = newx;
   oldy = newy;
   curx = parseInt(movingobj.style.left);
   cury = parseInt(movingobj.style.top);
   movingobj.style.left = String(curx+delx)+"px";
   movingobj.style.top = String(cury+dely)+"px";
 }
};

检查movingobj不为 null 和mouseDown为 true 是多余的,但我决定保留它,以防将来要添加什么。moving功能执行movingobj的相对移动。当鼠标移动时,移动的拼图块水平和垂直移动相同的量。鼠标在画布上的位置无关紧要。无论从上一次mousemove事件发生以来发生了什么变化,都使用相同的变化来调整画布。

当玩家释放鼠标按钮时,调用release功能。我通过设置另一个事件来处理当一个部分在另一个部分之上时调用发布失败的情况:

    document.body.onmouseup = release;

多次调用 release 没有问题。

function release(e){
   mouseDown = false;
   movingobj = e.target;
   movingobj.removeEventListener("mousemove",moving);
   movingobj.removeEventListener("mouseup",release);
   movingobj=null;
   checkpositions();
}

将变量mouseDown改为false意味着如果玩家移动鼠标,直到玩家再次按下鼠标按钮,调用startdragging函数,什么都不会发生。这就完成了鼠标事件的处理。下一节将解释checkpositions功能。

计算拼图是否完成

回想一下,我设置了计算谜题是否完整的要求,即谜题可以位于屏幕上的任何位置,玩家不必精确。另一个或多或少隐含的需求是自动完成检查。玩家释放鼠标或抬起手指后,release函数调用checkpositions。每次移动后都会调用checkpositions函数。别担心,是 JavaScript 在做这项工作,而不是你。

checkpositions函数计算每个块元素的piecesx值和style.left值之间的差值,以及每个块元素的piecesy值和style.top值之间的差值。style.leftstyle.top值是字符串,不是数字,包括"px"。代码需要删除代表“像素”的"px",并计算数值。差异存储在数组deltaxdeltay中。

该函数计算这些差异的平均值(一个用于 x,一个用于 y)。如果拼图完全按照piecesxpiecesy数组中的值放在一起,那么差值都为零,因此,x 和 y 的平均值都为 0。如果将拼图放在一起,使得实际位置每个都更靠近左侧 100 个像素,也就是说,页面更靠左 50 个像素,这是更高的 y 值,那么平均值将是 100 和 50。拼图将被完美地组合在一起,但是位于原始位置的左下方。所有物件的 x 差值为 100,所有物件的 y 差值为 50。每个差异将具有与相应的(x 或 y)平均值相同的值。

目标是而不是要求完美。checkpositions函数的任务是计算 x 和 y 的差值,计算两个平均值,并检查每个差值是否足够接近平均值。

计算完差值后,该函数通过迭代每一部分并与相应的平均值进行比较来执行这些任务。检查是使用绝对值来完成的,因为我们的代码并不关心一块是向左、向右、向上还是向下几个像素。足够接近的标准是保存在变量tolerance中的值。如果任何一块的差距大于tolerance,则认为拼图不完整。关键的if考验是

if ((Math.abs(averagex - deltax[i])>tolerance) || (Math.abs(averagey-deltay[i])>tolerance)) {
         break;
      }

函数计算并返回数组中数字的平均值。这是以通常的方式完成的。变量sum被称为累加器。它被初始化为 0。一个for循环遍历数组中的元素,将每个元素添加到变量sum中。

function doaverage(arr) {
   var sum;
   var i;
   var n = arr.length;
   sum = 0;
   for(i=0;i<n;i++) {
      sum += arr[i];
   }
   return (sum/n);
}

为了以不同的方式总结动作,checkpositions函数使用第一个for循环来确定每个棋子当前水平和垂直位置的差异。然后,它计算两个平均值:x 和 y。然后,该函数使用第二个for循环来查看任何棋子的水平或垂直差异在绝对值上是否与相关平均值有显著差异。一旦发生这种情况,控制就离开for循环,拼图被认为没有完成。如果循环已经完成,则拼图完成,并且视频被定位和播放。checkpositions功能如表 8-2 所示。我选择向玩家显示一条信息,给出对谜题的反馈。表单元素questionfel保存对表单的引用,而feedback是一个输入字段。

我将在下一节描述当谜题被认为完成时会发生什么。

准备、定位和播放视频,并使其隐藏或可见

准备视频剪辑与您在其他涉及视频的项目中看到的一样。您需要创建视频的多种编码。此外,与其他项目一样,当我们不希望视频在特定情况发生之前出现时,style 部分包含使视频最初不可见的指令,将它设置为绝对定位,并(当它显示时)将其放在窗口中与左上角块firstpkel相同的位置。相关代码是

      v.style.left = firstpkel.style.left;
      v.style.top = firstpkel.style.top;
      v.style.display="block";
      v.currentTime = 0;
      v.play();

视频可能会在不同的情况下展示不同的行为。具体来说,在 iPad 或 iPhone 上,播放器可能需要单击一个箭头来播放视频。在我使用 Chrome 或 Firefox 的桌面上(我用的是 iMac ),以及安卓手机上,视频会自动播放,这是我更喜欢的。在第二章中,我讨论了自动播放政策的问题。我没有静音单杠视频。可能是在 Chrome 中对媒体参与指数进行的计算(参见 https://developers.google.com/web/updates/2017/09/autoplay-policy-changes )产生了这些结果。

您已经看到了几个可以使用的 HTML5 特性,以及可以在其他应用程序中使用的编程技巧。下一节将向您展示该项目的大部分代码。整个程序与源代码存储在一起。我包含了同一个应用程序的程序,但是使用了 touch,第十章的源代码。

构建应用程序并使之成为您自己的应用程序

您可以使用自己的视频剪辑来制作这些项目。你也可以自己制作一个拼图玩具,不过你也许应该等下一章再看,那一章描述了一个更复杂的拼图玩具,并且包含了如何切割更复杂形状的一些提示。如果这些部分有透明区域,您仍然需要为整个 canvas 元素设置mousedown事件。但是,你需要检查鼠标“下面”的像素是透明的还是不透明的。

拼图游戏的另一种方法是使用一些公差或余量计算来检查一个块是否足够靠近另一个块,然后将它们合在一起。然后,您的代码必须将咬合在一起的片段移动到一起。

您可以决定忽略或更改“继续工作”或“表现良好”的反馈。我的实现在片段的顶部和视频剪辑的下面有“重新拼图”按钮和反馈框。这意味着,如果玩家选择创建谜题,以便隐藏重做按钮,除了重新加载以重新开始之外,没有其他方法。

以下是拼图转视频项目的非正式总结/大纲:

  • init:初始化,包括调用setupGamesetupjigsaw

  • makePieces:用于创作作品。

  • setupGame:随机定位棋子,设置事件处理。

  • endjigsaw:停止视频并使其不显示,然后调用setupGame开始新游戏。

  • startdragging movingrelease:用于处理事件。

  • checkpositions:用于确定拼图是否完成。

  • doaverage:用于计算数组中数值的平均值。

表 8-1 列出了所有的功能,并指出它们是如何被调用的以及它们调用了什么功能。

表 8-1

拼图转视频项目中的功能

|

功能

|

调用/调用者

|

打电话

|
| --- | --- | --- |
| init | 由<body>标签中的onLoad属性的动作调用 | makePiecessetupGame |
| makePieces | 由init调用 |   |
| setupGame | 由initendjigsaw调用 |   |
| endjigsaw | 由表单主体中的onSubmit设置调用 | setupGame |
| checkpositions | 由release调用 | doaverage |
| doaverage | 由checkpositions调用 |   |
| startdragging | 由makePieces中的事件设置调用 |   |
| moving | 由startdragging中的事件设置调用 |   |
| release | 由主体的startdragging for individual piecesmakePieces中的事件设置调用 | checkpositions |

表 8-2 显示了基本应用程序的代码,每一行都有注释。这些代码中的大部分你已经在前面的章节中看到过了。

表 8-2

完成拼图转视频项目代码

| `` | 页眉 | | `` | `html`标签 | | `` | 特定字符集 | | `` | `head`标签 | | `Jigsaw Monkey bars` | 选项卡的标题 | | `` | 结束`title`标签 | | `` | 关闭`style` | | `` | 结束脚本标签 | | `` | 结束标题标签 | | `` | 正文标签;注意`onload`属性 | | `

Monkey` `bars

` | 显示的标题 | | `
` | 提交按钮结束和重新开始以及反馈的表单 | | `` | 提交按钮 | | `Feedback: ` | 反馈字段 | | `
` | 关闭表单 | | `` | 关闭视频元素 | | `` | `img`为基础图像;t 从不显示 | | `` | 关闭`body` | | `` | 关闭`html` |

测试和上传应用程序

测试应用程序要求基础图像的视频文件和图像文件与 HTML 文档位于同一文件夹中。你可以通过改变窗口和重装来测试对不同窗口尺寸的适应性。

摘要

在本章中,您学习了如何构建一个可以变成视频剪辑的拼图玩具。这些技术包括以下内容:

  • 适应不同的屏幕尺寸,同时保持拼图块和视频的比例。

  • 通过动态创建 HTML 元素和设置 HTML 标记来形成拼图块。

  • 为鼠标事件定义事件处理。

  • 在游戏开始时将拼图块随机放在屏幕上,然后随着鼠标的移动移动拼图块。

  • 生成代码来检查拼图是否完整,是否在公差范围内。

  • 在适当的时候,让视频出现并播放。

在下一章中,我们将处理另一个项目,包括一个拼图游戏,以及玩家其他可能的动作。因为像我的 50 个州这样的拼图很有挑战性,所以我解释了一种使用 HTML5 的localStorage特性将拼图存储为正在进行的工作的方法。

九、美国各州游戏(USStatesGame):构建多活动游戏

在本章中,您将学习以下内容:

  • 如何为一个游戏建立一个用户界面,包括不同类型的玩家移动,包括拼拼图

  • 如何使用鼠标重新定位棋子

  • 如何获取一幅图像,把它分成小块,并确定这些小块的坐标来制作拼图

  • 如何编码和检索拼图游戏的当前状态

  • 如何使用localStorage存储和检索信息,包括在localStorage不允许的情况下使用trycatch

介绍

本章的项目是一个教育游戏,在这个游戏中,玩家/学生响应文本提示,在美国地图上单击一个州,通过键入名称来命名一个用边框表示的州,或者将随机放置在屏幕上的州再次放在一起。图 9-1 为开启画面。

img/272384_2_En_9_Fig1_HTML.jpg

图 9-1

美国各州比赛的开幕画面

我遵循惯例,展示了一张阿拉斯加和夏威夷位置不正确,大小也不成比例的地图。还要注意,罗德岛比实际要大,所以有足够的空间点击它。这个游戏给玩家提供了不同的可能性。图 9-2 显示了点击查找状态按钮的结果。

img/272384_2_En_9_Fig2_HTML.jpg

图 9-2

提示是找到华盛顿

当我点击俄勒冈时,我看到了如图 9-3 所示的内容。

img/272384_2_En_9_Fig3_HTML.jpg

图 9-3

对错误选择的反应

当我点击正确的选项时,应用程序做出适当的响应,如图 9-4 所示。

img/272384_2_En_9_Fig4_HTML.jpg

图 9-4

对正确答案的回应

我认为给玩家提供分散所有州的选项会有所帮助。点击标签为展开状态的按钮后,您会看到如图 9-5 所示的内容。

img/272384_2_En_9_Fig5_HTML.jpg

图 9-5

各州分散开来

玩家可以使用“恢复原始地图/压缩地图”按钮,或者继续播放展开的状态。点击状态名称按钮会产生一个由一个随机选择的状态组成的提示,并被一个边框包围,如图 9-6 所示。

img/272384_2_En_9_Fig6_HTML.jpg

图 9-6

待命名州的边界

注意中间右边(大西洋海岸)非常小的州特拉华州周围的双线边界。这展示了一种情况,其中分散的状态将对玩家产生真正的影响。图 9-7 显示了我输入正确答案后的反应。

img/272384_2_En_9_Fig7_HTML.jpg

图 9-7

提交正确答案后的响应

该应用程序还以拼图游戏的形式为玩家提供活动。点击 Do Jigsaw 按钮后,您将看到类似图 9-8 的内容。我说“类似”是因为这些状态是使用伪随机处理排列的,所以它们每次都会以不同的方式出现。

img/272384_2_En_9_Fig8_HTML.jpg

图 9-8

各州为了拼图而乱成一团

玩家现在可以使用鼠标以与第八章中描述的拼图转视频游戏相同的方式拖放棋子。图 9-9 显示了我正在进行的工作。

img/272384_2_En_9_Fig9_HTML.jpg

图 9-9

拼图正在进行中

请注意,我已经整理了阿拉斯加和夏威夷、西部的五个州、南部的七个州、新英格兰的所有地区以及纽约和新泽西。反馈说伊利诺伊州甚至更多的州都不在位置上。反馈可以改进,但严格来说问题不在于编程。

这对我来说是一个具有挑战性的难题。为了完全公开,也因为它展示了游戏的一个特性,我点击了 Save & Close Jigsaw 按钮,这让我可以看到所有的状态都回到原位。然后我点击恢复最后一个拼图,回到我在的地方。有了这个工具,我能够得到图 9-10 中所示的内容。

img/272384_2_En_9_Fig10_HTML.jpg

图 9-10

不完全正确

反馈表明北达科他州有问题。作弊之后——也就是点击 Save & Close Jigsaw,看着完成的地图——我意识到北达科他州和堪萨斯州这两个相似的矩形需要交换。图 9-11 显示了正确的布置。

img/272384_2_En_9_Fig11_HTML.jpg

图 9-11

正确组装的拼图玩具

请注意,阿拉斯加和夏威夷的位置没有仔细检查。这个谜题被认为是完整的。

在介绍了这个教育游戏的特性之后,我将描述实现的关键需求。

关键要求

教育游戏的关键要求包括为玩家提供不同类型的活动。对于拼图游戏活动,应用程序提供了保存和恢复功能。此功能可用于查看已完成的拼图,或将拼图搁置一段时间,然后做其他事情。游戏制作器的任务是提供用户界面的特征和游戏从一种类型的活动到另一种类型的方式。

该应用程序需要一个完整的美国地图,每个州都可以点击。我在“简介”部分描述的第一类活动是让游戏显示一个州的名称,并提示玩家点击它。应用程序必须能够确定响应是对还是错,并提供反馈。

我演示的下一种活动是相反的。地图上的一个州会以某种方式标记出来,并提示玩家输入名称。挑出一个州有不同的方法。我选择在要命名的州周围加一个边框。程序必须读入玩家输入,并确定名字是否正确。

在实施了这两类活动之后,我想到我们有一些非常小的国家。然后我决定提供展开特性和撤销它的能力。这对其他地图也很有用。我还修改了代表小罗德岛的图片,使其变得更大。

最后,我决定提供一种方法,看看人们是否能把各州放在一起。该应用程序提供了一个拼图游戏,其中状态被随机放置在屏幕上,玩家使用鼠标来重新定位它们。在这一点上,我意识到我需要一些不同于 HTML5 的拖放功能的东西。如果你还没有这样做,你现在可以阅读第八章,了解如何实现拼图游戏。美国各州游戏有两个额外的要求:我需要建立一种方法来进入和退出拼图模式,以便所有的按钮都工作,所以玩家可以点击一个州。我还需要一种方法来保存一个不完整的拼图。这对于第八章拼图转视频项目中的猴栏视频来说是不必要的,但是对于一个有 50 块拼图来说是必要的。我也认为这是一个有教育意义的游戏,所以给玩家一个机会看看完成的地图,也休息一下是合适的。

HTML5、CSS、JavaScript 特性、编程技术和图像处理

实现教育州游戏的特性和技术,在很大程度上,是你以前见过的。然而,把它们放在一起会很棘手,所以在本章和前几章的材料之间会有一些冗余。

获取片段的图像文件并确定偏移量

50 个州的图像文件是本章下载的一部分。但是,由于您可能想要制作自己的地图拼图,我将描述拼图块的关键特征以及检查定位和恢复必须记录的完整地图所需的信息。

您需要为每个拼图块生成图像文件,也就是说,为我的游戏生成美国的每个州的图像文件。由于没有一个状态是严格的矩形,并且图像文件必须是矩形,所以图像将是每个状态的边界框,实际状态之外的区域是透明的。没有特殊待遇,以适应夏威夷或上下密歇根州的岛屿。

制作代表各州的单张地图的首要任务是获得一张美国地图(或你选择的国家或地区)并选择你最喜欢的图像处理程序。我使用了 Adobe Flash,这是我制作第一个美国游戏示例时流行的,但我将使用在线图像编辑工具 pixlr 来说明这个过程。源代码中的数字来自我最初的实现,不会是这里提到的数字。图 9-12 显示美国地图。阿拉斯加和夏威夷定位不准。当我的代码检查玩家完成的工作时,我通过简单地不检查这两种状态的定位来巧妙应对这个挑战。

img/272384_2_En_9_Fig12_HTML.jpg

图 9-12

pixlr 中的原始完整地图图像

下一个任务是确定每个州的相对位置信息。所需的信息是每个州的边框左上角的相对位置。这个点可能不在状态上,但会确定正确的位置。在图 9-13 中,我使用选框工具在伊利诺斯州周围画了一个方框。

img/272384_2_En_9_Fig13_HTML.jpg

图 9-13

伊利诺伊周围的盒子

这样做的时候,我从导航面板上记下起始位置的 x 和 y 坐标,即盒子的左上角,如图 9-14 所示。

img/272384_2_En_9_Fig14_HTML.jpg

图 9-14

导航面板

注意

这些不是伊利诺伊州上角的坐标,而是我截图过程中产生的。导航器面板显示鼠标的位置。

下一个任务是使用 pixlr 工具栏上“编辑”下的下拉菜单中的“第一次复制”,然后使用“文件”下的“新图像”,将选区复制到新图像中。图 9-15 显示了出现的面板。请注意,我已经为图像命名为 Illinois,并给出了从剪贴板中取出图像并保持透明度的指令。我需要做一些事情来创建透明区域。

img/272384_2_En_9_Fig15_HTML.jpg

图 9-15

面板创建一个新的图像,给它一个名称和说明

pixlr 程序现在有两个图像,我需要移动大地图来得到新的图像。我还使用了视图下的缩放功能来放大它。如图 9-16 所示。

img/272384_2_En_9_Fig16_HTML.jpg

图 9-16

伊利诺伊州新形象控股公司

我现在使用魔杖(有时也称为魔术棒)工具,并点击浅绿色伊利诺伊州。这仅选择伊利诺伊州,如图 9-17 所示,使用颜色。它不一定是地图上唯一的浅绿色区域,而只是与相邻区域不同。

img/272384_2_En_9_Fig17_HTML.jpg

图 9-17

使用魔杖工具选择伊利诺伊州

我想要的是把所有东西都剪下来,除了伊利诺伊州的形状。这是通过编辑/反转选择来执行的。如图 9-18 所示。对了,我把这个文件存成了带透明度的 PNG,命名为Illinois1,就是为了不让自己搞混。

img/272384_2_En_9_Fig18_HTML.jpg

图 9-18

反向选择:除了伊利诺伊州的形象

然后我编辑/剪切并制作出白色背景下的伊利诺伊州形状,实际上是透明的。图 9-19 显示了我保存的图像。

img/272384_2_En_9_Fig19_HTML.jpg

图 9-19

伊利诺伊州的形象对透明背景

这些是每个国家的必要步骤。

我创建了保存图像文件名以及水平(x)和垂直(y)偏移数据的数组。我还创建了一个数组,列出了各州的全名。这是四个平行的阵列。另一种方法是用下划线系统地保存文件,以避免任何内部中断——例如,North_Carolina.gif。我可以编写代码,用空格替换下划线,以便显示游戏和检查玩家的答案。然而,我决定直接说出这些名字。描述了保存程序状态所需的所有内容的四个并行数组的创建之后,现在是时候回顾如何创建元素了。

动态创建元素

第六章和第八章分别涉及动态生成 HTML 标记——也就是说,在运行时。你可能创建的州游戏和其他地图游戏也将采用这种技术。这项工作在函数setupgame中完成。

代码决定了来自nums变量的多少元素——也就是拼图块——被设置为states数组的长度。例如,如果你用 10 个国家建造一个拼图,nums将被设置为 10。一个for循环用于为每个状态构建一个元素。每个元素都有一个生成的唯一 ID 值。任何元素的属性innerHTML都被设置为标记。代码使用数组变量statesstatesxstatesy中的信息。与上一章中的情况一样,代码将数字转换为字符串,然后连接字符串"px"以产生用于设置元素的style.topstyle.left属性的值。代码如下:

function setupgame() {
   var i;
   var x;
   var y;
   var uniqueid;
   var s;
   for(i=0;i<nums;i++) {
      uniqueid = "a"+String(i);
      s = document.createElement('state');
      s.innerHTML = (
                   "<img src='"+states[i]+"' id='"+uniqueid+"'/>");
      document.body.appendChild(s);
      thingelem = document.getElementById(uniqueid);
      x = statesx[i] +310;
      y = statesy[i] + 200;
      thingelem.style.top = String(y)+"px";
      thingelem.style.left = String(x)+"px";
      stateelements.push(thingelem);
   }
  questionfel = document.getElementById("questionform");
   questionfel.style.left = "100px";
   questionfel.style.top = "500px";
   questionfel.question.value = " ";
   questionfel.feedback.value = "  ";
}

该元素由自定义类型'state'创建。它的innerHTML被设置为适当的值。使用statesxstatesy数组中的偏移值完成定位(对应于我在第八章中命名为piecesxpiecesy的数组)。setupgame函数的第二部分定位已经出现在body元素中的表单。该表格将用于识别和命名活动。

整体用户界面

是时候展示应用程序的body元素了,因为它将显示各种操作的按钮:

<body id="body" onLoad="init();">
<button onClick="spread();">Spread out states </button>
<button onClick="restore();">Restore original /compress map </button>
<button onClick="setupfindstate();">Find the state </button>
<button onClick="setupidentifystate();">Name the state</button>
<button onClick="setupjigsaw();">Do jigsaw</button>
<button onClick="restorepreviousjigsaw();">Restore last jigsaw in process </button>
<h1>USA</h1>
<form id="questionform" name="questionform" onSubmit="return checkname();">
State name: <input type="text" name="question" value="   " size="40"/>
<input name="submitbut" type="submit" value="       " size="30"/>
Feedback: <input type="text" name="feedback" value="   " size="40" />
</form>
</body>

HTML 标记在屏幕顶部生成六个按钮(回头参考图 9-1 )。顶部的每个按钮都调用一个功能;在接下来的几节中,我们将详细介绍每一种方法。底部的表格以不同的方式用于三种不同类型的活动。这是一个设计决策;我试图有效地利用屏幕空间,避免多种形式的混乱,这可能会给玩家带来困惑。

要求玩家点击一个状态的用户界面

玩家单击 Find State 按钮后,应用程序会生成一个问题。在选择州之前,程序会删除最后选择的州周围可能存在的任何边框。如果玩家刚刚执行了 name a 状态活动,就会出现这种情况。如果这是玩家的第一个活动,代码不会产生错误,而只是将第 0 个状态的边界设置为空,这就是它已经存在的状态。这是一个好习惯,让任何活动开始做这种类型的内务。这使得应用程序在将来更容易更改或升级。同样,如果前一个问题也是一个识别问题,代码也不会产生错误。这种从一个活动到另一个活动的过渡必须被注意,以使游戏能够顺利进行。当玩家进入下一个活动时,我们不希望任何状态有边界。

setupfindstate函数在状态中随机选择。全局变量 choice 保存随机选择的值。然后,该函数为对应于一个状态的每个元素设置事件处理。对玩家的提示放在表单的问题字段中。

function setupfindstate(){
   var i;
   var thingelem;
   stateelements[choice].style.border="";
   choice = Math.floor(Math.random()*nums);
   for (i=0;i<nums;i++) {
    thingelem = stateelements[i];
    thingelem.addEventListener('click',pickstate,false);
   }
   var nameofstate = names[choice];
   questionfel.question.value = "Click on "+nameofstate;
   questionfel.feedback.value = "  ";
   questionfel.submitbut.value = "";
}

玩家对此活动的适当反应是点击地图上的一个州。当玩家点击任何状态时,JavaScript 事件处理被设置为调用pickstate函数。这个函数的任务是确定玩家的选择是否正确。为此,我的代码使用了传递给函数的事件信息中的信息和由setupfindstate设置的全局变量choice中的值。pickstate的代码是

function pickstate(ev) {
    var picked = Number(ev.target.id.substr(1));
   if (picked == choice) {
   questionfel.feedback.value = "Correct!";
   }
   else {
      questionfel.feedback.value = "Try Again.";
   }
 }

现在我需要提醒您我是如何为每个状态元素设置 ID 字段的。我使用了索引值 0 到 49,并在开头添加了一个一个。增加一个一个并不是绝对必要的。当我想到我可能会创建其他元素集时,我这样做了。pickstateev参数有一个目标属性,引用接收点击事件的目标。那个目标的 ID 将是a0,或者a1,或者a2,等等。String方法substr从参数开始提取字符串的子串,所以substr(1)返回 0、1、2 等等。我的代码将字符串转换成数字。现在可以将它与全局变量choice中的值进行比较。

你可以决定限制玩家尝试和/或提供提示的次数。

要求玩家命名一个州的用户界面

在玩家选择命名一个州的活动后,调用setupidentifystate函数。任务是在地图上的一个州周围放置一个边界,并提示玩家输入名称。对于这个操作,与上一个不同,我的代码为 submit 按钮输入了一个值。该函数还删除了单击状态的事件处理。

function setupidentifystate(){
   stateelements[choice].style.border="";
   stateelements[choice].style.zIndex = "";
   choice = Math.floor(Math.random()*nums);
   stateelements[choice].style.border="double";
   stateelements[choice].style.zIndex = "20";
   questionfel.question.value = "Type name of state with border HERE";
   questionfel.submitbut.value = "Submit name";
   questionfel.feedback.value = "  ";
   var thingelem;
   for (i=0;i<nums;i++) {
    thingelem = stateelements[i];
    thingelem.removeEventListener('click',pickstate,false);
   }
}

玩家的动作由checkname函数检查。这已经被设置为表单的onsubmit属性。函数checkname实际上有双重功能:如果当前活动正在拼图,checkname通过将州恢复到它们的原始位置,即整个美国的原始地图,来结束该活动。如果玩家没有在玩拼图游戏,checkname会检查玩家是否输入了所选州的正确名称。checkname中的代码如下:

function checkname() {
   if (doingjigsaw) {
      restore();
   }
   else {
   var correctname = names[choice];
   var guessedname = document.questionform.question.value;

   if (guessedname==correctname) {
      questionfel.feedback.value = "Correct!";
   }
   else {
      questionfel.feedback.value = "Try again.";

   }
   return false;
   }
}

请注意,我没有限制尝试的次数,也没有对拼写错误给出任何暗示或容忍。

展开碎片

在保持状态的位置关系的同时展开它们的任务很简单,尽管我用常数做了一些实验来得到我想要的效果。想法是以系统的方式使用偏移值。偏移表示从地图大致中心点的距离。我的代码扩展了除阿拉斯加和夏威夷之外的所有州的偏移值。我认为阿拉斯加和夏威夷是最后两个州。代码如下:

function spread() {
   var i;
   var x;
   var y;
   var thingelem;
   for (i=0;i<nums-2;i++) {  // don't move alaska or hawaii

      x = 2.70*statesx[i] +410;
      y = 2.70*statesy[i] + 250;
      thingelem = stateelements[i];
      thingelem.style.top = String(y)+"px";
      thingelem.style.left = String(x)+"px";
   }
}

恢复状态只是将它们重新定位在statesxstatesy数组中指示的值上。下面将在“保存和重新创建拼图游戏的状态并恢复原始地图”一节中解释restore功能。

设置拼图玩具

设置 jigsaw 活动包括在屏幕上随机定位状态,并为鼠标操作设置事件处理。这还意味着关闭默认的拖放事件处理,并关闭屏幕顶部的按钮。屏幕底部的问题表单上的提交按钮将保持可操作,该按钮将执行保存拼图状态的操作,如下一节所述。停止 jigsaw 活动、恢复地图并返回到其他活动的唯一方法是单击按钮。

新创建的 ID 为fullpagediv是为了防止拖放默认动作而创建的,它在样式部分被设置为不覆盖包含表单的屏幕底部。CSS 是

#fullpage
{
   display:block;
   position:absolute;
   top:0;
   left:0;
   width:100%;
   height:90%;
   overflow: hidden;
   z-index: 1;
}

回想一下,在 CSS 中,分层是通过属性z-index完成的。在 JavaScript 中,属性是zIndexsetupjigsaw功能如下:

function setupjigsaw() {
  doingjigsaw = true;
   stateelements[choice].style.border="";
  var i;
  var x;
  var y;
  var thingelem;
    for (i=0;i<nums;i++) {
      x = 100+Math.floor(Math.random()*600);
      y = 100+Math.floor(Math.random()*320);
      thingelem = stateelements[i];
      thingelem.style.top = String(y)+"px";
      thingelem.style.left = String(x)+"px";
      thingelem.removeEventListener('click',pickstate,false);
     }
  d.onmousedown = startdragging;
  d.onmousemove = moving;
  d.onmouseup = release;
  var df = document.createElement('div');
  df.id = "fullpage";
  bodyel.appendChild(df);
   questionfel.question.value = "";
   questionfel.submitbut.value = "Save & close jigsaw";
   questionfel.feedback.value = "  ";
   questionfel.style.zIndex = 100;
}

玩家用鼠标重新定位拼图玩具。回到第八章了解鼠标事件的使用说明。每次玩家放开鼠标按钮时,都会检查完整性。release函数调用我命名为checkpositions的函数。checkpositions谜题计算棋子实际位置与存储在statesxstatesy数组中的偏移量在 x 轴和 y 轴上的平均差值。然后,代码检查与相应平均值的差值是否大于tolerance值。一旦发现一个片段不合适,该函数就停止迭代。对于第八章中非常简单的六块拼图,当这种情况发生时,我给玩家的反馈只是显示“继续工作”对于美国各州的比赛,我想做更多的事情。我决定做的是报告第一个状态,其中 x 或 y 的差值大于平均值。当大多数部分都不在适当的位置时,这些信息不是特别有用,所以这是一个改进的机会。

保存和重新创建拼图游戏的状态并恢复原始地图

正如我前面提到的,结束 jigsaw 活动的唯一方法是单击表单上的 submit 按钮。如果全局变量doingjigsawtrue,那么restore函数被调用。restore功能将关闭鼠标的事件处理并移除fullpage div。我意识到,即使是我也不可能在一次会议中不作弊地完成拼图——也就是说,看着完成的拼图。然而,我越来越擅长了。这就是促使我实现保存和恢复功能的原因。

定义应用程序状态的问题自然取决于应用程序。保存拼图游戏的状态需要代码来编码每个拼图块的位置。对于拼图游戏,需要存储的是每个元素的style.topstyle.left属性。我将使用 HTML5 的localStorage特性,这是一个 cookies 版本。接下来我会描述localStorage。这个程序的目标是用一个字符串保存所有的信息。我首先做的是用&style.topstyle.left连接成一个字符串。然后,我使用以下代码行将每个字符串放入一个数组中:

xydata.push(thingelem.style.top+"&"+thingelem.style.left);

当所有 50 个字符串都放入数组中时,我的代码使用join方法将所有内容合并到一个大数组中,并用我选择的分隔符(;)将它们分开。这是使用localStorage存储的字符串。

在 HTML5 中,localStorage是 cookies 的变体。值以名称/值对的形式存储在玩家(客户端)的计算机上。一个localStorage项目与浏览器相关联。使用 Firefox 时存储的拼图状态在使用 Chrome 时将无法使用。对于localStorage项的名称,我使用名称jigsaw,对于值,使用join操作的结果。

localStorage设施可能不工作。例如,玩家可能已经使用浏览器设置来阻止对 cookies、localStorage或其他类似特征的任何使用。一个localStorage项目与一个特定的网络域相关联。Chrome 允许在本地计算机上设置和检索程序。当我最初构建这个应用程序时,Firefox 抛出了一个检索数据的错误。我的代码使用trycatch在出现问题时给出警告声明。图 9-20 显示了当使用本地计算机上的文件时,尝试恢复使用 Firefox 保存的拼图游戏的结果。如果玩家/用户关闭了 cookies 的使用,也会发生这种情况。

img/272384_2_En_9_Fig20_HTML.jpg

图 9-20

尝试在 Firefox 中本地使用 localStorage 时显示警告

继续,有两个不同的功能:restorerestorepreviousjigsaw。记住restore功能有双重功能:它在棋子铺开后恢复原始地图在玩家完成拼图活动后恢复原始地图。

function restore() {
   var i;
   var x;
   var y;
   var thingelem;
   var df;
   var lsname = "jigsaw";
   var xydata = [];
   var stringdata;
   if (doingjigsaw) {
      doingjigsaw = false;
       d.onmousedown = "";
         d.onmousemove = "";
         d.onmouseup = "";
         df = document.getElementById("fullpage");
       bodyel.removeChild(df);
       for (i=0;i<nums;i++) {
          thingelem = stateelements[i];
         xydata.push(thingelem.style.top+"&"+thingelem.style.left);
       }
       stringdata = xydata.join(";");
       try {
         localStorage.setItem(lsname,stringdata);
        }
       catch(e) {
         alert("data not saved, error given: "+e);
       }
   }
   for (i=0;i<nums;i++) {
      x = statesx[i] +310;
      y = statesy[i] + 200;
      thingelem = stateelements[i];
      thingelem.style.top = String(y)+"px";
      thingelem.style.left = String(x)+"px";
   }
}

restorepreviousjigsaw函数试图以jigsaw的名字读入作为一个长字符串存储在localStorage中的数据;将字符串解码为 50 个字符串的数组,每个字符串保存topleft信息;并利用这些信息来定位棋子。然后,该函数为鼠标事件设置事件处理,并设置fullpage div。最后,该函数设置提交按钮的标签,以指示该按钮保存并关闭谜题。代码如下:

function restorepreviousjigsaw() {
   var i;
   var lsname = "jigsaw";
   var xydata;
   var stringdata;
   var ss;   // will hold combined top and left for a state
   var ssarray;
   var thingelem;
   try {
   stringdata = localStorage.getItem(lsname);
   xydata = stringdata.split(";");
   for (i=0;i<nums;i++) {
     ss = xydata[i];
     ssarray = ss.split("&");
     thingelem = stateelements[i];
     thingelem.style.top = ssarray[0];
     thingelem.style.left = ssarray[1];
   }

   doingjigsaw = true;
   stateelements[choice].style.border="";
   d.onmousedown = startdragging;
                d.onmousemove = moving;
                d.onmouseup = release;
                 var df = document.createElement('div');
                df.id = "fullpage";
                bodyel.appendChild(df);
   questionfel.question.value = "";
   questionfel.submitbut.value = "Save & close jigsaw";
   questionfel.feedback.value = "  ";
   questionfel.style.zIndex = 100;
   }
   catch(e) {
      alert("Problem in restoring previous puzzle. Click on Do jigsaw.");}
}

构建应用程序并使之成为您自己的应用程序

您可以通过改进和构建 states 应用程序来创建自己的项目,可能会给出提示或记录分数,或者将应用程序用作世界不同地方的模型。对于不同的地图,请注意我对阿拉斯加和夏威夷的特殊处理。您可能想要删除出现的nums-2。您可以添加另一个带有首都名称的并行数组,并使命名首都和标识带有指示首都的州成为附加活动。您还可以使用它作为模型来识别任何图或图片的部分(例如,身体的部分)。请注意,每个活动都有一个设置函数和一个检查响应的函数。

你可以使用第八章中所描述的方法,通过手指触摸来完成这个项目。美国各州对手机来说似乎太多了,但对平板电脑来说可能是可行的。您可以使用第五章中所示的方法将内容提取到外部文件中。如果你觉得很勇敢,你可能还想尝试使用 SVG(可缩放矢量图形)来创建一个矢量版本的地图。

该应用程序演示了您可以用于其他项目的个别功能。下面是州游戏中功能的非正式概述/总结:

  • init用于初始化,包括调用setupgame

  • setupgame构建状态元素并定位表单。

  • setupfindstate设置点击状态功能,pickstate检查玩家的反应。

  • setupidentifystate设置名称的输入,checkname检查响应。

  • setupjigsaw设置拼图玩具。函数startdraggingmovingrelease以及offsetdraw处理玩家使用鼠标移动棋子的动作。checkpositions函数和doaverage一起检查拼图是否完成。

  • spread展开棋子,restore将棋子恢复到原始地图位置。restore功能也使用localStorage保存拼图游戏的状态。

  • restorepreviousjigsawlocalStorage中提取信息,按原样设置拼图。

更正式的说法是,表 9-1 列出了所有的函数,并指出它们是如何被调用的以及它们调用了什么函数。请注意,由于函数被指定为对象类型的方法,因此会调用几个函数。

表 9-1

功能 美国各州游戏项目

|

功能

|

调用/调用者

|

打电话

|
| --- | --- | --- |
| init | 由<body>标签中的onLoad属性的动作调用 | setupgame |
| setupgame | 由init调用 |   |
| pickstate | 由setupfindstate中的addEventListener调用调用 |   |
| spread | 通过按钮调用 |   |
| restore | 通过按钮和checkname调用 |   |
| restorepreviousjigsaw | 通过按钮调用 |   |
| setupfindstate | 通过按钮调用 |   |
| setupidentifystate | 通过按钮调用 |   |
| checkname | 作为表单中onSubmit的动作调用 | restore |
| checkpositions | 通过释放鼠标调用(mouseup事件) | doaverage |
| doaverage | 由checkpositions调用 |   |
| setupjigsaw | 通过按钮调用 |   |
| release | 通过设置restorepreviousjigsawsetupjigsaw中的事件来调用 | checkpositions |
| startdragging | 通过设置restorepreviousjigsawsetupjigsaw中的事件来调用 | offset |
| moving | 通过设置restorepreviousjigsawsetupjigsaw中的事件来调用 | draw |
| draw | 通过移动鼠标调用(mousemove事件) |   |
| offset | 由startdragging调用 |   |

表 9-2 显示了基本应用程序的代码,每一行都有注释。

表 9-2

美国各州游戏项目的完整代码

|

代码行

|

描述

|
| --- | --- |
| <!DOCTYPE html> | 文档类型标题 |
| <html> | html标签 |
| <head> | head标签 |
| <title>USA States game</title> | 完整标题 |
| <style> | style标签 |
| img {position:absolute;} | 绝对定位的所有图像元素 |
| form {position: absolute; z-index: 10;} | 绝对定位的表单 |
| body{ height:100%; margin: 0;} | 身体造型占据整个高度 |
| #fullpage | 已创建的样式指令div |
| { display:block; position:absolute; top:0; left:0; width:100%; height:90%; overflow: hidden; z-index: 1; } | 占据整个宽度和几乎整个高度;下面一层 |
| </style> | 结束style标签 |
| <script type="text/javascript"> | script标签 |
| var names = [ | 各州的名称;许多带有状态信息的并行数组中的一个;这里的顺序和分组并不重要,但顺序必须相同,阿拉斯加和夏威夷排在最后 |
| "Illinois","Iowa","Missouri","Oregon","Michigan", |   |
| "Indiana","Vermont","New Hampshire","Maine","South Dakota","North Dakota", |   |
| "Ohio","Wisconsin","Kentucky","Tennessee", |   |
| "North Carolina","South Carolina","Georgia","Alabama","Mississippi", |   |
| "Virginia","West Virginia","Maryland","Delaware","Pennsylvania","New Jersey","New York", |   |
| "Rhode Island", "Connecticut","Massaschusetts","Louisiana","Arkansas","Minnesota", |   |
| "Florida","Kansas", |   |
| "Arizona","California","Colorado","Idaho","Montana","Nebraska", |   |
| "Nevada","New Mexico","Texas","Oklahoma","Utah","Washington","Wyoming","Hawaii","Alaska" |   |
| ] | names数组的结尾 |
| var states = [ | 图像文件地址数组 |
| "illinois.gif", |   |
| "iowa.gif", |   |
| "missouri.gif", |   |
| "oregon.gif", |   |
| "michigan.gif", |   |
| "indiana.gif", "vermont.gif","newhampshire.gif","maine.gif","southdakota.gif","northdakota.gif", |   |
| "ohio.gif","wisconsin.gif","kentucky.gif","tennessee.gif", |   |
| "northcarolina.gif","southcarolina.gif","georgia.gif","alabama.gif","mississippi.gif", |   |
| "virginia.gif","westvirginia.gif","maryland.gif","delaware.gif", |   |
| "pennsylvania.gif","newjersey.gif","newyork.gif", |   |
| "rhodeislandbig.gif","connecticut.gif","massachusetts.gif","louisiana.gif","arkansas.gif","minnesota.gif", |   |
| "florida.gif","kansas.gif", |   |
| "arizona.gif","california.gif","colorado.gif","idaho.gif","montana.gif","nebraska.gif", |   |
| "nevada.gif","newmexico.gif","texas.gif","oklahoma.gif","utah.gif","washington.gif","wyoming.gif","hawaii.gif","alaska.gif" |   |
| ]; | 图像文件地址数组结束 |
| var statesx = [ | 水平(x)偏移量数组 |
| 88.65,60.15,65.40, |   |
| -81.70,90.40, |   |
| 107.40,171.95,181.00,183.00,21.10,22.60, |   |
| 121.70,78.90,103.65,99.40, |   |
| 132.20,138.95,125.45,110.45,93.90, |   |
| 138.95,138.95,151.65,171.95,144.20,174.20,147.95, |   |
| 187.75,179.35,177.60,77.40,73.65,54.15, |   |
| 115.70,32.35, |   |
| -44.95,-86.85,-8.15,-47.20,-32.15,21.10, |   |
| -66.70,-11.15,-4.40,22.60, -36.70,-72.50,-15.65,-300.95,-230.30 |   |
| ]; | statesx数组的结尾 |
| var statesy = [ | 垂直(y)偏移量数组 |
| -26.10,-29.85,-8.45, |   |
| -64.75,-59.05, |   |
| -22.70,-66.00,-67.30,-85.65,-47.15,-70.30, |   |
| -27.90,-55.30,-3.60,12.90, |   |
| 5.20,21.45,26.40,27.90,29.65, |   |
| -13.20,-17.10,-19.85,-20.85,-36.40,-31.35,-61.30, |   |
| -41.85,-41.85,-50.85,47.10,21.15,-72.70, |   |
| 55.45,-2.85, |   |
| 15.15,-35.75,-11.85,-76.70,-76.30,-23.85, |   |
| -27.60,18.15,22.65,19.65,-22.35,-83.45,-41.75,31.55,-171.30 |   |
| ]; | statesy数组的结尾 |
| var doingjigsaw = false; | 指示是否执行jigsaw的标志 |
| var bodyel; | 用于保存对主体的引用 |
| var nums = states.length; | 状态数 |
| var stateelements = []; | 将保存动态创建的元素 |
| var questionfel; | 用于保存对表单的引用 |
| function init(){ | init功能的标题 |
| setupgame(); | 调用setupgame |
| bodyel = document.getElementById("body"); | 设置用于添加fullpage div的参考 |
| } | 关闭init功能 |
| function setupgame() { | setupgame功能的标题 |
| var i; | 用于索引 |
| var x; | 对于 x 值 |
| var y; | 对于 y 值 |
| var uniqueid; | 对于为每个元素创建的唯一 ID |
| var s; | 保留每个新创建的元素 |
| for(i=0;i<nums;i++) { | 遍历各州 |
| uniqueid = "a"+String(i); | 定义一个 ID |
| s = document.createElement('state'); | 创建元素 |
| s.innerHTML = (``"<img src='"+states[i]+``"' id='"+uniqueid+"'/>"); | 将新创建的元素的 HTML 标记内容设置为具有所示属性的图像 |
| document.body.appendChild(s); | 附加到正文 |
| thingelem = document.getElementById(uniqueid); | 获取参考 |
| x = statesx[i] +310; | 计算水平坐标 |
| y = statesy[i] + 200; | 计算垂直坐标 |
| thingelem.style.top = String(y)+"px"; | 将style.top设置为 x |
| thingelem.style.left= String(x)+"px"; | 将style.left设置为 y |
| stateelements.push(thingelem); | 添加到stateelements数组 |
| } | 关闭for回路 |
| questionfel = document.getElementById(“questionform”); | 设置对表单的引用 |
| questionfel.style.left = "100px"; | 水平放置表单 |
| questionfel.style.top = "500px"; | 垂直放置表单 |
| questionfel.question.value = " "; | 清除问题字段 |
| questionfel.feedback.value = "  "; | 清除反馈字段 |
| } | 关闭setupgame功能 |
| function pickstate(ev) { | pickstate功能的标题 |
| var picked = Number(ev.target.id.substr(1)); | 提取并计算玩家选择的州的指数 |
| if (picked == choice) { | 与选择相比 |
| questionfel.feedback.value = "Correct!"; | 显示正确的反馈 |
| } | 关闭条款 |
| else { | 其他 |
| questionfel.feedback.value = "Try Again."; | 显示反馈以重试 |
| } | 关闭条款 |
| } | 关闭pickstate功能 |
| function spread() { | spread功能的标题 |
| var i; | 用于索引 |
| var x; | 对于 x 值 |
| var y; | 对于 y 值 |
| var thingelem; | 对于元素 |
| for (i=0;i<nums-2;i++) { | 遍历 48 个州 |
| x = 2.70*statesx[i] +410; | 拉伸 x 并添加常数 |
| y = 2.70*statesy[i] + 250; | 拉伸 y 并添加常数 |
| thingelem = stateelements[i]; | 获取第 i 个元素 |
| thingelem.style.top = String(y)+"px"; | 设置style.top |
| thingelem.style.left= String(x)+"px"; | 设置style.left |
| } | 关闭for回路 |
| } | 关闭spread功能 |
| function restore() { | restore功能的标题 |
| var i; | 用于索引 |
| var x; | 对于 x |
| var y; | 为了你 |
| var thingelem; | 对于元素引用 |
| var df; | 用于移除fullpage |
| var lsname = "jigsaw"; | localStorage的名称 |
| var xydata = []; | 用于储蓄 |
| var stringdata; | 用于储蓄 |
| if (doingjigsaw) { | 检查doingjigsaw是否为true |
| doingjigsaw = false; | 设置为false |
| d.onmousedown = ""; | 移除事件处理 |
| d.onmousemove = ""; | 移除事件处理 |
| d.onmouseup = ""; | 移除事件处理 |
| df=``document.getElementById("fullpage"); | 参考 |
| bodyel.removeChild(df); | 移除df |
| for (i=0;i<nums;i++) { | 迭代状态 |
| thingelem = stateelements[i]; | 获取对第 i 个状态元素的引用 |
| xydata.push(thingelem.style.top+"&"+thingelem.style.left); | 创建一个组合了顶部和左侧设置的字符串,并将其添加到xydata数组 |
| } | 关闭for回路 |
| stringdata = xydata.join(";"); | 从数组生成一个字符串 |
| try { | 尝试(因为localStorage可能有问题) |
| localStorage.setItem(lsname,stringdata); | 设置localStorage项 |
| } | 结束try子句 |
| catch(e) { | catch条款 |
| alert("data not saved, error given: "+e); | 出错信息 |
| } | 关闭catch子句 |
| } | 如果doingjigsaw关闭 |
| for (i=0;i<nums;i++) { | 迭代状态 |
| x = statesx[i] +310; | 将 x 设置为原始 x 坐标 |
| y = statesy[i] + 200; | 将 y 设置为原始 y 坐标 |
| thingelem = stateelements[i]; | 获取对第个状态的引用 |
| thingelem.style.top = String(y)+"px"; | 设置style.top |
| thingelem.style.left= String(x)+"px"; | 设置style.left |
| } | 关闭for回路 |
| } | 关闭恢复功能 |
| function restorepreviousjigsaw() { | restorepreviousjigsaw功能的标题 |
| var i; | 用于索引 |
| var lsname = "jigsaw"; | 用于localStorage的名称 |
| var xydata; | 将用于提取数据 |
| var stringdata; | 将用于提取数据 |
| var ss; | 将为一个状态按住组合的顶部和左侧 |
| var ssarray; | 将用于提取数据 |
| var thingelem; | 第 i 个状态元素的引用 |
| try { | 尝试 |
| stringdata = localStorage.getItem(lsname); | 以“jigsaw”的名称获取保存在localStorage中的数据 |
| xydata = stringdata.split(";"); | 从stringdata生成一个数组 |
| for (i=0;i<nums;i++) { | 迭代状态 |
| ss = xydata[i]; | 提取xydata的第 i 个元素 |
| ssarray = ss.split("&"); | 拆分该字符串以获得两个值 |
| thingelem = stateelements[i]; | 获取第 i 个元素 |
| thingelem.style.top = ssarray[0]; | 设置style.top为第 0 项 |
| thingelem.style.left = ssarray[1]; | 将style.left设为第一项 |
| } | 关闭for回路 |
| doingjigsaw = true; | 准备拼图 |
| stateelements[choice].style.border=""; | 移除任何边框 |
| d.onmousedown = startdragging; | 设置事件处理 |
| d.onmousemove = moving; | 设置事件处理 |
| d.onmouseup = release; | 设置事件处理 |
| var df = document.createElement('div'); | 创建一个div |
| df.id = "fullpage"; | 给它一个 IDfullpage |
| bodyel.appendChild(df); | 附加到正文 |
| questionfel.question.value = ""; | 清除问题字段 |
| questionfel.submitbut.value = "Save & close jigsaw"; | 设置提交按钮的标签 |
| questionfel.feedback.value = "  "; | 清除反馈字段 |
| questionfel.style.zIndex = 100; | 将表单设置在顶部 |
| } | 关闭try子句 |
| catch(e) { | 捕捉 |
| alert("Problem in restoring previous puzzle. Click on Do jigsaw.");} | 显示警告框 |
| } | 关闭restorepreviousjigsaw功能 |
| var choice = 0; | 持有正确答案的全局变量 |
| function setupfindstate(){ | setupfindstate功能的标题 |
| var i; | 用于索引 |
| var thingelem; | 元素引用 |
| stateelements[choice].style.border=""; | 移除最后选择的边框,如果有的话 |
| choice = Math.floor(Math.random()*nums); | 随机选择问题 |
| for (i=0;i<nums;i++) { | 遍历各州 |
| thingelem = stateelements[i]; | 设置对第 i 个元素的引用 |
| thingelem.addEventListener('click',pickstate,false); | 为此元素设置事件处理 |
| } | 关闭for回路 |
| var nameofstate = names[choice]; | 使用choice作为names数组的索引 |
| questionfel.question.value = "Click on "+nameofstate; | 设置提示 |
| questionfel.feedback.value = "  "; | 清除反馈 |
| questionfel.submitbut.value = ""; | 提交按钮不用于此任务 |
| } | 关闭setupfindstate功能 |
| function setupidentifystate(){ | setupidentifystate功能的标题 |
| stateelements[choice].style.border=""; | 删除以前的边框 |
| stateelements[choice].style.zIndex=""; | 把这个状态放在下一个选择的下面 |
| choice = Math.floor(Math.random()*nums); | 随机选择 |
| stateelements[choice].style.border="double"; | 在选择状态周围设置边框 |
| stateelements[choice].style.zIndex="20"; | 将此元素置于其他元素之上,这样边框将位于顶部 |
| questionfel.question.value = "Type name of state with border HERE"; | 设置提示,指示在哪里键入答案 |
| questionfel.submitbut.value = "Submit name"; | 为按钮设置标签 |
| questionfel.feedback.value = "  "; | 清除反馈字段 |
| var thingelem; | 用于保存对元素的引用 |
| for (i=0;i<nums;i++) { | 迭代状态 |
| thingelem = stateelements[i]; | 设置为第 i 个元素 |
| thingelem.removeEventListener('click',pickstate,false); | 移除事件处理 |
| } | 关闭for回路 |
| } | 关闭setupidentifystate功能 |
| function checkname() { | checkname功能的标题 |
| if (doingjigsaw) { | 如果玩家在玩拼图游戏。。。 |
| restore(); | 。。。调用恢复 |
| } | 结束子句 |
| else { | 否则 |
| var correctname = names[choice]; | 这是正确的名字 |
| var guessedname = document.questionform.question.value; | 这是玩家输入的内容 |
| if (guessedname==correctname) { | 玩家是正确的吗? |
| questionfel.feedback.value = "Correct!"; | 显示反馈 |
| } | 结束子句 |
| else { | 其他 |
| questionfel.feedback.value = "Try again."; | 显示反馈 |
| } | 结束子句 |
| return false; | 返回false防止刷新(可能不需要) |
| } | 结束如果不拼图条款 |
| } | 关闭checkname功能 |
| function checkpositions() { | checkpositions功能的标题 |
| var i; | 索引 |
| var x; | 对于 x |
| var y; | 对于 y |
| var tolerance = 20; | 定位允许的边距 |
| var deltax = []; | 将保持 x 差异 |
| var deltay = []; | 将保持 y 差异 |
| var delx; | 用于计算 |
| var dely; | 用于计算 |
| for (i=0;i<nums-2;i++) { | 迭代前 48 个州;不检查阿拉斯加或夏威夷 |
| x = stateelements[i].style.left; | x 是这个州的左边 |
| y = stateelements[i].style.top; | y 是这个州的榜首 |
| x = x.substr(0,x.length-2); | 移除px |
| y = y.substr(0,y.length-2); | 移除px |
| x = Number(x); | 转换为数字 |
| y = Number(y); | 转换为数字 |
| delx = x - statesx[i]; | 计算 x 偏移的差值 |
| dely = y - statesy[i]; | 计算与 y 轴偏移量的差值 |
| deltax.push(delx); | 添加到deltax数组 |
| deltay.push(dely); | 添加到deltay数组 |
| } | 关闭for回路 |
| var averagex = doaverage(deltax); | 计算所有 x 差异的平均值 |
| var averagey = doaverage(deltay); | 计算所有 y 差异的平均值 |
| for (i=0;i<nums;i++) { | 重复 |
| if ((Math.abs(averagex - deltax[i])>tolerance) &#124;&#124; (Math.abs(averagey-deltay[i])>tolerance)) { | 检查 x 差异或 y 差异是否大于各自平均值的公差 |
| break; | 如果是,退出循环 |
| } | 关闭条款 |
| } | 关闭for回路 |
| if (i<nums) { | 环路是否过早中断? |
| questionfel.feedback.value = names[i]+" and maybe more out of position"; | 设置反馈以显示被发现不在正确位置的状态 |
| } | 关闭条款 |
| else { | Else 循环没有过早结束;可以在这里检查夏威夷和阿拉斯加 |
| questionfel.feedback.value = "GOOD"; | 显示反馈 |
| } | 关闭条款 |
| } | 关闭checkpositions功能 |
| function doaverage(arr) { | doaverage功能的表头;参数是一个数组 |
| var sum; | 在计算中用作累加器 |
| var i; | 用于索引 |
| var n = arr.length; | 数组长度 |
| sum = 0; | 初始化为零 |
| for(i=0;i<n;i++) { | 迭代元素 |
| sum += arr[i]; | 添加第 i 个值 |
| } | 关闭for回路 |
| return (sum/n); | 返回除以数字 n 的总和 |
| } | 关闭doaverage功能 |
| function setupjigsaw() { | setupjigsaw功能的标题 |
| doingjigsaw = true; | 将标志设置为true |
| stateelements[choice].style.border=""; | 删除任何以前的边框 |
| var i; | 用于索引 |
| var x; | 对于 x 值 |
| var y; | 对于 y 值 |
| var thingelem; | 参考状态元素 |
| for (i=0;i<nums;i++) { | 迭代状态 |
| x = 100+Math.floor(Math.random()*600); | 为 x 选择随机值 |
| y = 100+Math.floor(Math.random()*320); | 为 y 选择随机值 |
| thingelem = stateelements[i]; | 设置第 i 个元素 |
| thingelem.style.top = String(y)+"px"; | 顶部位置 |
| thingelem.style.left =String(x)+"px"; | 左侧位置 |
| thingelem.removeEventListener('click',pickstate,false); | 移除事件处理 |
| } | 关闭for回路 |
| d.onmousedown = startdragging; | 设置事件处理 |
| d.onmousemove = moving; | 设置事件处理 |
| d.onmouseup = release; | 设置事件处理 |
| var df = document.createElement('div'); | 创建div |
| df.id = "fullpage"; | 给它身份证 |
| bodyel.appendChild(df); | 添加到正文 |
| questionfel.question.value = ""; | 清除问题字段 |
| questionfel.submitbut.value = "Save & close jigsaw"; | 更改提交按钮上的标签 |
| questionfel.feedback.value = "  "; | 清除反馈字段 |
| questionfel.style.zIndex = 100; | 将表单设置在顶部 |
| } | 关闭setupjigsaw功能 |
| var d = document; | 保留文档 |
| var ie= d.all; | Internet Explorer 检查;请注意,应用程序尚未检查最新的 Internet Explorer 版本 |
| var mouseDown = false; | 将标志初始化为false |
| var curX; | 当前 x |
| var curY; | 当前 |
| var adjustX; | 用于拖动 |
| var adjustY; | 用于拖动 |
| var movingobj; | 被拖动的对象 |
| function release(e){ | 释放功能的标题 |
| mouseDown = false; | 将标志设置回false |
| checkpositions(); | 对正在完成的谜题调用检查 |
| }; | 关闭release功能 |
| function startdragging(e) { | startdragging功能的标题 |
| var o; | 用于计算偏移量 |
| var j; | 用于保存对元素的引用 |
| var i; | 用于索引 |
| curX = ie ? e.clientX+d.body.scrollLeft : e.pageX; | 计算光标在 x 轴上的位置 |
| curY = ie ? e.clientY+d.body.scrollTop  : e.pageY; | 计算光标在 y 轴上的位置 |
| for (i=0; i<nums;i++) { | 迭代状态 |
| j = stateelements[i]; | 获取第 i 个元素 |
| o = offset(j); | 确定偏移量 |
| if (curX >= o.x && curX <= o.x + j.width && curY >= o.y && curY <= o.y + j.height) | 检查鼠标是否在第个元素上 |
| { break; } | 如果是,离开for循环 |
| } | 子句结束 |
| if (i<nums) { | for循环是否过早退出? |
| movingobj = stateelements[i]; | 将第 i th 设置为移动对象 |
| adjustX = curX- o.x; | 以 x 为单位的数量偏离鼠标光标 |
| adjustY = curY- o.y; | y 件中的数量偏离鼠标光标 |
| mouseDown = true; | 将标志设置为true:运动中的物体 |
| } | 鼠标悬停在对象上的 Close 子句 |
| } | 关闭startdragging功能 |
| function moving(e) { | moving功能的标题 |
| if (!mouseDown) return; | 如果没有移动任何对象,则返回 |
| if (ie) | 检查是否设置了ie标志 |
| draw(e.clientX+d.body.scrollLeft, e.clientY+d.body.scrollTop); | 使用这些值进行绘制 |
| else | 其他 |
| draw(e.pageX, e.pageY); | 使用这些值进行绘制 |
| } | 关闭moving功能 |
| function draw(x, y) { | draw功能的表头;这将移动/拖动状态 |
| var js = movingobj.style; | 提取指向样式的点 |
| js.left = (x - adjustX) + "px"; | 将样式更改为新的 x(左)值 |
| js.top  = (y - adjustY) + "px"; | 将样式更改为新的 y(顶部)值 |
| } | 关闭draw功能 |
| function offset(obj) { | offset功能的表头;添加obj从祖先开始的所有偏移 |
| var left = 0; | 向左初始化 |
| var top  = 0; | 初始化顶部 |
| if (obj.offsetParent) | 有家长吗? |
| do { | 然后 |
| left += obj.offsetLeft; | 向左增量 |
| top  += obj.offsetTop; | 增量顶部 |
| } while (obj = obj.offsetParent); | 有家长就继续走 |
| return {x: left, y: top}; | 返回数组的左值和上值 |
| } | 关闭offset功能 |
| </script> | 结束脚本标记 |
| </head> | 结束标题标签 |
| <body id="body" onLoad="init();"> | Body 标签,onLoad设置为init(); |
| <button onClick="spread();">Spread out states </button> | 展开状态的按钮 |
| <button onClick="restore();">Restore original /compress map </button> | 恢复原始地图的按钮 |
| <button onClick="setupfindstate();">Find the state </button> | 开始Find the state任务的按钮 |
| <button onClick="setupidentifystate();">Name the state</button> | 开始Name the state任务的按钮 |
| <button onClick="setupjigsaw();">Do jigsaw</button> | 开始拼图的按钮 |
| <button onClick="restorepreviousjigsaw();">Restore last jigsaw in process </button> | 按钮来恢复保存的拼图 |
| <h1>USA</h1> | 前往屏幕上的美国拼图 |
| <form id="questionform" name="questionform" onSubmit="return checkname();"> | form标签,用onSubmit设置为checkname调用 |
| State name: <input type="text" name="question" value="   " size="40"/> | 州名的标签和位置 |
| <input name="submitbut" type="submit" value="       " size="30"/> | 提交按钮;值现在为空 |
| Feedback: <input type="text" name="feedback" value="   " size="40" /> | 反馈的标签和位置 |
| </form> | 结束form标签 |
| </body> | 结束body标签 |
| </html> | 结束html标签 |

测试和上传应用程序

这个项目可以使用 Chrome 和 Firefox 进行本地测试(在你的家用电脑上),尽管在某一点上,正如我提到的,这是不正确的。这个应用程序需要 50 个代表州的文件,所以一定要上传它们(或者任何与应用程序的地图部分相对应的文件)。

摘要

在这一章中,你学习了如何为玩家构建一个以不同类型的问题为特色的教育游戏。HTML5 的特性和编程技术包括以下内容:

  • 构建包含文本或视觉提示的用户界面。玩家的反应包括点击屏幕上的元素和输入文本。进入拼图模式后,玩家的动作是拖动和重新定位屏幕上的元素。

  • 使用拆分和连接方法对信息进行编码和解码。

  • 保存和恢复正在进行的工作,包括使用try...catch构造。

  • 重用上一章解释的技术:

    • 动态创建 HTML 标记以在屏幕上创建片段元素

    • 将拼图块随机放置在屏幕上

    • 确定指示拼块如何装配在一起的坐标值,并使用这些值以及定义的公差来检查拼图是否正确装配在一起

    • 操纵片段元素的位置以展开片段并将其恢复到原始位置

在第十章,也就是最后一章,我们探索了准备一个在不同设备上工作的 web 文档的要求,这被称为响应设计,以及使应用程序更广泛地可访问的初始步骤

十、响应式设计和可访问性

在本章中,您将:

  • 学习使您的交互式应用程序可在各种设备上使用的技术

  • 了解如何让人们只使用键盘和屏幕阅读器就能访问您的应用程序

  • 进行随机的多项选择,此外,将这些任务组合起来

  • 查看动态创建 HTML 标记的其他示例

介绍

过去,人们使用台式机或笔记本电脑来使用计算机应用程序!现在,许多人希望在他们的平板电脑或智能手机上查看和使用计算机应用程序,包括网页。此外,许多人希望在所有三类设备上访问一个网站,并有类似的体验。他们还可以选择修改台式机或笔记本电脑上的窗口尺寸,或者改变移动设备的方向。准备一个使用 HTML、CSS 和 JavaScript 制作的项目以适应设备(和设备的状态)被称为响应式设计。另一个不同但相似的目标是准备一个可供各种用户访问的项目。在这种情况下,一个关键的挑战是使应用程序适合使用屏幕阅读器的有视觉障碍的人和/或只能使用键盘的人。在这一章中,我描述了一些有助于实现这些目标的技术,重点是一些具体的例子。

图 10-1 显示了一个调整到设备尺寸的 HTML 和 JavaScript 项目的截图。该程序通过在台式机或笔记本电脑上用鼠标或在平板电脑或手机上触摸来循环播放一系列图像。

img/272384_2_En_10_Fig1_HTML.jpg

图 10-1

显示程序的打开屏幕

按下鼠标按钮并向下移动或用手指向下触摸并向下移动会使下一张图片逐渐显示,直到接近底部。图 10-2 为正在更改中的图片。

img/272384_2_En_10_Fig2_HTML.jpg

图 10-2

从一张图片到下一张图片的变化正在进行中

当鼠标或手指足够近时,整个下一张图片就会出现。类似地,用户/玩家可以在屏幕上向上移动鼠标或手指,并获得先前出现的图片。我鼓励读者尝试源代码。

图 10-3 显示了一个竞猜游戏的截图,可以用鼠标或触摸或键盘单独操作。这一组四个国家是从 G20 国家中随机选择的,相应的首都是混在一起的,所以每次您都会看到一组不同的项目。该测验可以仅使用键盘和屏幕阅读器程序进行,tab 键将玩家从一个项目带到另一个项目,使其适合视觉能力有限或没有视觉能力的人和/或不能使用鼠标或触摸的人。

img/272384_2_En_10_Fig3_HTML.jpg

图 10-3

国家/首都测验的开始屏幕

“动作”和“分数”字段显示了到目前为止的表现。这些框会改变颜色,并移动到匹配框的旁边,让视觉能力强的人觉得更有趣。当匹配正确时,使用黄色/金色。如说明所示,当玩家正确匹配四次时,将播放一段视频。图 10-4 为截图。

img/272384_2_En_10_Fig4_HTML.jpg

图 10-4

成功完成测验的屏幕截图

视频有声音,因此视障人士也能获得奖励。

注意

屏幕阅读器很复杂,并提供自定义使用的选项。我使用的一个屏幕阅读器单独“说出”国家和首都的名称,但另一个添加了术语“组”,这很烦人。可以使用 Tab 键和 Shift 键来来回听国家和首都的名称。然而,屏幕阅读器会阅读整个屏幕,包括我的浏览器工具栏上的所有内容,当点击文档末尾的 Tab 键时,这种情况会重复出现。我的例子展示了如何在动态生成的 HTML 标记中包含标签信息。我强烈建议您在继续探索屏幕阅读器和键盘操作的使用时,先研究静态 HTML 页面,然后再研究具有动态生成的 HTML 元素的程序。

关键要求

在深入研究具体的技术特性之前,开发人员需要考虑计划中的应用程序的最重要的目标受众以及应用程序在不同情况下的可行性,这一点非常重要。有一个叫做 mobile first 的概念,它建议,如果要在移动设备上使用某些东西,最好的方法是首先设计和规划移动实现,而不是为桌面设计和实现,然后进行调整。从问题规范开始,包括最常见的设备和用户,并制定解决方案是一个好策略。教师和书籍作者经常做一些完全不同的事情:从我们想要解释的概念和功能开始,设计我们认为使用这些功能的有趣程序。

当你设计一个网络应用程序时,考虑某些程序,比如那些具有地理定位功能的程序,最适合移动设备是很重要的。相比之下,需要大量文本输入的程序最适合台式机和笔记本电脑。拼图玩具不适合视力有障碍的人。不过国家/首都的小测验,我本来是做给鼠标或者触控用的,可以改编成键盘操作。考虑不同的屏幕和不同的受众是确定什么对你的应用程序至关重要的一种有价值的方法,并且致力于响应性设计和提高可访问性的过程可以使所有的受众受益。

在这一章中,我将重点介绍如何适应屏幕尺寸,确保除了鼠标之外或代替鼠标使用触摸功能,以及支持屏幕阅读器和至少某些应用程序的纯键盘操作。我还想让用户调整大小,并再次调整到任意宽度和高度的窗口。

我将简要地提到对各种网站有用的特性,通常有一个静态的设计。

屏幕大小和尺寸

您可能希望看到按名称检查特定设备或设备类型的代码,但在许多情况下,这不是推荐的方法。相反,如果要检查的关键属性包括屏幕宽度和屏幕高度,则直接检查这些尺寸。在 HTML 元素属性、CSS 规则和指令以及 JavaScript 代码中有多种方法可以做到这一点。在 HTML、CSS 和 JavaScript 特性一节中,您可以学习,或者至少被介绍了许多细节。

触控

移动设备通常没有鼠标,而是依靠触摸。将触摸解释为鼠标点击是“免费的”,也就是说,不需要额外的编码,将在测验示例中演示。Reveal 应用程序基于按下鼠标、移动鼠标和抬起鼠标的操作序列,它需要 JavaScript 代码来支持触摸。该技术是设置触摸事件来模拟适当的鼠标事件。

屏幕阅读器和选项卡

存在各种各样的屏幕阅读器工具。我使用运行 MacOS High Sierra 的 iMac 上的内置 VoiceOver 功能来测试这个测试程序。有视觉障碍的人和不能操作鼠标的人需要通过键盘完成所有的事情。这包括提供支持使用 Tab 键的编码。对屏幕阅读器和键盘的最佳支持的一般建议是良好的整体组织,将文本分成更小的部分,并为用户看不到的部分提供标签。

HTML、CSS 和 JavaScript 特性

HTML 和 CSS 一起提供了支持响应式设计和可访问性的方法。在涉及更多交互和动态行为的情况下,可能有必要使用 JavaScript,我将重点讨论示例中的 JavaScript 技术。

当标记

标签为浏览器、搜索引擎和其他网络程序提供关于文档的信息。没有显示任何内容。charset meta标签

<meta charset="UTF-8">

指定要使用的字符集。UTF-8 指定是默认的,表示 1 到 4 字节的 Unicode 标准。Unicode 的目的是支持世界上所有的语言,尽管可能不完全是这样,但大多数语言,包括日语和中文,都是受支持的。尽管 Unicode 是默认设置,但如果没有这个meta标记,web 控制台上仍会显示一条警告消息,因此包含它将防止您在访问 web 控制台时看到该消息。

建议使用以下meta标签将宽度设置为器件宽度:

<meta name="viewport" content="width=device-width, initial-scale=1.0">

我在 Reveal 示例中使用了以下代码,允许用户在移动设备上缩放窗口。这仅适用于移动设备。

<meta name="viewport"
content="width=device-width, user-scalable=yes, initial-scale=1.0, minimum-scale=1.0, maximum-scale=2.0" />

如果图像或视频元素没有给定宽度或高度属性设置或给定固定量,使窗口变小将导致滚动。垂直滚动被认为是可以接受的,但水平滚动是不能接受的。下一节将描述产生预期效果的技术。

HTML 和 CSS 使用百分比和自动

以像素为单位指定img等元素的宽度和高度是一种标准做法。如果只使用 HTML 为body元素中的元素指定了一个,则修改另一个以保持纵横比。在style部分,使用 CSS,可以使用术语auto。这是默认设置,但我喜欢明确地提到它,主要是作为对我的提醒。

以像素为单位指定宽度或高度的一种变化是将维度指定为包含元素的百分比。包含元素可以是body元素或div或语义标签或其他东西。块显示元素的默认宽度尺寸,如div,是屏幕的 100%。可以指定另一个百分比,比如 50%或 80%。一个例子是在正文中包含

<img id="animal" width="50%" src="monkey.jpg">

这将设置图像的宽度占屏幕的 50%,高度保持纵横比。在“样式”部分,以下任一项

#animal {width:50%; height: auto;}
#animal {width:50%;}

会产生同样的效果。

如果应用程序窗口被操作(比如说,在桌面上)为比计算的高度短,那么图像将被切掉,并且将出现滚动条用于垂直滚动。如果想要的效果是在宽度(或高度)上设置一个界限,但不将图像拉伸到超出其原始尺寸,可以使用max-widthmax-height属性。一般来说,垂直滚动比水平滚动更容易被接受,所以通常只需指定宽度或max-width就可以获得响应。这是我在国家/首都测验中制作奖励视频时使用的方法。

百分比可与宽度和/或max-width一起使用,为元素设置网格布局。我鼓励你尝试这些特性。许多 W3Cschool 示例的 TRY-IT 特性很有帮助。

您将看到我如何使用 JavaScript 修改宽度和高度。

CSS @media

Web 开发人员可以在style元素中设置@media查询。这些提供了一种方法来检查设备的属性,并为某些条件指定样式指令。例如,在我的购物网站上,我指定某些元素属于一类,名为col。如果屏幕足够宽,我希望这些元素以列的形式分布在窗口中。然而,如果屏幕宽度很小,我不想要求水平滚动,而是让col元素垂直显示,假设用户将垂直滚动。下面的@media指令产生了这种效果:

@media all and (max-width: 640px)
   {.col {display: block; width: 100%;}}

如前所述,这是为测试手机等狭窄设备而建议的技术。@media特性也可以用于为计算机和设备的屏幕指定不同的格式,术语print用于打印网页,术语speech用于屏幕阅读器。

一个@media查询可以有修饰符notonly。举个例子,

@media only screen and (max-width: 600px) {
    body {
        background-color: lightblue;
    }
}

为在屏幕上使用的 body 元素中的所有内容生成浅蓝色的背景色(这是已知的颜色名称之一(见 https://www.w3schools.com/Colors/colors_names.asp )。更多的例子和解释请参考 https://www.w3schools.com/CSSref/css3_pr_mediaquery.asp

HTML alt 属性和语义元素

元素的属性为屏幕阅读器提供信息。如果文件丢失或下载缓慢,将显示alt属性的值。在正常情况下建议使用alt元素,检查可访问性的程序将指示任何没有alt元素的img标签。思考什么是alt属性可能是开发网页的一个重要练习。请注意,我在这个例子中的代码没有显示img元素,这只是用来确保图片被完全下载。因此,我认为包含alt属性并不合适。

语义元素可以提供屏幕阅读器可以使用的信息。当在大型项目中与其他人一起工作时,术语headerfootermainsectionarticle等是有意义的。它们没有必须提供的特定格式。

HTML tabIndex

依赖屏幕阅读器或者不能或不习惯使用鼠标或触摸的人依赖于使用 Tab 键来浏览文档。可以为任何元素设置tabIndex属性。按 tab 键将用户带到 Tab 键顺序中的下一个元素(按数字顺序从低到高进行)。按 Tab 键和 Shift 键可以反转方向。元素的tabindex可以在准备 HTML 文档时设置,也可以在动态创建 HTML 标记时通过编码产生。在 quizTab 应用程序中,我包含了以下语句

d.innerHTML = (
"<div tabIndex='"+String(2+i)+"' class="thing" id='"+uniqueid+"'>placeholder</div>");

该代码为国家名称的tabindex产生连续的值。

在 web 页面的操作过程中,可以更改tabindex,尽管在我的例子中我没有这样做。玩(参加)测验确实意味着多次浏览项目,这确实意味着再次听到指示,并且再次进入浏览器的地址栏,并且在某些情况下,听到浏览器中选项卡所代表的所有活动站点。

宽度和高度属性的 JavaScript 用法

任何计算机或设备的浏览器都会调整文本的线宽以适应窗口。然而,我也想调整说明中的字体大小。指令使用我的代码根据计算选择的字体大小显示。使用以下语句设置fontsz数组:

var fontsz = ["14px","16px","18px","20px","24px"];

字体的大小在init函数中使用已经分配了代码为window.innerWidth.cwidth变量进行设置

  fs = Math.floor (cwidth/200);
  fs = Math.min(fs,4);
  bodyel.style.fontSize = fontsz[fs];

在 Reveal 示例中,我给自己设定的挑战是图像要适合窗口,不需要任何滚动,同时保持比例。我使用的属性包括窗口的window.innerWidthwindow.innerHeight,图像的widthnaturalWidthheightnaturalHeight。“自然”属性代表图像的原始尺寸。它们不能被改变。对于 Reveal 示例,我已经确保所有的图像都有相同的尺寸,所以我只需要做一组计算。代码检查宽度是否小于屏幕宽度并调整高度,然后确保高度小于屏幕高度并调整宽度。您可以回到第八章,了解在drawImage方法中使用的计算值的变化。

动态创建元素

Reveal 示例中的图像序列是通过将每个图像绘制到 canvas 元素中来动态实现的。对于任何涉及网络上的图像或其他媒体的作品,确保文件下载完成是至关重要的。我通过在主体中包含img元素,但在样式部分将可见性设置为隐藏来实现这一点。然后,我的代码调用一个名为init的函数来完成创建 canvas 元素的所有工作,将每个图像绘制到它的 canvas 元素中,并将第一个图像绘制到主体中设置的 canvas 中。

国家/首都测验也动态创建元素。这些是长方形,上面有国家和首都的名称。HTML 标记是用为idclasstabindex设置的属性创建的。id值将索引保存在facts数组中,用于确定玩家是否正确匹配了国家和首都。

从列表中选择

测验示例随机选择两种情况。如何做出一个随机的选择是很简单的。然而,对于这个程序,我需要从 20 个国家的事实数组中随机选择四个国家/首都对,但不允许重复。然后,对于每个国家和首都对,由于我不希望每个首都的名称与其国家名称相对,所以我需要代码从第二列中代表位置的四个位置中随机选择每个首都的位置。这也需要做到不重复。注意:可能会有这样的情况,一个首都确实结束于其国家的对面,但大多数时候不会发生。参见图 10-3 中墨西哥城、安卡拉、华盛顿特区和布宜诺斯艾利斯的位置。

我解决这个问题的第一步是让facts数组保存一些东西来告诉我是否有一个事实被接受了。facts数组是一个数组的数组,内部数组有三个元素:country、capital 和 true/false。false 设置意味着事实没有被选择,true 意味着它已经被选择。slots数组将保存四个首都名称的索引。我将使用-100 的初始设置来表示一个槽没有被占用。它实际上可以是任何小于零的数。当选择一个槽时,slots数组中的相应值被设置为facts数组中国家/首都的索引值。请注意,我可以在这里使用任何非负数,因为我(我的代码)不使用该值,但我在考虑未来可能的应用。

我在这两种情况下使用的编码结构是一个do/while循环。do/while结构可以在很多情况下使用,所以请记住它。用一般术语来描述:括号中的代码至少被调用一次。然后评估术语while后面括号中的条件。如果为真,则再次执行括号中的代码。括号中可以有多个语句。一种伪代码的思考方式是

do { one or more statements }
   while (repeat if this condition is true )

选择facts的检查由以下代码完成:

do {c = Math.floor(Math.random()*facts.length);}
             while (facts[c][2]==true)

如果已经选择了子数组facts[c]中表示的事实,将重复对变量c的赋值。

这段代码完成了对选择插槽的类似检查。

do {s = Math.floor(Math.random()*nq);}
             while (slots[s]>=0)

表示该位置已被占用的值由大于或等于零的slots[s]表示,因此如果该位置已被占用,将重复随机选择。

鼠标事件、触摸事件和按键事件

响应式设计有两个主要问题需要解决。我已经描述了检查和修改元素以适应窗口大小。第二个考虑是提供触摸而不是鼠标事件。触摸事件的处理通过模拟鼠标事件来完成。鼠标事件大概已经被定义了。正如我已经提到的,某些触摸事件的处理不需要任何额外的编程。这些都是简单的事件,比如点击一个元素。但是,mousedownmousemovemouseup等事件需要翻译。这是因为计算需要鼠标或触摸的准确位置来从源画布绘制到显示的画布。

init函数中,addEventListener方法涉及五个事件。如果这段代码在没有这些事件的设备上执行,那么引用任何不可能发生的事件都没有问题。

  canvas.addEventListener("mousedown",startreveal,true);
  canvas.addEventListener("touchstart", touchHandler, true);
  canvas.addEventListener("touchmove", touchHandler, true);
  canvas.addEventListener("touchend", touchHandler, true);
  canvas.addEventListener("touchcancel", touchHandler, true);

touchHandler函数执行的任务是确定要模拟哪个鼠标事件(使用 switch 语句),创建事件(使用 new MouseEvent),然后调度它。MouseEvent函数使用一个关联数组(也称为字典),其中设置了某些属性。对于这个例子,我让其他属性采用默认值。

function touchHandler(event)
{
  var touches = event.changedTouches;
  if (touches.length>1) {
    return false;
  }
  var first = touches[0];
  var type = "";
  switch(event.type)
    {
        case "touchstart": type = "mousedown"; break;
        case "touchmove":  type="mousemove"; break;
        case "touchend":   type="mouseup"; break;
        default: return;
    }
   var simulatedEvent = new MouseEvent(type,{
           screenX: first.screenX,
           screenY: first.screenY,
           clientX: first.clientX,
           clientY: first.clientY
  });

    first.target.dispatchEvent(simulatedEvent);
    event.preventDefault();
}

注意

构造函数MouseEvent相对较新,取代了document.createEvent("MouseEvent")的使用,后者现在被标记为不推荐使用,这意味着不鼓励使用它,将来可能不会被识别。工具的变化是我们需要接受的。事实上,与旧方法相比,新方法有一个显著的优势:对参数使用了一个关联数组,而不是由位置指示的一长串参数,其中大多数参数采用默认值。

请注意,如果有多点触摸手势,则不会发生任何事情,同时请注意,任何默认操作都会被阻止。我知道至少有一个商业纸牌游戏——一个 iPad 应用程序——没有做到这一点,所以当移动一张牌时,整个棋盘可能会移动。

与鼠标和触摸的问题无关:如果 Reveal 程序中的用户用鼠标或手指在图像的右边或下面(屏幕的更下方)按下,什么都不会发生。通过在startreveal函数中使用以下代码,可以忽略这种不良行为:

var startxy = getCoords(ev);
   if (startxy[0]>pwidth) return;
   if (staryxy[1]>pheight) return;

类似地,如果玩家在测验应用程序中点击两个国家名或两个大写字母,程序不会提供特殊的反馈,但会将第二个项目放在第一个项目的旁边。它不会被视为正确答案,因为两个id值不匹配。

在构建自己的应用程序时,您必须决定在我们称之为不良行为的情况下提供什么反馈(如果有的话)。

对于测验示例,触摸(点击)事件的事件处理被解释为手机和平板设备的鼠标点击。然而,我给自己设定了支持键盘操作的挑战。我所做的是设置keyup事件来调用我的pickelement函数,但是在那个函数中,如果 keycode 是 9,即 Tab 的 keycode,就返回。因此,使用键盘的玩家可以切换到每个国家和首都项目,听到屏幕阅读器说出名称,然后按 Return 键选择一个项目,或者切换到下一个项目。

构建 Reveal 应用程序并使其成为您自己的应用程序

揭示程序从以下事件和动作序列开始。

  1. 当文档被完全加载时,包括图像,调用init函数。在玩家重新加载和调整大小之后,调用init函数。请注意,由于 style 元素中的指令,这些图像是不可见的。

  2. init函数决定窗口的尺寸,并使用该信息选择字体的大小。

  3. init函数调用setupimages函数。

  4. setupimages函数进行计算以确保图像适合窗口,保持纵横比。它为每个图像创建一个画布元素。

  5. 返回到init功能,设置mouseDown和所有触摸事件。第一个图像被绘制到 canvas 元素中,从画布绘制到画布。设置nextprev变量。

显示下一张图片的动作由函数startrevealrevealingstopreveal处理,被叫和主叫关系如表 10-1 所示。我决定允许用户上下滑动并改变方向。我还决定,如果垂直水平在顶部或底部的蒙混因素之内,就完成过渡。我的意图是通过程序中嵌套的if/else语句实现的。

表 10-1

揭示的功能关系

|

功能

|

由...调用

|

祈求者

|
| --- | --- | --- |
| init | body标签中的onloadonresize属性 | setupimages |
| setupimages | init |   |
| touchHandler | initaddEventListener的调用 |   |
| getCoords | startreveal, revealing |   |
| startreveal | 在initstopreveal调用addEventListener | getCoords |
| revealing | startrevealaddEventListener的调用 | getCoords |
| stopreveal | startrevealaddEventListener的调用和揭示中的直接调用 |   |

表 10-2 显示了 reveal 程序的注释代码。

表 10-2

显示程序代码

|

密码

|

描述

|
| --- | --- |
| <!DOCTYPE HTML> | 页眉 |
| <html> | html标签 |
| <head> | head标签 |
| <title>Reveal next</title> | 完整的标题元素 |
| <meta name="viewport" | 视口起点 |
| content="width=device-width, user-scalable=yes,``initial-scale=1.0, minimum-scale=1.0,``maximum-scale=2.0" /> | 指示用户更改的处理方式 |
| <meta charset="UTF-8"> | 将字符集指定为 unicode |
| <style> | 样式标签 |
| body { | 身体指令 |
| font-family: Garamond, serif; | 字体是 Garamond,如果可用,否则衬线 |
| font-size: 24px; | 笔记大小可能会改变 |
| overflow: hidden; | 如果太大,没有溢出和滚动 |
| } | 关闭几何体指令 |
| div#images {display:none;} | 不显示任何图像;用于绘制到画布元素中的图像 |
| </style> | 关闭style元素 |
| <script> | script元素的开始 |
| var ctx; | 将保存画布元素的上下文 |
| var fudge = 40; | 不需要用户一直向上或向下才能看到下一幅图像;这是一个“忽悠因素”,意思是给用户一点空间 |
| var canvas; | 将容纳画布 |
| var pwidth; | 图片的宽度 |
| var pheight; | 图片的高度 |
| var cwidth; | 窗口宽度 |
| var cheight; | 窗口高度 |
| var current = 0; | 从第 0 张到第张图片开始 |
| var prev = 3; | 从索引 3 处的前一张图片开始 |
| var next = 1; | 下一张图片在索引 1 处 |
| var rect; | 用于获取鼠标坐标 |
| var revealflag = false; | 旗 |
| var lastdrawn; | 跟踪最后一张图片 |
| var lasty; | 最后一个 y 值 |
| var moving = false; | 设置以指示移动鼠标 |
| var canvases = []; | 会为所有的图片准备画布 |
| var fontsz = ["14px","16px","18px","20px","24px"]; | 可能的字体大小 |
| function init() { | init功能的标题 |
| var fs; | 用于确定字体大小 |
| canvas=document.getElementById("canvas"); | 指向画布元素的指针 |
| bodyel = document.getElementById("body"); | 指向主体元素的指针 |
| ctx = canvas.getContext("2d"); | 画布的上下文 |
| ctx.font = "24px serif"; | 默认字体和大小 |
| cwidth = window.innerWidth; | 窗口宽度 |
| cheight = window.innerHeight; | 窗口高度 |
| fs = Math.floor (cwidth/200); | 计算字体大小 |
| fs = Math.min(fs,4); | 最小值为 4 |
| bodyel.style.fontSize = fontsz[fs]; | 将正文中指令的字体大小设置为fontsz数组的fs元素 |
| canvas.width = cwidth; | 设置画布宽度 |
| canvas.height= cheight; | 设置画布高度 |
| rect = canvas.getBoundingClientRect(); | 用于确定鼠标坐标 |
| var noOfImgs = document.getElementsByTagName('img').length; | 确定文档中img元素的数量 |
| setupimages("noodles", noOfImgs); | 执行任何缩放后,调用将设置图像(图片)画布的函数 |
| canvas.addEventListener("mousedown",``startreveal,true); | 设置mousedown事件 |
| canvas.addEventListener("touchstart",``touchHandler, true); | 所有触摸事件都会调用touchHandler |
| canvas.addEventListener("touchmove",``touchHandler, true); |   |
| canvas.addEventListener("touchend",``touchHandler, true); |   |
| canvas.addEventListener("touchcancel",``touchHandler, true); |   |
| ctx.drawImage(canvases[0],0,0); | 将第一张(第 0 张到第 1 张索引)图片绘制到画布上 |
| current = 0; | 设置电流;这条语句和接下来的两条语句是onresize调用init所必需的 |
| prev = 3; | 设置prev |
| next = 1; | 设置next |
| } | 关闭init |
| function setupimages (base, lim){ | setupimages功能的表头;基数表示图像名称的开头,而lim表示图像的数量 |
| var dref; | 对第一幅图像的引用;这个用来计算比例因子 |
| var can; | 会指向每一张画布 |
| var canctx; | 将保存每个画布的上下文 |
| canvases = []; | 将保存创建的画布和图像,所有图像都缩放到适当的大小;你可以把它们想象成保存图像的缓冲器 |
| var img; | 将依次按住每个img |
| dref = document.getElementById("dummy"); | 获取对第一幅图像的引用 |
| if (dref.naturalWidth) { | 这提供了图像的原始宽度 |
| dref.width = dref.naturalWidth; | 将宽度设置为该值 |
| pratio = dref.naturalHeight/dref.naturalWidth; | 计算纵横比 |
| } | 关闭if |
| else { | 当naturalWidth不存在时不太通用,这可能是某些浏览器的情况 |
| pratio = dref.height/dref.width; | 计算比率 |
| } | 关闭 else |
| dref.width = Math.min(dref.width,cwidth-fudge); | 现在可能重置宽度 |
| dref.height = pratio * dref.width; | 设置高度以匹配可能修改的宽度 |
| dref.height = Math.min(dref.height,cheight-fudge); | 现在可能修改高度 |
| dref.width = dref.height * (1/pratio); | 设置宽度以匹配可能修改的高度 |
| pwidth = dref.width; | 设置变量以备后用 |
| pheight = dref.height; | 设置变量以备后用 |
| for(var i=1;i<=lim;i++){ | 通过将 1、2、3、4 加到基数上,按名称引用图像文件;这个循环缩放图像(重复第一个循环的操作);还要注意画布数组中的项目位于索引位置 0,1,2,3 |
| img = new Image(); | 创建一个Image对象 |
| img.width = pwidth; | 设置宽度 |
| img.height = pheight; | 设置高度 |
| img.src=base+String(i)+".jpg"; | 使用基数和数字设置src |
| can = document.createElement("canvas"); | 创建画布 |
| can.width = cwidth; | 设置其宽度 |
| can.height = cheight; | …和高度 |
| canctx = can.getContext('2d'); | 将canctx设置为上下文 |
| canctx.drawImage(img,0,0,pwidth,pheight); | 将图像绘制到画布上 |
| canvases.push(can); | 推入canvases数组 |
| } | 关闭for回路 |
| } | 关闭setupimages功能 |
| function touchHandler(event) { | touchHandler的标题 |
| var touches = event.changedTouches; | 获取触摸数组 |
| if (touches.length>1) { | 如果不止一个 |
| return false; | 返回;不要为多点触控手势做任何事情 |
| } | 关闭if |
| var first = touches[0]; | 进行第一次(也是唯一一次)接触 |
| var type = ""; | 初调 |
| switch(event.type)    { | 开启事件类型 |
| case "touchstart": type = "mousedown"; break; | 将类型变量设置为相应的鼠标事件 |
| case "touchmove":  type="mousemove"; break; |   |
| case "touchend":   type="mouseup"; break; |   |
| default: return; | 什么也不做 |
| } | 关闭开关 |
| var simulatedEvent = new MouseEvent(type,{``screenX: first.screenX,``screenY: first.screenY,``clientX: first.clientX,``clientY: first.clientY``}); | 创建计算类型的MouseEvent;从触摸事件的位置设置位置 |
| first.target.dispatchEvent(simulatedEvent); | 调度事件将被视为实际事件 |
| event.preventDefault(); | 停止对触摸事件的任何默认响应 |
| } | 关闭touchHandler |
| function getCoords(ev){ | 用于getCoords拾取鼠标位置的标题 |
| var mx; | 将保持水平 |
| var my; | 将保持垂直 |
| mx = ev.clientX-rect.left; | rect变量已经设置;计算mx |
| my = ev.clientY-rect.top; | 计算my |
| return [mx,my]; | 返回一个数组 |
| } | 关闭getCoords |
| function startreveal(ev){ | startreveal的标题 |
| var startxy = getCoords(ev); | 获取鼠标的坐标 |
| if (startxy[0]>pwidth) return; | 如果鼠标或触摸在图像的右边,则没有动作 |
| if (startxy[1]>pheight) return; | 如果鼠标或触摸位于图像下方,则无动作 |
| lasty = Math.max(startxy[1],fudge); | 从顶部开始至少要有一段距离 |
| canvas.addEventListener("mousemove",``revealing,true); | 设置mousemove |
| canvas.addEventListener("mouseup",``stopreveal,true); | 设置mouseup |
| canvas.removeEventListener("mousedown",``startreveal,true); | 停止监听mousedown |
| revealflag = true; | 设置标签 |
| } | 关闭startreveal |
| function revealing(ev){ | 显示标题 |
| var slice; | 将指示从下一张图片中提取多少(垂直量) |
| var curxy; | 将保持鼠标位置 |
| if (!revealflag) return; | 如果不在显示阶段,返回 |
| curxy = getCoords(ev); | 获取鼠标坐标 |
| cury = curxy[1]; | 设置cury |
| if (moving){ | 检查移动标志 |
| if (cury>=lasty){ | 如果比上次更低 |
| if (cury<(pheight-fudge)){ | 如果它不在底部 |
| slice = Math.max(1,cury-lasty) | 计算高度 |
| ctx.drawImage(canvases[next],0,lasty,pwidth,``slice,0,lasty,pwidth,slice); | 从下一张画布开始绘制 |
| lastdrawn = next; | 为下一步行动做准备 |
| lasty = cury; |   |
| } | 关闭if |
| else { | 马上去准备下一个 |
| lastdrawn = next; | 设置lastdrawn |
| stopreveal(ev); | ev通过与被通过eventhandler |
| } | 为cury<(pheight-fudge)关闭 else |
| } | 关闭cury>=lasty |
| else { | Else cury< lasty,如此向上移动 |
| if (cury>fudge){ | 如果仍然在软糖区之外 |
| slice = Math.max(1,lasty-cury); | 计算切片 |
| ctx.drawImage(canvases[prev],0,cury,pwidth,``slice,0,cury,pwidth,slice); | 从prev图像绘制 |
| lastdrawn = prev; | 将lastdrawn设置为prev |
| lasty = cury; | 设置lasty |
| } | 关闭if |
| else { | 否则在软糖区,所以完成过渡 |
| lastdrawn = prev; | 设置lastdrawn |
| stopreveal(ev); | 调用stopreveal |
| } | 为cury>fudge关闭 else |
| } | 为cury>=lasty关闭 else |
| } | 移动时关闭 |
| else { | 第一部电影 |
| moving = true; | 设置moving |
| if (cury>=lasty){ | 检查方向 |
| prev = current; | 向上移动,因此设置 prev |
| if (cury<(pheight-fudge)){ | 检查上面是否有软糖 |
| slice = Math.max(1,cury-lasty); | 设置切片 |
| ctx.drawImage(canvases[next],0,lasty,pwidth,``slice,0,lasty,pwidth,slice); | 从下一个开始绘制 |
| lastdrawn = next; | 设置lastdrawn |
| lasty = cury; | 设置lasty |
| } | 如果高于软糖,则关闭 |
| else { | 其他 |
| lastdrawn = next; | 设置lastdrawn |
| stopreveal(ev); | 立即转到stopreveal;通过的ev与成为的eventhandler一致 |
| } | 关闭cury<(pheight-fudge)else |
| } | 关闭cury>=lasty移动false |
| else { | 向上移动图像 |
| next = current; | 设置下一个 |
| if (cury>fudge){ | 如果大于软糖 |
| slice = Math.max(1,lasty-cury); | 计算切片 |
| ctx.drawImage(canvases[prev],0,cury,pwidth,``slice,0,cury,pwidth,slice); | 绘制切片 |
| lastdrawn= prev; | 设置lastdrawn |
| lasty = cury; | 设置lasty |
| } | 如果高于软糖,则关闭 |
| else { | 否则(在软糖区) |
| lastdrawn = prev; | 设置 lastdrawn |
| stopreveal(ev); | 立即转到stopreveal |
| } | 关闭 else for if cury>fudge |
| } | 为cury>=lasty关闭 else |
| } | 如果移动,则关闭 else |
| } | 关闭功能 |
| function stopreveal(ev) { | stopreveal的标题 |
| revealflag = false; | 重置〔??〕 |
| moving = false; | 重置移动 |
| ctx.drawImage(canvases[lastdrawn],0,0); | 绘制完整的图像 |
| current = lastdrawn; | 设置电流 |
| next = current+1; | 下一个增量 |
| if (next==canvases.length) next = 0; | 如果在最后,将旁边设置为 0 |
| prev = current-1; | 预测集 |
| if (prev<0) prev=canvases.length-1; | 如果 prev 太低,设置为最后一个指数 |
| canvas.removeEventListener("mousemove",``revealing,true); | 停止监听mousemove |
| canvas.removeEventListener("mouseup",``stopreveal,true); | 停止监听mouseup |
| canvas.addEventListener("mousedown",``startreveal,true); | 为startreveal设置事件 |
| } | 关闭stopreveal |
| </script> | 结束script元素 |
| <body id="body" onload="init();"``onresize="init();"> | Body 标签,带有onloadonresize的设置 |
| Mouse/touch down,``slowly drag mouse/finger down or up the photo,``then mouse/touch up. | 说明 |
| <canvas id="canvas" width="100%"``height="50%" > | 画布;可以缩放 |
| Your browser doesn't support canvas | 标准消息 |
| </canvas> | 画布结束标签 |
| <div id="images"> | div拿着图像 |
| <img src="noodles1.jpg" id="dummy"/> | 第一个用于比例计算 |
| <img src="noodles2.jpg"> |   |
| <img src="noodles3.jpg"> |   |
| <img src="noodles4.jpg"> |   |
| </div> | 关闭div |
| </body> | 关闭body |
| </html> | 关闭html |

测试和上传 Reveal 应用程序

Reveal(我也称之为 uncover)应用程序需要一组相同维度的图像,尽管这个维度不需要是我为吃面的女孩准备的。你可以有不同数量的图像,而不是我的四个。这是通过在setupimages函数中使用document.getElementsByTagName调用来支持的。当然,如果您选择包含其他图像,您将需要调用持有img元素的div来代替文档。

构建国家/首都测验,并使其成为您自己的测验

通过使用Math.random从事实表中选择四个国家/地区大写字母对,并检查以确保不重复任何一对,来设置智力竞赛节目。元素是为国家和首都动态创建的,首都在窗口中出现的顺序是随机的。这些元素是在设置了tabIndex的情况下创建的。当每个元素被创建时,addEventListener被点击事件调用,keyup事件调用。功能及其关系如表 10-3 所示。请注意,这里没有touchhandler事件,因为设备上的浏览器可以正确解释使用触摸的点击事件。更一般地说,设置tabIndex属性提供了选项卡功能,而不需要任何额外的 JavaScript 代码。

表 10-3

用于测验的函数关系

|

功能

|

由...调用

|

祈求者

|
| --- | --- | --- |
| init | 由body标签中的onload属性执行的操作 | setupgame |
| setupgame | init |   |
| pickelement | 在setupgameaddEventListener多次动作 |   |

带注释的代码如表 10-4 所示。

表 10-4

国家/首都测验代码

|

密码

|

描述

|
| --- | --- |
| <!DOCTYPE html> | 标准标题 |
| <html> | html标签 |
| <head> | head标签 |
| <title>Quiz with Reward!</title> | 完整标题 |
| <style> | style标签 |
| country {position:absolute;left: 0px; top: 0px; border: 2px; border-style: double; background-color: white; margin: 5px; padding: 3px; visibility:hidden;} | 格式化国家区块 |
| capital {position:absolute;left: 0px; top: 0px; border: 2px; border-style: double; background-color: white; margin: 5px; padding: 3px; visibility:hidden;} | 格式化资本块 |
| #vid {position :absolute; visibility:hidden; z-index: 0; max-width: 50%; height: auto;} | 使视频隐藏到播放时间;设置宽度限制 |
| main {display:block;} | 主线前后的力线断开 |
| </style> | 结束style标签 |
| <script type="text/javascript"> | Script标签 |
| var facts = [ | 保存测验信息的变量;内部数组中的第三个位置用于指示该事实是否已被选择用于该游戏 |
| ["China","Beijing",false], |   |
| ["India","New Delhi",false], |   |
| ["European Union","Brussels",false], |   |
| ["United States","Washington, DC",false], |   |
| ["Indonesia","Jakarta",false], |   |
| ["Brazil","Brasilia",false], |   |
| ["Russia","Moscow",false], |   |
| ["Japan","Tokyo",false], |   |
| ["Mexico","Mexico City",false], |   |
| ["Germany","Berlin",false], |   |
| ["Turkey","Ankara",false], |   |
| ["France","Paris",false], |   |
| ["United Kingdom","London",false], |   |
| ["Italy","Rome",false], |   |
| ["South Africa","Pretoria",false], | 注:南非有三个首都;我选择了比勒陀利亚。这是一个匹配的游戏,所以玩家永远不会看到其他两个城市的名字 |
| ["South Korea","Seoul",false], |   |
| ["Argentina","Buenos Aires",false], |   |
| ["Canada","Ottawa",false], |   |
| ["Saudi Arabia","Riyadh",false], |   |
| ["Australia","Canberra",false] |   |
| ]; | 封闭的外部事实阵列 |
| var thingelem; | 用于每个块项目 |
| var nq = 4; | 游戏中提问的问题数量 |
| var elementinmotion; | 用于指示将被移动到下一个玩家匹配项的选定元素 |
| var makingmove = false; | 当玩家选择两个方块时设置为真 |
| var inbetween = 150; | 列间距 |
| var col1 = 0; | 第一列的开始 |
| var row1; | 在 init 中设置;第一行开始 |
| var rowsize = 60; | 为每个项目分配的垂直空间 |
| var slots = new Array(nq); | 将索引保存到资本项目的事实数组中 |
| function init(){ | init功能的标题 |
| row1= .6* window.innerHeight; | 设置row1为总窗高的表达式;如果高度太小,将需要垂直滚动 |
| setupgame(); | 调用setupgame |
| } | 关闭init |
| function setupgame() { | setupgame的标题 |
| var i; | 用于for循环 |
| var c; | 用于计算随机选择 |
| var s; | 用于计算大写字母的候选槽 |
| var mx = col1; | 起始 x 位置 |
| var my = row1; | 起始 y 位置 |
| var d; | 将保存对国家/地区的已创建元素的引用 |
| var uniqueid; | 用于保存所有生成的 id |
| for (i=0;i<facts.length;i++) { | 重复所有的事实 |
| facts[i][2] = false; | 设置为未使用 |
| } | 关闭for回路 |
| for (i=0;i<nq;i++) { | 循环所有插槽 |
| slots[i] = -100; | 设置槽值 |
| } | 关闭循环 |
| for(i=0;i<nq;i++) { | 循环,直到选择了nq个不同的事实 |
| do {c = Math.floor(Math.random()*facts.length);} | Do获取一个随机值 |
| while (facts[c][2]==true) | 如果这一事实成立,重复do条款 |
| facts[c][2]=true; | 现在,选择了一个未采用的事实,将其标记为采用 |
| uniqueid = "c"+String(c); | 生成一个 ID |
| d = document.createElement('country'); | 创建一个元素 |
| d.innerHTML = ( | 设置innerHTML |
| "<div tabIndex='"+String(2+i)+"' class="thing" id='"+uniqueid+"'>placeholder</div>"); | …成为响应style指令的divclass 'thing' |
| document.body.appendChild(d); | 附加到正文(以便显示) |
| thingelem = document.getElementById(uniqueid); | 设置thingelem来引用新创建的元素 |
| thingelem.textContent=facts[c][0]; | 让它的背景成为国家 |
| thingelem.style.top = String(my)+"px"; | 将其定位在my垂直位置… |
| thingelem.style.left = String(mx)+"px"; | … mx水平位置 |
| thingelem.addEventListener('click',pickelement); | 为click设置事件处理 |
| thingelem.addEventListener('keyup',pickelement); | 为keyup设置事件处理 |
| thingelem.style.visibility="visible"; | 将可见性设置为可见 |
| uniqueid = "p"+String(c); | 现在创建一个新的唯一 ID |
| d = document.createElement('cap'); | 创建一个元素 |
| d.innerHTML = ( | 设置innerHTML |
| "<div tabIndex="0" class="thing" id='"+uniqueid+"'>placeholder</div>"); | 物品的div;代码将改变tabIndex |
| document.body.appendChild(d); | 附加到正文,这样它就可见了 |
| thingelem = document.getElementById(uniqueid); | 设置thingelem以引用该元素 |
| thingelem.textContent=facts[c][1]; | 设置其内容 |
|   | 从空位中随机选择一个 |
| do {s = Math.floor(Math.random()*nq);} | 务必将s设为随机值 |
| while (slots[s]>=0) | 如果slots[s]的当前值大于或等于零,则意味着该槽被占用,因此重复do子句以获得新值s |
| slots[s]=c; | 该槽未被占用,因此将其设置为值c |
| thingelem.tabIndex = String(6+s); | 设置其tabIndex |
| thingelem.style.top = String(row1+s*rowsize)+"px"; | 使用s指示的位置将其垂直放置 |
| thingelem.style.left = String(col1+inbetween)+"px"; | 所有大写字母都有相同的水平位置作为第二列 |
| thingelem.addEventListener('click',pickelement); | 为click设置事件处理 |
| thingelem.addEventListener('keyup',pickelement); | 为keyup设置事件处理 |
| my +=rowsize; | 增加my以进入下一行 |
| } | 关闭for回路 |
| document.f.score.value = "0"; | 将分数设置为 0 |
| return false; | 返回,防止文档的任何刷新(可能不需要) |
| } | 关闭setupgame |
| function pickelement(ev) { | pickelement的标题 |
| if (ev.keyCode ===9) {return;} | 如果keycode是 tab 键,立即返回 |
| var thisx; | 将保存指示水平位置的字符串 |
| var thisxn; | 移除px后将保持新的水平位置 |
| var sc; | 得分 |
| if (makingmove) { | 如果makingmove为真 |
| if (this==elementinmotion) { | 如果这是选择的第一个项目,即同一个块被点击两次,则认为这不是一个好的移动,并返回等待第一次点击 |
| elementinmotion.style.backgroundColor = "white"; | 把它变白 |
| makingmove = false; | 重置〔??〕 |
| return; | return |
| } | 如果关闭 |
| thisx= this.style.left; | this块是被选择的第二个不同的项目;获得水平位置 |
| thisx = thisx.substring(0,thisx.length-2); | 移除px |
| thisxn = Number(thisx) + 115; | 转换为数字并添加一些空格 |
| elementinmotion.style.left = String(thisxn)+"px"; | 重置elementinmotion以移动元素 |
| elementinmotion.style.top = this.style.top; | 设置垂直坐标以保存垂直标高 |
| makingmove = false; | 重置〔??〕 |
| if (this.id.substring(1)``==elementinmotion.id.substring(1)) { | 现在,通过比较"c""p"之后的id字符串部分,检查这是否是一个好的匹配 |
| elementinmotion.style.backgroundColor = "gold"; | 设置为gold |
| this.style.backgroundColor = "gold"; | 设置为gold |
| document.f.out.value = "RIGHT"; | 输出信息 |
| sc = 1+Number(document.f.score.value); | 增量分数 |
| document.f.score.value = String(sc); | 显示分数 |
| if (sc==nq) { | 检查这是否意味着nq已经匹配 |
| v = document.getElementById("vid"); | 如果有,获取视频 |
| v.style.top = String(row1+4*rowsize+20)+"px"; | 位于项目正下方;如果高度很小,视频将离屏,需要垂直滚动才能看到;它会被听到。 |
| v.style.visibility = "visible"; | 使视频可见 |
| v.style.zIndex="10000"; | 放在项目的顶部(这是任何改变把视频放在顶部;现在它在街区之下) |
| v.play(); | 播放视频 |
| } | 如果sc==nq关闭 |
| } | 如果 id 匹配则关闭 |
| else { | 否则(糟糕的举动) |
| document.f.out.value = "WRONG"; | 显示错误 |
| elementinmotion.style.backgroundColor = "white"; | 设置为白色 |
| } | 关闭 else |
| } | 如果是第二项,则关闭 |
| else { | Else(选择的第一个项目) |
| makingmove = true; | 设置makingmove标志 |
| elementinmotion = this; | 保存此参考供以后使用 |
| elementinmotion.style.backgroundColor="tan"; | 设置颜色 |
| } | 关闭 else |
| } | 关闭pickelement功能 |
| </script> | 关闭script部分 |
| </head> | 关闭head部分 |
| <body onLoad="init();"> | body标签;加载时通知调用init |
| <main tabIndex="1"> | tabIndex设定主要元素 |
| <h1>G20 Countries and Capitals </h1> | 标题 |
| <br/> | 强制换行 |
| This is a quiz for matching country and capital.``There are 4 countries and 4 capitals. | 说明 |
| Click (or tab and then press enter)``to pick a country or capital and``then click (or tab and then press enter)on corresponding capital or country.``There will be a video (with sound) if you match all 4\. You can tab through``all the elements repeated times. | 说明,续 |
| <p> | 段落标签 |
| Reload for new game. </p> | 更多说明 |
| </main> | 关闭main元素 |
| <p> | 段落标签 |
| <form name="f" > | 一个form元素 |
| Action: <input name="out" type="text"``value="RIGHT OR WRONG"/><br/> | 将显示玩家移动的结果 |
| Score: <input name="score" type="text"``value="0"/> | 分数,从 0 开始 |
| </form> | 关闭表单 |
| </p> | 结束段落 |
| <video id="vid" controls="controls"``preload="auto" width="50%"``alt="Fireworks video"> | 视频标签;注释alt |
| <source src="sfire3.webmvp8.webm" type='video/webm; codec="vp8, vorbis"'> | 保存不同格式的视频,从webm开始;注意:我保留了这些文件的长名称 |
| <source src="sfire3.mp4"> | MP4 格式 |
| <source src="sfire3.theora.ogv"``type='video/ogg; codecs="theora, vorbis"'> | OGV 格式 |
| Your browser does not accept the video tag. | 旧浏览器的标准消息 |
| </video> | 关闭video |
| </body> | 关闭body |
| </html> | 关闭html |

测试和上传国家/首都测验应用程序

您可以决定在您的列表中包括哪些国家,或者,如果您想将测验更改为不同的内容,您需要制定和创建定义内容的字符串对。您也可以选择不同的视频作为成功完成测验的奖励。如果你想让这个测验为视障人士和其他人服务,你会想要选择一个包含响亮、欢快音乐的视频。

测试并上传拼图转向视频应用程序

在第八章中,你学习了如何创建一个简单的拼图游戏,当拼图游戏完成时,它会变成一个视频剪辑。我在 jigsaw 程序中添加了本章中讨论的对触摸响应的增强功能,并将其包含在本章的源代码中。如果您在台式机或笔记本电脑上检查这段代码,您不会发现任何不同。但是,如果您将代码与基本图像和视频文件一起上传到您自己的网站,您应该会看到它在移动设备上工作。

正如我在第八章中所指出的,要注意移动设备上的苹果操作系统可能要求用户点击所有视频的播放按钮。这被苹果认为是一个特性,而不是一个 bug。要求点击确实给了设备所有者阻止下载视频的机会,这需要时间和电池电量,并可能产生费用。我已经在第二章和第三章讨论了 Chrome 的自动播放政策。对于拼图到视频的项目,我更喜欢从拼图到视频的转换是自动的,这就是在台式机或笔记本电脑上。你需要意识到这个问题,因为将来浏览器可能会有变化。

您可以使用自己的视频制作自己的游戏,提取第一帧作为图像文件作为基础。

摘要

在这一章中,你探讨了对于扩大你的作品的受众至关重要的问题。

响应式设计的关注点包括适应不同屏幕的大小和形状,以及提供触摸和鼠标操作。描述了示例中没有使用的某些 HTML 和 CSS 特性。

对可访问性的关注包括当鼠标或触摸不可行时提供对键盘操作的支持。这包括设置制表符索引,即使在动态创建元素时也可以这样做。如果视频中的音频存在且合适,播放视频作为成功完成测验的奖励对视障人士有效。

这里描述的应用程序和增强的 jigsaw 转换成视频是建立在您在本书中学到的一切之上的,包括动态构建元素,使用数组和图像,以及设置事件和事件处理。我希望你喜欢这种体验,并开始构建自己的项目。

posted @ 2024-08-19 15:41  绝不原创的飞龙  阅读(109)  评论(0)    收藏  举报