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 中出现的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-1

HTML5 徽标的打开屏幕

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-2

徽标缩小

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1-3

使用 Firefox 的应用程序

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

项目历史和关键要求

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 所示。检查代码时可以参考这张图。请记住,在坐标中,第一个数字是距画布左边缘的距离,第二个数字是距画布上边缘的距离。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 的结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 显示了我的程序的开始屏幕。请注意,您从要排列的七个对象开始。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2-1

家庭照片的打开屏幕

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 所示。如果向应用程序添加新类型的形状,您将需要发明或发现定义该形状的数据。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 表示变量的含义。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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` | 针对旧浏览器的消息 | | `` | 结束画布标签 | | `Open window with image (which you can save into image file) ` | 保存图像的按钮 | | `Remove last object moved ` | 用于移除对象的按钮 | | `` | 关闭`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 来学习制作球形视频的两种技术。注意:所有图形都是静态的动画截图。你需要相信我的话,视频确实会在盒子里移动和跳动。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-1

屏幕捕获,全窗口

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-2

虚拟球的轨迹

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-3

较小窗口中的应用程序

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-4

窗口调整到非常小

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-5

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 所示的内容。视频元素也可以移动得如此之快,以至于它逃离了盒子。这是留给读者的一个有趣的练习。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 显示了两条路径的轮廓。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3-8

遮罩路径的轮廓

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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,使画布交替位于谷歌地图产生的材料之下和之上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值