HTML5 canvas开发详解(第2版)

978-7-115-35148-7
作者: 【美】Steve Fulton Jeff Fulton
译者: 任旻罗泽鑫
编辑: 汪振傅道坤

图书目录:

详情

HTML5 是Web应用程序开发的新趋势,Canvas是其最令人兴奋的新特性。本书即通过对Canvas元素进行详细介绍,引领读者进入HTML5开发的大门。通过本书,读者将学到如何使用Canvas进行绘图、渲染文字、处理图像、创建动画,最终构建出交互式的多媒体应用程序。

图书摘要

版权信息

书名:HTML5 canvas开发详解(第2版)

ISBN:978-7-115-35148-7

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] Steve Fulton Jeff Fulton

  译    任 旻  罗泽鑫

  责任编辑 傅道坤

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Steve Fulton是作家、讲师,同时还是一位游戏开发专家。他在Mattel Toys公司担任数字游戏软件开发部的高级经理职务。

Jeff Fulton是一位RIA(富Internet应用)和网页游戏的开发者,同时也是移动游戏及应用的开发者,在过去的五年半中,他在自己的个人网站(http://www.8bitrocket.com)中记录了很多关于他的新闻、故事、博客,内容包括Flash、Corona教程,以及现在的HTML5技术。他现在是Producto Studio公司的首席技术官,在Twitter上名称@8bitrocket。

Jeff 在过去的14年中担任Mattel Toys公司的网站开发经理,帮助公司赢得了众多在线用户。

专业书评

“Canvas可以说是HTML5中最令人兴奋的标记,本书对它进行了钜细靡遗的讲解,从详尽程度上来讲 ,没有图书能出其右。本书囊括的大量实例令人深受启发,此外,它还详细介绍了HTML5的补充特性(比如视频和音频),这对读者来说是意外的惊喜”。

——《JavaScript高效图形编程》作者,资深视频游戏开发人员,Raffaele Cecco

本书卖点

随着Canvas的持续升温,Flash的光芒迅速消退。本书是Canvas的畅销图书,它在上一版的基础上,通过讲解如何开发可交互式多媒体应用,引导读者学习HTML5 Canvas。通过本书,你将学到如何使用Canvas进行绘图、渲染文字、处理图像、创建动画,而这些是开发交互式Web游戏的必备知识。

本书针对Canvas和HTML5技术的最新变动进行了更新,其中包含了大量清晰、可重用的代码示例,无论你当前使用的是Flash、Silverlight,还是HTML与JavaScript,都可以通过本书中的这些代码示例迅速掌握HTML5 Canvas。

你也会从本书中发现,为什么HTML5代表着创新性Web开发的未来。


本书封面中的动物是新西兰的Kaka鸟,这是一种当地特有的鹦鹉。Kaka这个名字来源于毛利语中的鹦鹉一词。它们是鹦鹉家族的一个成员,在距今80万~100万年以前,随着新西兰从冈瓦纳大陆分裂出来而离开了其他种类的鹦鹉。这种鹦鹉的一大特征就是拥有布满刷毛的舌头,用来收集花蜜。

一只中等体型的鹦鹉大约在18英尺长,Kaka鹦鹉的体型则很矮小,但也很结实,有着正方形的尾巴。Kaka的羽毛主要是橄榄棕色,在后翅和臀部有明亮的深红色斑点。脸颊上有黄褐色的斑点,还有灰色的头冠。Kaka和别的鹦鹉一样,有着大幅度弯曲的喙,用来撬出土壤的种子,挖掘土里的虫子。Kaka也吃水果、浆果、花蜜和花。这些鸟栖息在树上,生活在新西兰森林的华盖上。Kaka的行为很社会化,它们成群地生活在一起,有时也会和一些本地的鹦鹉种类在一起。在冬季,处于繁殖期的鹦鹉夫妇会在空心的书中搭建巢穴,繁殖2~4枚蛋。鹦鹉夫妇会一起喂养新出生的小鹦鹉。

由于森林砍伐、捕食者以及与非本地物种的食物竞争等原因,如今Kaka已经濒临灭绝。与Kaka相似的另外两种鹦鹉Kea和Kakapo也面临着相似的问题。事实上,这两种Nestor属的生物已经灭绝了(最近的在1851年)。


Copyright ©2013 by O’Reilly Media, Inc.

Simplified Chinese Edition, jointly published by O’Reilly Media, Inc. and Posts & Telecom Press, 2014. Authorized translation of the English edition, 2013 O’Reilly Media, Inc., the owner of all rights to publish and sell the same.

All rights reserved including the rights of reproduction in whole or in part in any form.

本书中文简体字版由O’Reilly Media, Inc.授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式复制或抄袭。

版权所有,侵权必究。


O’Reilly Media通过图书、杂志、在线服务、调查研究和会议等方式传播创新知识。自1978年开始,O’Reilly一直都是前沿发展的见证者和推动者。超级极客们正在开创着未来,而我们关注真正重要的技术趋势——通过放大那些“细微的信号”来刺激社会对新科技的应用。作为技术社区中活跃的参与者,O’Reilly的发展充满了对创新的倡导、创造和发扬光大。

O’Reilly为软件开发人员带来革命性的“动物书”;创建第一个商业网站(GNN);组织了影响深远的开放源代码峰会,以至于开源软件运动以此命名,创立了Make杂志,从而成为DIY革命的主要先锋;公司一如既往地通过多种形式缔结信息与人的纽带。O’Reilly的会议和峰会集聚了众多超级极客和高瞻远瞩的商业领袖,共同描绘出开创新产业的革命性思想。作为技术人士获取信息的选择,O’Reilly现在还将先峰专家的知识传递给普通的计算机用户。无论是通过书籍出版,在线服务或者面授课程,每一项O’Reilly的产品都反映了公司不可动摇的理念——信息是激发创新的力量。

业界评论

“O’Reilly Radar博客有口皆碑。”

——Wired

“O’Reilly凭借一系列(真希望当初我也想到了)非凡想法建立了数百万美元的业务。”

——Busincss 20

“O’Reilly Conference是聚集关键思想领袖的绝对典范。”

——CRN

“一本O’Reilly的书就代表一个有用、有前途、需要学习的主题。”

——Irish Times

“Tim是位特立独行的商人,他不光放眼于最长远,最广阔的视野并且切实地按照Yogi Berra的键议去做了‘如果你在路上遇到岔路口,走小路(岔路)。’回顾过去Tim似乎每一次都选择了小路,而且有几次都是一闪即逝的机会,尽管大路也不错。”

——Linux Journal


本书是HTML5 Canvas的畅销图书,在上一版的基础之上,针对Canvas和HTML5技术的最新变动进行了更新。本书通过讲解如何开发交互式多媒体应用,引导读者学习HTML5 Canvas,其内容包括HTML5 Canvas简介、在Canvas上绘图、Canvas的文本API、Canvas图像、Canvas中的数学、物理知识以及由其实现的动画效果、整合操作视频和音频、使用位图和tile表格开发游戏、开发Web应用,以及WebGL和ElectroServer5的使用等内容。

本书包含了大量清晰、可重用的代码示例,适合各个层级的Web开发人员阅读,而且无论他们当前使用的是Flash、Silverlight,还是HTML与JavaScript,都可以通过本书迅速掌握HTML5 Canvas。


Steve Fulton

首先,我要感谢我美丽的妻子Dawn,感谢她的耐心、指导和她在本书写作过程中的支持。我还要感谢我的女儿Rachel、Daphnie和Kaitlyn,她们每次要我一起玩时,我都说“好的,就几分钟”,我要感谢她们对此没有表现出特别沮丧。当时我的头都快被这些书稿和例子埋起来了。我还要感谢我的母亲用非常有限的资源为我创造了一个无所不能的童年。同时还要感谢我的姐妹Mari、Carol、Susan和叔叔Richard,感谢他们教的每一件事。我还要感谢Martin、Fulton、Campi、Miller、 Garnica和Peters全家给予的关爱和支持。最后要特别感谢我的父亲,他牺牲了他的梦想来成就了我的梦想。

Jeff Fulton

我要感谢我迷人的妻子Jeanne和两个出色的儿子Ryan和Justing,感谢他们允许我第三次将精力全部投入到写作过程中。当时间变得难熬,例子不能在每个浏览器上运行时他们都给了我强有力的支持。我还希望感谢我的母亲,以及姐妹Mari和Carol,以及Perry、Martin、Campi和Backlar全家的关爱和支持。像Steve一样,我也要感谢父亲,他在我们写完本书第1版的时候去世了。他教导我要在有能力的时候追逐梦想,因为我们只有很短的时间来实现。

作者还要感谢所有O’Reilly的工作人员,特别是Mike Loukides,他为我们提供了写作本书的机会。还要感谢Simon St. Laurent和Meghan Blanchette带领我们走出各种困境。还要感谢编辑Kristen Borg帮助我们完成工作。

还要感谢技术审核员Nick Pinkham、Kyle Nau、Tracey Oshiro和Robert Brisita。

感谢Producto Studio的John和Sandy Santos,感谢Electrotank的每一个员工,感谢Creative Bottle、Jet Morgan Games、Sprout、Nickelodeon、Mattel和Adobe公司。还要感谢James Becker、Ace The Super Villain、Richard Davey、Marty Goldman、Curt Vendel、Squize,以及来自GamingYourWay.com的nGFX,Bonnie Kravis、Carl Ford和所有Crossfire Media的朋友。还要感谢Jen、Mike Foti、Wesley Crews、Eric Barth、Brandon Crist、Ian Legler、Mike Peters、Jason Neifeld、John Campi、Arnie Katz、Bill Kunkel (R.I.P.)、Chris Crawford、Rob Fulop和Nolan Bushnell。


自从本书第1版发行之后,在过去的两年里,HTML5 Canvas的使用有了突飞猛进的增长。本书的第1版可以称得上是第一批介绍Canvas的专著之一。在我们为自己的快速而感到自豪同时也意味着我们曾经独自进行了大量的研究和探索。早在2011年,只有极少数HTML5 Canvas应用的例子和教程。但在2013年情形发生了改变。现在有许多关于HTML5 Canvas的资源可供选择,从框架到API,有许多网站和书籍进行专门的阐述。为了编写第2版,我们进行了大量艰辛的工作来检查在第1版中哪些部分有效,哪些部分已经失效。在接下来的章节中,描述了一些令人激动的改变和更新,这几页是非正常值得期待的。

本书大部分内容与第1版保持一致。这样做的原因是因为本书是面向广泛开发者的,既有从来没有接触过Canvas的开发者,也有已经有一些经验想学习Canvas高级使用技巧的开发者。

本书每一章都重新进行了修订,对代码进行更新和优化,更新浏览器的兼容性以及在过去两年中发现的其他问题。一小部分内容被删除了。一些冗余的代码列表从书中移动到了代码包中,方便了本书的阅读。我们用更多更简短的示例替换了第4章的部分内容。我们还完全重写了第10章。我们删除了PhoneGap的介绍,这是因为类似的内容已经非常普遍了。

我们还添加了许多新的内容,相信这些内容可以帮助读者的Canvas应用更上一层楼。这些内容包括:

使用HTML5和Canvas编程最大的好处就是进入的门槛非常低——需要的所有工具就是一个现代浏览器和一个文本编辑器。

为了得到最大兼容性,本书建议读者下载或使用下列最新版本的浏览器,并且排名越前优先级越高:

本书中的每一个示例都在Google Chrome、Safari和FireFox中测试过。本书已经尽最大努力确保这些示例可以兼容尽可能多的浏览器,不过,这里依然推荐读者使用Google Chrome或Safari浏览器以得到最好效果。

读者最好了解一些现代语言的编程知识,如C、C++、C#、ActionScript 2、ActionScript 3、Java或JavaScript。不过,在第1章介绍Canvas的同时也会介绍一些网页编程知识,使得新读者入门更容易。

如果读者是Flash的开发者,那么JavaScript语言与ActionScript 1语言在本质上是同一种语言。Adobe公司在ActionScript 2中进行了一些改进,但读者应该也会比较容易地接受JavaScript语言。如果读者只有使用ActionScript 3语言的经验,那么使用JavaScript时会感觉有一些退步。

如果读者是Silverlight或C#的开发者,那么请深呼吸,回忆一下在ASP.NET和C#出现之前不得不使用VBScript语言开发网页应用程序时的场景。现在的情况与之有一些类似。

本书分为11章。在第2版中对所有章节都进行了更新、修订和扩充。第1~4章通过示例向读者展示HTML Canvas的API的使用方法。内容主要包括文本、图像和绘图相关功能。这些章节包含几个完整的应用,但是演示程序的组成部分主要是为了向读者展示Canvas API的不同功能。在接下来的第5~10章中,示例的范围被扩展到应用程序的级别,在这个过程中将对Canvas的API做进一步的介绍。这些章节介绍了应用中的数学和物理算法、视频、音频、游戏和移动应用。在最后的第11章中,本书介绍了还处于实验性的领域:3D、多人应用、Windows8和一个Canvas的对象模型。

本书不会将W3C发布的Canvas API标准列举出来并一一讲解。本书会对一些API详细讲解,对于另一些不适合游戏开发的API将会略过。读者可以在下面的网站中查看所有API文档。

http://dev.w3.org/html5/2dcontext

本书的目标是重点介绍Canvas中可用于创作动画、游戏和网页娱乐程序的特性,同时支持网页和移动网页。

本书的图例如下所示。

提示

提示这个图标用来强调一个提示、建议或一般说明。

警告

警告这个图标用来表示一个警告或注意事项。

本书的目的是为了帮助读者完成工作。一般而言,你可以在你的程序和文档中使用本书中的代码,而且也没有必要取得我们的许可。但是,如果你要复制的是核心代码,则需要和我们打个招呼。例如,你可以在无需获取我们许可的情况下,在程序中使用本书中的多个代码块。但是,销售或分发O’Reilly图书中的代码光盘则需要取得我们的许可。通过引用本书中的示例代码来回答问题时,不需要事先获得我们的许可。但是,如果你的产品文档中融合了本书中的大量示例代码,则需要取得我们的许可。

在引用本书中的代码示例时,如果能列出本书的属性信息是最好不过。一个属性信息通常包括书名、作者、出版社和ISBN。例如“HTML5 Canvas, Second Edition by Steve Fulton and Jeff Fulton (O’Reilly), Copyright 2013 8bitrocket Studios, 978-1-449-33498-7”。

在使用书中的代码时,如果不确定是否属于正常使用,或是否超出了我们的许可,请通过permissions@oreilly.com与我们联系。

如果你想就本书发表评论或有任何疑问,敬请联系出版社:

美国:

 O’Reilly Media Inc.

 1005 Gravenstein Highway North

 Sebastopol, CA 95472

中国:

 北京市西城区西直门南大街2号成铭大厦C座807室(100035)

 奥莱利技术咨询(北京)有限公司

我们还为本书建立了一个网页,其中包含了勘误表、示例和其他额外的信息。你可以通过如下地址访问该网页:

http://www.oreilly.com/catalog/9781449387860

关于本书的技术性问题或建议,请发邮件到:

bookquestions@oreilly.com

欢迎登录我们的网站(http://www.oreilly.com),查看更多我们的书籍、课程、会议和最新动态等信息。

Facebook: http://facebook.com/oreilly

Twitter: http://twitter.com/oreillymedia

YouTube: http://www.youtube.com/oreillymedia

Safari在线图书是一个按需订阅的数字图书馆。它有不少于7500本技术和创意相关的书籍和视频供你参考和搜索。

通过订阅,你可以在线阅读任何页面或任何视频,甚至可以从手机或移动设备上在线阅读。你可以在书籍出版前访问到它们,并给读者发送反馈。其他功能还包括:复制和粘贴代码、组织收藏夹、下载和标记章节、做笔记、打印等。

O’Reilly Media已经将本书英文版上传到Safari在线图书服务。在http://my.safaribooksonline.com上免费注册,你就可以访问本书所有章节以及类似主题的书籍。


HTML5是新一代的HTML,即超文本标记语言。HTML从1993年第一次标准化后,便奠定了万维网的基础。HTML通过使用将标签用尖括号(< >)括起来的方式定义Web页面内容。

HTML5 Canvas是屏幕上的一个由JavaScript控制的即时模式位图区域。即时模式是指在画布上呈现像素的方式,HTML Canvas通过JavaScript调用Canvas API,在每一帧中完全重绘屏幕上的位图。作为一名程序员,所要做的就是在每一帧渲染之前设置屏幕的显示内容,这样才能显示正确的像素。

这使得HTML5 Canvas与在保留模式下运行的Flash、Silverlight或SVG有很大的区别。在保留模式下,对象显示列表由图形渲染器保存,通过在代码中设置属性(例如,x坐标、y坐标和对象的alpha透明度)控制展示在屏幕上的对象。这使得程序员可以远离底层操作,但是它弱化了对位图屏幕最终渲染效果的控制。

基本的HTML5 Canvas API包括一个2D环境,允许程序员绘制各种图形和渲染文本,并且将图像直接显示在浏览器窗口定义的区域。读者可以对画布上放置的的图形、文本和图像应用颜色、旋转、渐变色填充、alpha透明度、像素处理等,并且可以使用各种直线、曲线、边框、底纹来增强其效果。

就其本身而言,HTML5 Canvas 2D环境是一个用来在位图区域渲染图形显示的API,但人们很少使用该技术在这个环境中创建应用程序。通过跨浏览器兼容的JavaScript语言可以调用键盘鼠标输入、定时器间隔、事件、对象、类、声音、数学函数等功能,希望读者能够学会并使用HTML5 Canvas创建优质的动画、应用程序和游戏。

本书将深入解读Canvas API。在此过程中,本书将展示如何使用Canvas API来创建应用程序。本书中的很多技术已经被成功应用于其他平台,现在,本书要将它们应用到HTML5 Canvas这个令人兴奋的新技术上来。

支持HTML5 Canvas的浏览器

除了IE 8以外,很多新版本的浏览器都支持HTML5 Canvas。几乎每天都会支持新的特性。支持最好的应该是Google Chrome,紧接着是Safari、Internet Explorer 10、Firefox、Opera。本书将利用名为modernizr.js的JavaScript库来帮助判断各个浏览器支持哪些Canvas特性。

最近HTML5的定义已经发生了转变,当作者在2010年编写本书第一版的时候,W3C的HTML5规范是一个独特的单元,它涵盖了有限的功能集合,其中包括了诸如新的HTML标签(<video>、<audio>和<canvas>)之类的东西。然而,在过去的一年中,这一定义已经发生了改变。

那么,究竟什么是HTML5?在W3C HTML5的常见问题中,关于HTML5是这样说明的:HTML5是一个开放的平台下开发的免费许可条款。

术语HTML5会被人们使用在以下两个方面。

在过去的几个月里,我们通过交谈和项目工作了解到的是:普通人(或者说那些急着要完成项目的客户)谁也不会严格遵守上述定义,这些都是HTML5。因此,当有人说起“HTML5”的时候,他们实际上指的是“开放式网络平台”。

当人们提及“开放式网络平台”时,有一件可以确定的事是,这份邀请名单中一定不能漏掉Adobe Flash。

HTML5是什么?总之,它不是Flash(也不是其他类似的技术)。HTML5 Canvas是最有能力在网络和移动互联网上取代Flash功能的最好的技术。这本书将带领读者学习如何开始使用HTML5 Canvas。

在开始讲解Canvas前,需要谈论一下HTML5的相关标准——这里将使用HTML5来创建Web页面。

HTML是用于在互联网上构建页面的标准语言。本书不会将很多时间花费在讲解HTML上,但HTML是<canvas>的基础,所以不能完全跳过它。

一个基本的HTML页面分成几个部分,通常有<head>和<body>,新的HTML5规范增加了一些新的部分,例如<nav>、<article>、<header>和<footer>。

<head>标签通常包含与使用<body>标签来创建HTML页面相关的信息。将JavaScript函数放在<head>中是约定俗成的,稍后讨论<canvas>标签时也会这样做。虽然有理由把JavaScript函数放在<body>中,但是简单起见,最好把JavaScript函数放在<head>中。

基本的HTML页面如例1-1所示。

例1-1 简单的HTML页面

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH1EX1: Basic Hello World HTML Page</title>
</head>
<body>
Hello World!
</body>
</html>

这个标签说明Web浏览器将在标准模式下呈现页面。根据W3C定义的HTML5规范,这是HTML5文档所必需的。这个标签简化了长期以来在不同的浏览器呈现HTML页面时出现的奇怪差异。它通常为文档中的第一行。

这是包含语言说明的<html>标签:例如," en "为英语。下面是一些常见的语言值:

中文——lang= " zh ";

法语——lang= " fr ";

德语——lang=" de ";

意大利语——lang=" it ";

日语——lang= " ja ";

韩语——lang= " ko ";

波兰语——lang= " pl ";

俄语——lang= "ru ";

西班牙语——lang = " es "。

这个标签说明Web浏览器使用的字符编码模式。如果没有需要特别设置的选项,一般没必要改变它。这也是HTML5页面需要的元素。

这个标签说明在浏览器窗口展示的HTML的标题。这是一个很重要的标记,它是搜索引擎用来在HTML页面上收录内容的主要信息之一。

现在,在浏览器中看看这个页面(这是一个伟大的时刻,可以准备好工具开始开发代码了)。打开所选择的文本编辑器以及Web浏览器——Safari、FireFox、Opera、Chrome或IE。

(1)在文本编辑器中,输入例1-1中的代码。

(2)选择路径,保存为CH1EX1.html。

(3)在Chrome、Safari或Firefox的File菜单中,找到Open File命令,单击它,将看到一个能够打开文件的对话框(在Windows下用Chrome时,也可以按Ctrl+O键来打开文件)。

(4)找到刚刚创建的CH1EX1.html。

(5)单击“打开”按钮。

可以看到图1-1所示的结果。

图1-1 “Hello World!”页面

可以使用多个HTML标签来创建HTML页面,在HTML以前的版本中,需明确指示Web浏览器如何渲染HTML页面的标签(例如<font>和<center>)。然而,在过去10年中,浏览器的标准越来越严格,这类标签就被束之高阁了。CSS(层叠样式表)成为定义HTML内容样式的主要方式。因为本书不是关于如何创建HTML页面的(页面中不包含Canvas),因此这里不打算讨论CSS的内部工作原理。

本节将只关注两个最基本的HTML标签:<div>和<canvas>。

<div>是本书主要使用的一个HTML标签,用来定位<canvas>在HTML页面的位置。

例1-2使用<div>标签定义了“Hello World!”在屏幕上的位置,如图1-2所示。

例1-2 HTML5中的“Hello World!”

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH1EX2: Hello World HTML Page With A DIV </title>
</head>
<body>
<div style="position: absolute; top: 50px; left: 50px; ">
Hello World!
</div>
</body>
</html>

图1-2 使用<div>的HTML5中的“Hello World!”

style="position: absolute; top: 50px; left: 50px;"——这段代码是在HTML页面中使用内联CSS的例子。它告诉浏览器呈现内容的绝对位置为:距离页面顶端50像素,并且距离页面左端50像素。

警告

这个<div>可以在浏览器中定位画布,但是对试图在画布上捕捉鼠标点击时则没有任何帮助。在第5章中,本书将讨论一种既能定位画布又能捕获正确的鼠标点击位置的方法。

利用对<div>进行绝对定位,有助于更好地使用<canvas>。把<canvas>放在<div>内,<div>可以帮读者获取信息。例如,当鼠标滑过画布时,可以获取定义指针的相对位置。

文档对象模型代表了在HTML页面上的所有对象。它是语言中立且平台中立的。它允许页面的内容和样式被Web浏览器渲染之后再次更新。用户可以通过JavaScript访问DOM。从20世纪90年代末以来,文档对象模型已经成为JavaScript、DHTML和CSS开发最重要的一部分。

画布元素本身可以通过DOM,在Web浏览器中经由Canvas 2D环境访问。但是,在Canvas中创建的单个图形元素是不能通过DOM访问的。正如本章前面讲到的,画布工作在即时模式,它并不保存自己的对象,只是说明在每个单个帧里绘制什么。

例1-2在HTML5页面上使用DOM定位<canvas>标签,这也可以用JavaScript来操作。在开始使用<canvas>前,首先需要了解两个特定的DOM对象:window和document。

window对象是DOM的最高一级,需要对这个对象进行检测来确保开始使用Canvas应用程序之前,已经加载了所有的资源和代码。

document对象包含所有在HTML页面上的HTML标签。需要对这个对象进行检索来找出用JavaScript操纵<canvas>的实例。

JavaScript是用来创建Canvas应用程序的一种程序设计语言,能在现有的任何Web浏览器中运行。如果需要重温JavaScript的相关内容,请关注Douglas Crockford的书《JavaScript: The Good Parts》(O’Reilly)。这本书很流行,并且有很强的参考价值。

使用JavaScript为Canvas编程会产生一个问题:在创建的页面中,从哪里启动JavaScript程序?

把JavaScript放进HTML页面的 <head> 标签中是个不错的主意,这样做的好处是很容易找到它。但是,把JavaScript程序放在这里就意味着整个HTML页面要加载完JavaScrpit才能配合HTML运行,这段JavaScript代码也会在整个页面加载前就开始执行了。结果就是,运行JavaScript程序之前必须检查HTML页面是否已经加载完毕。

最近有一个趋势是将JavaScript放在HTML文档结尾处的</body>标签里,这样就可以确保在JavaScript运行时整个页面已经加载完毕。然而,由于在运行<canvas>程序前需要使用JavaScript测试页面是否加载,因此最好还是将JavaScript放在<head>中。如果读者不喜欢这样,也可以采用适合自己的代码习惯。

代码放在哪儿都行——可以放在HTML页面代码行内,也可以加载一个外部 .js文件。加载外部JavaScript文件的代码大致如下。

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

简单起见,这里将把代码写在HTML页面行内。不过,如果读者有把握,把它放在一个外部文件再加载运行也未尝不可。

提示

HTML5不需要再指定脚本类型。

如前所述,将Canvas放入HTML5页面时第一件要做的事就是,看看整个页面是否已经加载,并且开始操作前是否所有HTML元素都已展现。在用Canvas处理图像和声音的时候,这点会非常重要。

为此,这里要使用JavaScript的事件。当定义的事件发生时,事件从对象发出。其他对象监听事件,这样就可以基于事件进行处理。用JavaScript可以监听对象的一些常见事件,包括键盘输入、鼠标移动以及加载结束。

第一个需要监听的事件是window对象的load事件。该事件在HTML页面加载结束时发生。

要为事件添加一个监听器,可以使用DOM的对象的 addEventListener()方法。因为window代表HTML页面,所以它是最高级别的DOM对象。

addEventListener()函数接受以下3个参数。

(1)load事件。

这是监听器的事件名称。对于诸如window的已有对象的事件已经预先定义过。

(2)eventWindowLoaded()事件处理器函数。

当事件发生时调用这个函数。在代码中会调用canvasApp()函数来开始执行主应用程序。

(3)useCapture:true或false。

这个参数设置函数是否在事件传递到DOM对象树的底层对象之前捕捉此种类型的事件。此处始终设为false。

下面是用来测试window是否加载完毕的最终代码。

window.addEventListener("load", eventWindowLoaded, false);
function eventWindowLoaded (){
  canvasApp();
}

另外,也可以用许多其他方式为load事件设置事件监听器。

window.onload = function()
  {
    canvasApp();
  }

或者使用如下代码。

window.onload = canvasApp;

本书使用第一种方法。

前面已经创建了一种测试HTML页面是否加载完毕的方法,下面开始创建JavaScript应用程序。因为JavaScript在HTML页面中运行,所以它可以与其他JavaScript应用程序和代码一起运行。通常,这不会导致什么问题。但是,可能会出现一种情况,即代码中的变量或者函数会与HTML页面中的其他JavaScript代码产生冲突。

Canvas应用程序与在浏览器中运行的其他应用有所不同。由于Canvas只在屏幕上特定的区域执行并显示结果,可以说它的功能是独占的,因此不太会受到页面上其他内容的影响,反之亦然。读者如果想在同一个页面上放置多个Canvas应用,那么在定义JavaScript代码时必须将对应的代码分开。

为避免出现这个问题,可以将变量和函数都封装在另一个函数中。JavaScript函数本身就是对象,Javascript对象既可以有属性也可以有方法。将一个函数放到另一个函数中,读者可以使第二个函数只在第一个函数的局部作用域中。

在示例中,从window load事件中调用的canvasApp()函数将包含整个Canvas应用程序。“Hello World!”示例中将会有一个名为drawScreen()的函数。一旦canvasApp()被调用,则立即调用drawScreen()来绘制“Hello World!”文本。

drawScreen()函数现在是canvasApp()的局部函数。在canvasApp()中创建的任何变量或函数对于drawScreen()来说都是局部的。但是,对于HTML页面的其余部分或其他可能运行的JavaScript应用程序来说却并非如此。

以下是如何封装函数的示例代码,也是Canvas应用程序的代码。

function canvasApp(){
  drawScreen();
  ...
  function drawScreen(){

    ...
  }
}

在HTML的<body>部分添加一个<canvas>标签时,可以参考以下代码。

<canvas id="canvasOne" width="500" height="300">
 Your browser does not support HTML5 Canvas.
</canvas>

现在,小结一下正在讨论的内容。<canvas>标签有3个主要属性。在HTML中,属性被设在尖括号括起来的HTML标签中。这3个属性分别如下。

提示

HTML5元素(包括canvas)还有很多属性,如tabindex、title、class、accesskey、dir、draggable、hidden等。

在开始标签<canvas>和结束标签</canvas>中间可以添加文本,一旦浏览器执行HTML页面时不支持Canvas,就会显示这些文字。以本章的Canvas应用程序为例,这里将使用文本“Your browser does not support HTML5 Canvas(你的浏览器不支持HTML5 Canvas)”。实际上,此处可以放置任意文字。

在JavaScript中使用document对象引用Canvas元素

接下来,用DOM引用HTML中定义的<canvas>标签。document对象加载后可以引用HTML页面的任何元素。

这里需要一个Canvas对象的引用,这样就能够知道当JavaScript调用Canvas API时其结果在哪里显示。

首先定义一个名为theCanvas的新变量,用来保存Canvas对象的引用。

接下来,通过调用document的getElementById()函数得到canvasOne的引用。canvasOne是在HTML页面中为创建的<canvas>标签定义的名字。

var theCanvas = document.getElementById("canvasOne");

现在已经得到了HTML页面上定义的canvas元素的引用,下面就需要检测它是否包含环境。Canvas环境是指支持Canvas的浏览器定义的绘图界面。简单地说,如果环境不存在,画布也不会存在。有多种方式可以对此进行验证。前面已经在HTML页面中定义了Canvas。第一种方式是在调用Canvas的getContext方法之前,检测Canvas对象以及getContext方法是否存在。

if (!theCanvas || !theCanvas.getContext){
  return;
}

实际上,这段代码测试了两件事:第一,它测试theCanvas是否包含false(如果命名的id不存在,document.getElementById()将返回此值);第二,它测试getContext()函数是否存在。

如果测试失败,那么return语句将中断程序执行。

另一个方法是创建一个函数,在其中创建一个虚拟画布,以此来检测浏览器是否支持。这个方法是由Mark Pilgrim在他的HTML5网站创建并流行起来的。

function canvasSupport (){
  return !!document.createElement('canvas').getContext;
}
function canvasApp(){ 
  if (!canvasSupport){
   return;
 }
}

作者最喜欢的方法是使用modernizr.js库Modernizr是一个易用并且轻量级的库,可以检测各种Web技术的支持情况。Modernizr创建了一组静态的布尔值,可以检测是否支持Canvas。

为了在HTML页面中包含modernizr.js,可以从http://www.modernizr.com/的空口下载代码并将外部.js文件包含到HTML页面中。

<script src="modernizr.js"></script>

为了检测是否支持Canvas,将canvasSupport()函数进行修改,如下所示。

function canvasSupport (){
  return Modernizr.canvas;
}

这里将要使用modernizr.js方法,因为它提供了测试Web浏览器是否支持Canvas的最佳途径。

最后需要得到2D环境的引用才能够操作它。HTML5 Canvas被设计为可以与多个环境工作,包含一个建议的3D环境。不过,本书只需得到2D环境。

var context = theCanvas.getContext("2d");

现在便可以创建实际的Canvas API代码了。在Canvas上运行的各种操作都要通过context对象,因为它引用了HTML页面上的对象。

在后面几章中,本书将深入讲解如何在HTML5 Canvas中绘制文本、图形和图像等内容,所以现在只需花一点点时间来了解drawScreen()函数的代码。

这里说的“屏幕”实际上就是定义的画布绘图区域,而不是整个浏览器窗口。之所以将它称为屏幕,是因为在编写游戏或应用程序时,它就是在操作画布时的显示窗口或屏幕。

首先要做的事情是清空绘图区域。下面的两行代码在屏幕上绘制出一个与画布大小相同的黄色方块。fillStyle()设置了颜色,fillRect()创建了一个矩形,并把它放到了屏幕上。

context.fillStyle = "#ffffaa";
context.fillRect(0, 0, 500, 300);

提示

注意,这里调用了context的函数。没有屏幕对象、颜色对象或者其他对象。这就是之前描述的即时模式的示例。

下一章将讨论Canvas的文本函数,这里只是简单地浏览将“Hello World!”文本放到屏幕上的代码。

首先,使用与设置矩形颜色相同的方法设置文本的颜色。

context.fillStyle = "#000000";

然后,设置字体的大小和字号。

context.font = "20px Sans-Serif";

接下来,设置字体的垂直对齐方式。

context.textBaseline = "top";

最后,通过调用context对象的fillText()方法将测试文本显示到屏幕上。这个方法的3个参数分别是是文本字符串、x坐标和y坐标。

context.fillText ("Hello World!", 195, 80);

下面给“Hello World!”文本添加图形。首先,加载一个图像并将它显示出来。第4章将深入讨论图像及其操作,现在仅仅要做的就是显示一个图像到屏幕上。为了将图像显示到画布上,需要创建一个Image()对象的实例,并且将Image.src属性设为将要加载的图像的名字。

提示

读者也可以将其他画布或者视频当作图像显示出来。本书会在第4章和第6章讨论相关主题。

在显示图像之前,需要等待图像加载完毕。设置Image对象的onload函数可以为Image load事件创建一个匿名的回调函数。这个匿名的回调函数将在onload事件发生时被执行。当图像加载完毕,调用context.drawImage()并传输3个参数将图像显示到画布上:Image对象、x坐标以及y坐标。

var helloWorldImage = new Image(); 
helloWorldImage.onload = function () { 
  context.drawImage(helloWorldImage, 160, 130); 
}
helloWorldImage.src = "helloworld.gif";

最后,围绕文本和图像绘制一个方块。为了绘制方块而不填充,可以使用context.strokeStyle属性设置方块边框的颜色,然后调用context.strokeRect()方法绘制矩形边框。strokeRect()的4个参数分别是:左上角的x坐标和y坐标,以及矩形的宽度和高度。

context.strokeStyle = "#000000";
context.strokeRect(5, 5, 490, 290);

完整的HTML5“Hello World!”应用程序代码如例1-3所示,结果如图1-3所示。

例1-3 HTML5 Canvas下的“Hello World!”

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH1EX3: Your First Canvas Application </title>

<script src="modernizr.js"></script>
<script type="text/javascript">
window.addEventListener("load", eventWindowLoaded, false);

var Debugger = function (){ };
Debugger.log = function (message){
  try {
   console.log(message);
  } catch (exception){
   return;
  }
}

function eventWindowLoaded (){
  canvasApp();
}

function canvasSupport (){
  return Modernizr.canvas;
}

function canvasApp (){

    if (!canvasSupport()){ 
      return;
    }

   var theCanvas = document.getElementById("canvasOne");
   var context = theCanvas.getContext("2d");

   Debugger.log("Drawing Canvas");

    function drawScreen(){
     //背景
     context.fillStyle = "#ffffaa";
     context.fillRect(0, 0, 500, 300);

     //文字
     context.fillStyle = "#000000";
     context.font = "20px Sans-Serif";     
     context.textBaseline = "top";
     context.fillText ("Hello World!", 195, 80 );

     //图像
     var helloWorldImage = new Image(); 
     helloWorldImage.onload = function () { 
       context.drawImage(helloWorldImage, 155, 110); 
      } 
      helloWorldImage.src = "helloworld.gif"; 

     //边框
     context.strokeStyle = "#000000";
     context.strokeRect(5, 5, 490, 290);

   }

   drawScreen();

}

</script>
</head>
<body>
<div style="position: absolute; top: 50px; left: 50px;">
<canvas id="canvasOne" width="500" height="300">
Your browser does not support HTML5 Canvas.
</canvas>
</div>
</body>
</html>

图1-3 HTML5 Canvas下的“Hello World!”

在超越“Hello World!”去探索更强大更丰富的内容前,还有些内容需要讨论。本书通过使用现代Web浏览器的console.log功能实现了一个简单的调试方法。这个函数可以通过代码在JavaScript控制台中记录文本信息日志,从而可以帮助用户找出问题(或者机会)。每个浏览器都有一个可以使用console.log的JavaScript控制台(Chrome、Opera、Safari、安装Firebug的Firefox等)。同时,那些不支持console.log的浏览器将弹出讨厌的错误提示。

为了处理这个错误,可以用一个外壳将console.log包装一下,使其只在浏览器支持的情况下被调用。这个外壳创建了一个名叫Debugger的类,然后创建一个在任何位置都可以被调用的Debugger.log静态函数,如下所示。

Debugger.log("Drawing Canvas");

以下是console.log()函数的代码。

var Debugger = function (){ };
Debugger.log = function (message){
  try {
   console.log(message);
  } catch (exception){
   return;
  }
}

通过调用Canvas对象的getContext()方法可以获得HTML5的2D环境对象(Canvas RenderingContext2D对象)。所有操作都要通过该对象进行。CanvasRenderingContext2D对象包含了在画布上绘图所需的所有方法和属性。CanvasRenderingContext2D(简称环境,后同)采用画布左上角为原点(0,0)的笛卡尔坐标系,坐标轴向右、向下为正方向。

然而,所有这些属性和方法都与当前状态关联使用。当前状态是一个必须掌握的概念。它可以帮助读者真正理解HTML5 Canvas的工作方式。实际上,当前状态是一个绘制状态的堆栈,这些状态可以应用到整个画布。在画布上绘制时将操作这些状态。主要包括以下状态。

不必担心,虽然读者现在还不熟悉这些属性,但是后面3章将深入讨论这些属性。

读者是否还记得本章前面讨论的即时模式与保留模式?Canvas是一个即时模式的绘图界面,这就意味着如果什么东西发生了变化就需要即时重新绘制。这样做有以下好处:例如,全局属性很容易将效果应用到整个屏幕。一旦读者想好了,每次重新绘制屏幕的动作都有一个直接并且简单的画布绘制更新过程。

另一个方面,保留模式采用一个绘制界面储存一组对象,并通过一个显示列表来操作。Flash和Silverlight就是使用这种模式。如果应用程序依赖多个拥有独立状态的对象,使用保留模式创建应用程序会很有用。许多能够充分利用画布功能的应用程序(如游戏、活动、动画)都是相同的。这些程序通常很容易在保留模式的绘图界面下进行编码,尤其对于初学者来说。

这里面临的挑战是,既要利用即时模式绘图界面的优势,又要为代码增加更多的功能,以使程序就像工作在保留模式下一样。本书将讨论改善即时模式操作方式的策略,以及如何通过代码使其很容易操作。

也可以使用CSS样式来改变画布的缩放比例。与调整大小不同,缩放提取当前画布的位图区域,然后重新取样以适应CCS样式中设定的宽度和高度的值。例如,将画布缩放到400 × 400区域,可以使用以下CSS样式。

第3章有一个使用变换矩阵缩放Canvas的示例。

Canvas对象是通过在HTML页面的<body>部分中放置<canvas>标签创建的,也可以通过以下代码创建画布实例。

style="width: 400px; height:400px"
var theCanvas = document.createElement("canvas");

Canvas对象有两个相关的属性和方法可以通过JavaScript访问:width和height。这些属性显示当前HTML页面创建的画布的宽度和高度。这里需要强调的是,这两个属性并不是只读的。例如,通过代码可以对它们进行更新,从而影响HTML页面上的对象。这意味着什么?这意味着用户可以在HTML页面上动态地调整画布尺寸而无须重新加载。

提示

Canvas对象目前有两个公共方法。第一个是getContext(),本章前面就使用过。本书将继续使用这个方法获得Canvas 2D环境对象的引用,这样才能在画布上进行绘图。第二个方法是toDataURL(),这个方法返回的数据是代表当前Canvas对象产生位图的字符串。它就像是屏幕的一个快照,通过提供一个不同的MIME类型作为参数,可以返回不同的数据格式。基本的格式是image/png,但是也可以获取image/jpeg和其他格式。下一个应用程序将会使用toDataURL()方法,把画布中的图像导出到另一个浏览器窗口。

提示

第三个公共的方法——toBlob(),已经被定义,并且正在不同的浏览器上实现该方法。toBlob([callback])将返回一个引用图像的文件,而不是一个base64编码的字符串。目前,尚未有任何浏览器实现该方法。

现在来快速看一下另一个广泛提及的“Hello World!”类型的应用程序示例——“猜字母”游戏。本章通过这个示例来说明用JavaScript编写Canvas程序比用Canvas API多了哪些工作量。

图 1-4所示的游戏中,玩家要做的是猜出计算机从字母表中随即抽取的字母。游戏会记录玩家已经猜了多少次,并列出已经猜过的字母,同时告诉玩家需要往哪个方向猜(往Z方向猜还是往A方向猜)。

图1-4 HTML5下的“猜字母”游戏

这个游戏的基本结构与“Hello World!”的设置相同——canvasApp()是主函数,所有其他函数都定义为局部函数。这里使用drawScreen()函数在画布上显示文本。不过,本示例中也包括了其他一些函数,后面的章节会继续描述。

这里是游戏中将要用到的所有变量的列表,它们都在canvasApp()中定义并初始化。因此,它们的作用域都被限定在本地定义的封装函数内。

代码如下所示。

var guesses = 0;
var message = "Guess The Letter From a (lower)to z (higher)";
var letters = [
      "a","b","c","d","e","f","g","h","i","j","k","l","m","n","o",
      "p","q","r","s","t","u","v","w","x","y","z"
      ];
var today = new Date();
var letterToGuess = "";
var higherOrLower = "";
var lettersGuessed;
var gameOver = false;

initGame()函数为玩家初始化游戏。以下是两段重要的代码。第一段代码从字母数组中找出一个随机字母,然后将其储存在letterToGuess变量中。

var letterIndex = Math.floor(Math.random()* letters.length);
letterToGuess = letters[letterIndex];

第二段代码为DOM的window对象添加了一个事件监听器,以“监听”键盘的keydown事件。当某个键被按下时,将调用eventKeyPressed事件处理函数检测按下的字母。

window.addEventListener("keydown",eventKeyPressed,true);

以下是函数的全部代码。

function initGame(){
  var letterIndex = Math.floor(Math.random()* letters.length);
  letterToGuess = letters[letterIndex];
  guesses = 0;
  lettersGuessed = [];
  gameOver = false;
  window.addEventListener("keydown",eventKeyPressed,true);
  drawScreen();
}

当玩家按下一个键时将调用此函数,这个函数包含了游戏中的大部分操作。JavaScript中的每个事件处理函数都会传递event对象。该对象中包含发生事件的相关信息,这里使用e参数表示该对象。

首先检测gameOver变量是否为false,如果是false,则继续检测玩家按下的是哪个键。以下代码实现了该功能:第一行代码从事件中获得按键值,并将其转换为一个字母表中的字母,用于与letterToGuess中存储的字母进行比较。

var letterPressed = String.fromCharCode(e.keyCode);

下一行代码将这个字母转换为小写字母。如果玩家不小心打开大写锁定键也可以检测大写字母。

letterPressed = letterPressed.toLowerCase();

接下来,增加guesses变量的计数,用于显示猜测次数。然后,使用Array.push()方法将字母添加到lettersGuessed数组。

guesses++;
lettersGuessed.push(letterPressed);

检测游戏的当前状态,给予玩家反馈。首先,测试letterPressed与letterToGuess是否相同,如果相同玩家就赢了。

if (letterPressed == letterToGuess){
  gameOver = true;

如果玩家没赢,程序需要分别获得letterToGuess以及letterPressed在数组letters中的索引。下面将用这些数值计算是应该显示“Higher”还是“Lower”,或者显示“That is not a letter.”(这不是一个字母)。为此,这里使用数组的indexOf()方法获得每个字母的对应索引。由于数组是按字母顺序排列的,因此判断显示哪条信息会非常容易。

} else {
  letterIndex = letters.indexOf(letterToGuess);
  guessIndex = letters.indexOf(letterPressed);

现在来进行检测。首先,如果guessIndex小于0,意味着indexOf()返回了−1,也就是说,按键不是一个字母,那么就显示一条错误信息。

if (guessIndex < 0){
  higherOrLower = "That is not a letter";

剩下的测试就简单了。如果guessIndex大于letterIndex,就把higherOrLower文本设为“Lower”。反之,若guessIndex小于letterIndex,就把higherOrLower文本设为“Higher”。

  } else if (guessIndex > letterIndex){
   higherOrLower = "Lower";
  } else {
   higherOrLower = "Higher";
  }
}

最后,调用drawScreen()在屏幕上进行绘制。

drawScreen();

以下是函数的全部代码。

function eventKeyPressed(e){
   if (!gameOver){
     var letterPressed = String.fromCharCode(e.keyCode);
     letterPressed = letterPressed.toLowerCase();
     guesses++;
     lettersGuessed.push(letterPressed);

     if (letterPressed == letterToGuess){
      gameOver = true;
     } else {

      letterIndex = letters.indexOf(letterToGuess);
      guessIndex = letters.indexOf(letterPressed);
      Debugger.log(guessIndex);
      if (guessIndex < 0){
        higherOrLower = "That is not a letter";
      } else if (guessIndex > letterIndex){
        higherOrLower = "Lower";
      } else {
        higherOrLower = "Higher";
      }
     }
     drawScreen();
     }
   }

下面开始编写drawScreen()函数。之前已经学习过其中的大部分代码了——代码几乎与“Hello World!”中的代码相同。例如,这里使用Canvas文本API在屏幕上绘制多个变量。仅需要设置一次context.textBaseline = 'top',就可以对所有显示的文本生效。另外,还可以使用context.fillStyle改变颜色,使用ontext.font改变字体。

这里最有趣的事就是显示lettersGuessed数组的内容。在画布上,数组将显示为一组用逗号分隔的字符串,例如:

Letters Guessed: p,h,a,d

为了输出这个字符串,只需使用lettersGuessed数组的toString()方法,即可以使用逗号间隔的方式打印出玩家猜到的数组。

context.fillText ("Letters Guessed: " + lettersGuessed.toString(), 10, 260);

接下来,还需检测gameOver变量。如果结果为真,程序在屏幕上使用大字号(40px)显示文本“You Got It!”(你胜利了)。这样,用户就知道自己获胜了。

以下是函数的完整代码。

function drawScreen(){
    //背景
    context.fillStyle = "#ffffaa";
    context.fillRect(0, 0, 500, 300);
    //边框
    context.strokeStyle = "#000000";
    context.strokeRect(5, 5, 490, 290);

    context.textBaseline = "top";
    //日期
    context.fillStyle = "#000000";
    context.font = "10px Sans-Serif";
    context.fillText (today, 150 ,10);
    //消息
    context.fillStyle = "#FF0000";
    context.font = "14px Sans-Serif"; 
    context.fillText (message,125,30);
    //猜测的次数
    context.fillStyle = "#109910";
    context.font = "16px Sans-Serif";
    context.fillText ('Guesses: ' + guesses, 215, 50);
    //显示Higher或Lower
    context.fillStyle = "#000000";
    context.font = "16px Sans-Serif";
    context.fillText ("Higher Or Lower: " + higherOrLower, 150,125);
    //猜过的字母
    context.fillStyle = "#FF0000";
    context.font = "16px Sans-Serif";
    context.fillText ("Letters Guessed: " + lettersGuessed.toString(), 

               10, 260);
    if (gameOver){
      context.fillStyle = "#FF0000";
      context.font = "40px _ sans-serif";
      context.fillText ("You Got It!", 150, 180);
    }
  }

之前,本章简要讨论了Canvas对象的toDataUrL()属性。这里将使用这个属性让用户能够随时创建一个游戏画面的图像,类似基于Canvas制作的游戏中的屏幕捕捉功能。

此处需要在HTML页面上创建一个按钮,用户单击该按钮就可以获得屏幕捕捉的图像。下面将这个按钮添加到<form>中,然后赋予其编号createImageData。

<form>
<input type="button" id="createImageData" value="Export Canvas Image">
</form>

在init()函数中,通过document对象的getElementById()方法获得了这个表单元素的参考。然后,使用createImageDataPressed()方法为按钮的“单击”事件设置一个事件处理器。

var formElement = document.getElementById("createImageData");
formElement.addEventListener('click', createImageDataPressed, false);

在canvasApp()函数中,定义createImageDataPressed()函数作为事件处理器。这个函数调用window.open(),并将Canvas.toDataUrl()方法的返回数值传送给窗口。由于这个数据表单是一个有效的.png,因此图像会在一个新窗口中显示。

function createImageDataPressed(e){

  window.open(theCanvas.toDataURL(),"canvasImage","left=0,top=0,width=" +
  theCanvas.width + ",height=" + theCanvas.height +",toolbar=0,resizable=0");
  }

提示

第3章将深入讨论这些过程。

读者可以在本书分发的代码包中的CH1EX4.html文件中找到“猜字母”的最终游戏代码。

“Hello World”和“猜字母”本身都是不错的示例,但是它们都没能回答出“为什么”——究竟为什么要使用HTML5 Canvas?自创立以来,静态的图像和文字就是HTML的领域,那么画布的不同之处在哪里呢?要回答这个问题,需要创建第二个“Hello World”示例。这个示例将介绍画布与HTML上的其他显示方式的最大不同之处:动画。在这个示例中,将为“Hello World”添加一个简单的在屏幕上“淡入淡出”的效果。虽然很简单,但却是进入HTML5 Canvas更高境界的第一步。读者可以在图1-5中看到这个示例的最终效果。

图1-5 HTML5 Canvas中动画显示“Hello World”

为了完成这个程序,还需要设置一些必要的属性。

alpha属性用于保存文字的透明度,在“淡入淡出”文字的时候通过context.globalAlpha方法进行设置。当设置为0时,文字将完全不可见。本书将在后面的章节中做更详细的解释。

fadeIn属性用于在程序标识文字是淡入还是淡出。

text属性保存用于显示的字符串。

helloWorldImage属性保存显示在动画文字后面的背景图片。

var alpha = 0;
var fadeIn = true;
var text = "Hello World";
var helloWorldImage = new Image();
helloWorldImage.src = "html5bg.jpg";

requestAnimationFrame()

创建一个动画循环的最好办法是采用全新的window.requestAnimationFrame()方法。

这种新方法使用一个变化的计时器,可以让 JavaScript 程序获知浏览器为渲染一个新的动画帧做好准备的确切时间。代码如下:

然而,这个方法还正在变动,而且不是所有的浏览器都支持。因此,本书将在所有程序中使用window.setTimeout()方法。

为了使画布上的每一样东西都动起来,还需要一个“动画循环”。“动画循环”是一个函数,每隔一定时间就会被一遍又一遍地重复调用它。这个函数被用于清除画布的内容,然后在画布上重新绘制更新后的图像、文字视频和其他绘画对象。

创建动画间隔最简单的方法是使用一个setTimeout()循环。要做到这一点,首先要创建一个名为gameLoop()的函数(也可以叫任何喜欢的名字),在这个函数中使用window.setTimeout()方法在指定的时间周期后调用其自身。对于应用程序,时间间隔是20ms。此函数首先对自己进行重置,准备好20ms后再次调用,然后调用drawScreen()函数。

通过这种方式,drawScreen()函数每隔 20ms 就会被调用一次。将所有的绘制代码放在drawScreen()函数中。这种方式与使用setInterval()函数的效果相同,只不过这种方式每次会清除自己,不会永远运行下去,这样对性能更好。

window.requestAnimFrame = (function() {
   return window.requestAnimationFrame   ||
      window.webkitRequestAnimationFrame ||
      window.mozRequestAnimationFrame  ||
      window.oRequestAnimationFrame   ||
      window.msRequestAnimationFrame   ||
      function( callback ){
       window.setTimeout(callback, 1000 / 60);
      };
   })();
(function animloop(){
   requestAnimFrame(animloop);
   render();
  })();
(以上代码的原始作者是Paul Irish)
function gameLoop() {
  window.setTimeout(gameLoop, 20);
  drawScreen()
}
gameLoop();

之所以选择使用context.globalAlpha属性设置动画是因为它非常容易解释,非常适合在Canvas上制作一个动画效果的展示。context.globalAlpha属性用于在画布上设置透明度。这个属性可以接受0~1之间的数字,表示设置给属性之后绘制对象的不透明度的百分比。例如:

context.globalAlpha = 0;

上面的代码将会设置后续渲染的对象的不透明度为0%,即完全透明。

context.globalAlpha = 1;

上面的代码将会设置后续渲染的对象为100%不透明,即透明度是0%。

context.globalAlpha = .5;

上面的代码将会设置后续渲染的对象为50%不透明,即50%透明。

通过在循环中修改这些值,就可以在画布上绘制出淡入淡出的效果。

提示

context.globalAlpha属性会影响后续绘制的所有对象。因此,如果不希望在绘制对象时使用上一个对象绘制时的context.globalAlpha属性值,则需要在将对象绘制在画布上之前重置该属性的值。

在drawScreen()函数每隔20ms被调用一次,用户需要在其中重新绘制画布以更新动画。

由于程序中是通过globalAlpha属性来改变绘制对象的透明度,因此必须确保在绘制操作开始前重置该属性。将context.globalAlpha属性设置为1,然后绘制背景(一个黑色矩形)。接下来,将context.globalAlpha设置为.25,然后绘制已经加载的helloWorldImage图片。图片将按照25%不透明度显示,黑色背景可以透过图片显示出来。

function drawScreen() {
  //背景
 context.globalAlpha = 1;
 context.fillStyle = "#000000";
 context.fillRect(0, 0, 640, 480);
  //图片
 context.globalAlpha = .25;
 context.drawImage(helloWorldImage, 0, 0);

由于本示例中的动画是由画布上文字的淡入和淡出组成的,因此在drawScreen()函数中的主要操作是根据fadeIn属性更新alpha的值。如果文字正在淡入(fadeIn为true),那么将alpha的值每次增加.01。如果alpha的值大于1(能够接受的最大值),那么将它重置为1,然后将fadeIn设置为false(这意味文字开始淡出)。如果fadeIn为false,那么进行相反的操作,当alpha属性为0时将fadeIn设置为true。在设置alpha属性的值之后,通过设置context.globalAlpha属性,将它应用到画布上。

if (fadeIn) {
  alpha += .01;
  if (alpha >= 1) {
    alpha = 1;
    fadeIn = false;
  }
} else {
  alpha -= .01;
  if (alpha < 0) {
    alpha = 0;
    fadeIn = true;
  }
}
context.globalAlpha = alpha;

最后,在画布上绘制文字,drawScreen()函数就完成了。20ms后,drawScreen()函数会再次调用,alpha的值会被更新,文字也会被重新绘制。

  context.font = "72px Sans-Serif";
  context.textBaseline = "top";
  context.fillStyle = "#FFFFFF";
  context.fillText (text, 150,200);
}

本示例的完整代码如下。

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CH1EX5 : Hello World Animated </title>

<script src="modernizr.js"></script>
<script type="text/javascript">
window.addEventListener("load", eventWindowLoaded, false);

function eventWindowLoaded () {
  canvasApp();
}

function canvasSupport () {
   return Modernizr.canvas;
}
function canvasApp () {

     if (!canvasSupport()) {
       return;
     }

   var theCanvas = document.getElementById("canvasOne");
   var context = theCanvas.getContext("2d");

    function drawScreen() {
     //背景
     context.globalAlpha = 1;
     context.fillStyle = "#000000";
      context.fillRect(0, 0, 640, 480);
     //图像
     context.globalAlpha = .25;
     context.drawImage(helloWorldImage, 0, 0);

     if (fadeIn) {
       alpha += .01;
       if (alpha >= 1) {
         alpha = 1;
         fadeIn = false;
       }
     } else {
       alpha -= .01;
       if (alpha < 0) {
         alpha = 0;
         fadeIn = true;
       }
     }

     //text
     context.font = "72px Sans-Serif";
     context.textBaseline = "top";

     context.globalAlpha = alpha;
     context.fillStyle = "#FFFFFF";
     context.fillText (text, 150,200);
   }
   var text = "Hello World";
   var alpha = 0;
   var fadeIn = true;
   //image
   var helloWorldImage = new Image();
   helloWorldImage.src = "html5bg.jpg";

   function gameLoop() {
     window.setTimeout(gameLoop, 20);
     drawScreen()
   }

   gameLoop();
}

</script>
</head>
<body>
<div style="position: absolute; top: 50px; left: 50px;">
<canvas id="canvasOne" width="640" height="480">
 Your browser does not support HTML 5 Canvas.
</canvas>
</div>
</body>
</html>

目前,用于实现画布无障碍访问的方法被称为“后备DOM概念”或者“子dom”(其中包括将文字直接加入的<canvas></canvas>标签中)。

众所周知,由于HTML5 Canvas是一个采用即时模式进行位图映射的屏幕区域,因此并不适合实现无障碍访问。在Canvas中,没有任何固定的DOM元素或显示列表可以帮助无障碍设备(比如屏幕阅读器)搜索画布上绘制的文字、图像以及它们的属性。为了使得Canvas可以无障碍访问,建议使用一种被称为“后备DOM”的概念,有时也被称为“子dom”。利用这种方法,开发者创建一个Dom元素使其匹配Canvas上的每一个元素,然后将其放入子dom中。

在创建的第一个“Hello World”的Canvas示例中(参见CH1EX3.html),文本“Hello World!”显示在一张背景图上(见图 1-3)。如果为这个示例创建一个子dom,可以这样做:

<canvas id="canvasOne" width="500" height="300">
<div>A yellow background with an image and text on top:
  <ol>
    <li>The text says "Hello World"</li>
    <li>The image is of the planet earth.</li>
    </ol>
  </div>
</canvas>

制作一个可以无障碍访问的标题替换下面的代码:

<title>Ch1Ex6: Canvas Sub Dom Example </title>

将它改为:

<title>Chapter 1 Example 6 Canvas Sub Dom Example </title>

为了测试这个页面,还需要一个屏幕阅读器(或者一个屏幕阅读器的模拟器)。Fangs是一个Firefox的屏幕模拟器插件,它可以在打开网页时将屏幕阅读器能够读出的文字列出来,这样就可以帮助用户进行无障碍的调试了。安装这个插件后,在页面上点击鼠标右键,选择“View Fangs”选项就可以看到屏幕阅读器是如何查看网页的了。

对于刚刚创建的Canvas页面,Fangs会显示,页面会按照下面的文字进行朗读:“Chapter one Example six Canvas Sub Dom Example dash Internet Explorer A yellow background with an image and text on top List of two items one The text says quote Hello World quote two The image is of the planet earth.List end.”

对于Google Chrome浏览器,可以选择Google Chrome的扩展程序Chrome Vox。这个工具会尝试朗读页面上所有的内容。

完整示例请参考本书代码中的CH1EX6.html。

点击测试的提案

如果尝试在画布上制作更复杂的效果,而不仅仅是一个简单的动画,那么就会很快发现子dom是一个相当笨拙的办法。为什么呢?因为将后备元素与Canvas的交互效果相关联并不一个轻松的任务。对于屏幕阅读器来说,它需要知道画布上每个元素的确切位置才能进行解读,并且这个过程相当复杂。

为了解决这个问题,需要一些方法将子dom元素与画布上的位图区域相关联。新的W3C Canvas点击测试的提案中对于为什么将此类功能添加到Canvas规范做了以下阐述:

在目前的HTML5规范中,开发者被建议在canvas元素中创建一个后备DOM,使屏幕阅读器能够与画布的用户界面交互。由于这些元素的大小和位置都没有定义,因此会导致无障碍工具出现一些问题。例如,这些工具应该如何汇报这些元素的大小和位置?

因为画布元素需要经常响应用户的输入操作,所以使用同一种机制处理点击测试和无障碍访问的问题似乎是一个明智的决定。

(1)他们在暗示一种什么机制?

这个想法似乎是在创建两个新的方法,setElementPath(element)和clearElementPath(element)。这将允许程序员定义(和删除)的画布上的一个区域,该区域可以作为点击区域使用,并与画布的后备DOM元素关联。这样看来,必须为setElementPath()方法提供一个可访问的后备DOM元素,才能将之与点击测试相关联。当一个点击被检测到,就会触发一个事件,整个机制就是这样运作的。

(2)这对开发者意味着什么?

对于相对固定的用户界面,这会使事情变得更容易。有人曾经有多少次,想为游戏界面上的按钮点击创建一个简单的方法?但是最终,不得不使用与处理游戏精灵交互时用到的点击检测流程相同的方法(笔者每一次都不得不这么做)。然而,对于移动游戏中的精灵来说,这个方法没什么帮助。用户不得不在每次精灵移动时调用setElementPath()方法,并更新后备DOM元素的坐标。对于一个游戏来说,这意味着3倍的开销。显然,此时无障碍化的优先级不能可能排在第一位。

现在,读者应该已经基本理解了HTML和JavaScript是如何呈现并控制HTML5 Canvas和HTML页面的。下一章将介绍更多内容,并使用这些知识创建一个互动应用程序。在这个应用程序中,读者可以用画布在屏幕上显示信息。


HTML5 Canvas的使用是以强大的绘图、着色和基本二维形状变换为基础的。然而,可供选择的内建形状相对有限,程序员可以通过一组称作路径的线段来绘制出想要的形状。2.4节将涉及相关内容。

提示

可以采用在线形式很好地了解HTML5 Canvas API。WSC网站上有详尽且不断更新的参考,具体地描述了Canvas 2D绘图API的功能。
然而,这个在线参考缺少使用API的具体例子。本书避免简单地介绍各个参数的使用,将花时间通过创建示例来解释并探索尽可能多的功能。

如同开始讲到绘图API时那样,本章中的示例也将使用相同的基本文件设置。这段代码为本书所有示例的基础,使用时只需改变drawScreen()函数的内容即可,具体如下。

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ch2BaseFile - Template For Chapter 2 Examples</title>

<script src="modernizr.js"></script>
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
function eventWindowLoaded(){

  canvasApp();
}

function canvasSupport (){
   return Modernizr.canvas;
}

function canvasApp(){

if (!canvasSupport()){
     return;
   }else{
   var theCanvas = document.getElementById("canvas");
   var context = theCanvas.getContext("2d");
  }

  drawScreen();

  function drawScreen(){
   //改变这里
   context.fillStyle = '#aaaaaa';
   context.fillRect(0, 0, 200, 200);
   context.fillStyle = '#000000';
   context.font = '20px _ sans';
   context.textBaseline = 'top';
   context.fillText ("Canvas!", 0, 0);

  }
}

</script>

</head>
<body>
<div style="position: absolute; top: 50px; left: 50px;">
<canvas id="canvas" width="500" height="500">
Your browser does not support HTML5 Canvas.
</canvas>
</div>
</body>
</html>

首先,从最简单、最原始的Canvas内建的几何形状——矩形开始。在Canvas上,绘制基本矩形有3种不同的方式:填充、描边和清除。创建矩形(或者其他形状)还可以使用路径,下节将会讲到。

实现这3种操作的API函数如下。

fillRect(x,y,width,height)

在位置(x, y)处以宽为width、高为height绘制一个填充的矩形。

strokeRect(x,y,width,height)

在位置(x, y)处以宽为width、高为height绘制一个矩形边框。它需要使用当前的strokeStyle、lineWidth、lineJoin以及miterLimit设置。

clearRect(x,y,width,height)

在位置(x, y)处以宽为width、高为height,清除指定区域并使其完全透明(使用透明黑作为颜色)。

在使用这些功能之前,需要预先设置好Canvas绘图时所需的填充或描边的样式。

设置这些样式最基本的方法就是使用24位的十六进制字符串。下面是第一个演示的示例。

context.fillStyle = '#000000';
context.strokeStyle = '#ff00ff';

在例2-1中,填充样式简单设为RGB黑色,描边样式设为传统的紫色,结果如图2-1所示。

例2-1 基本矩形

function drawScreen(){
   context.fillStyle = '#000000';
   context.strokeStyle = '#ff00ff';
   context.lineWidth = 2;
   context.fillRect(10,10,40,40);
   context.strokeRect(0, 0,60,60);
   context.clearRect(20,20,20,20);
}

图2-1 基本矩形

在Canvas环境中绘图时,可以利用所谓的绘图堆栈状态。每个状态随时存储Canvas上下文数据。下面是存储在状态堆栈的数据列表。

——globalAlpha
——globalCompositeOperation
——strokeStyle
——textAlign, textBaseline
——lineCap, lineJoin, lineWidth, and miterLimit
——fillStyle
——font
——shadowBlur, shadowColor, shadowOffsetX, and shadowOffsetY

本章稍后会讲到这些状态。

当前路径(本章稍后将探讨)和当前位图(参见第4章)受Canvas环境控制,不属于保存的状态。这个重要的功能允许在画布上对单个对象进行绘画和制作动画。2.7节将初始化Canvas状态,以将变换应用到当前建立和绘制的形状,同时保持画布其他部分不变。

保存(推送)当前状态到堆栈,调用以下函数。

context.save()

调出最后存储的堆栈恢复画布,使用以下函数。

context.restore()

路径可以用来在画布上绘制任何形状。路径就是一系列点以及这些点之间的连线。Canvas环境只能有一个“当前”路径,当调用context.save()方法时,不会将它储存为当前绘图状态的一部分。

路径的环境是一个需要读者理解的重要概念,因为它决定了只能变换画布上的当前路径。

调用beginPath()函数开始一个路径,调用closePath()函数结束一个路径。连接路径内的两个点的路径称为子路径。如果终点与起点相连,子路径即成为封闭路径。

提示

当前变换矩阵将影响路径中绘制的任何内容。正如将在下节中看到的,如果不想对路径应用变换,可以将变换矩阵设置为恒等(或复位)。

最基本的路径通过moveTo()命令和lineTo()命令控制,如例2-2所示。

例2-2 简单直线路径

function drawScreen(){
  context.strokeStyle = "black"; 
  context.lineWidth = 10;
  context.lineCap = 'square';
  context.beginPath();
  context.moveTo(20, 0);
  context.lineTo(100, 0);
  context.stroke();
  context.closePath();
}

图2-2为这个示例的输出效果。

图2-2 简单直线路径

例2-2简单地绘制了一个10像素宽的水平线(或笔画),从位置(20,0)到(100,0)。

此处添加了lineCap属性和strokeStyle属性。在继续学习更高级的绘制前,首先简单介绍可以应用在直线上的不同属性。context.stroke(); 方法将最终完成、画出构建的路径。

1.lineCap属性

lineCap定义上下文中线的端点,可以有以下3个值。

2.lineJoin属性

lineJoin定义两条线相交产生的拐角,可将其称为连接。在连接处创建一个填充三角形,可以使用lineJoin设置它的基本属性。

3.线宽

lineWidth定义线的宽度(默认值为1.0)。

4.笔触样式

strokeStyle定义线和形状边框的颜色和样式(见例2-2所示的简单矩形)。

例2-3展示了这些属性所起的作用,结果如图2-3所示。在画布上绘制线段时,会有一些奇怪的现象发生,本书将在讲解过程中详细指出。

例2-3 线段终点和连接

function drawScreen(){

   // 实例1: 圆形端点,斜角连接,在画布左上角
   context.strokeStyle = "black"; 
   context.lineWidth = 10;
   context.lineJoin = 'bevel';
   context.lineCap = 'round';
   context.beginPath();
   context.moveTo(0, 0);
   context.lineTo(25, 0);
   context.lineTo(25,25);
   context.stroke();
   context.closePath();

   // 实例2: 圆形端点,斜角连接,不在画布左上角
   context.beginPath();
   context.moveTo(10, 50);
   context.lineTo(35, 50);
   context.lineTo(35,75);
   context.stroke();
   context.closePath();

   // 实例3: 平直端点,圆形连接,不在画布左上角
   context.lineJoin = 'round';
   context.lineCap = 'butt';
   context.beginPath();
   context.moveTo(10, 100);
   context.lineTo(35, 100);
   context.lineTo(35,125);
   context.stroke();
   context.closePath();
  }

图2-3 线段端点和连接

这3个线段和连接的实例有助于说明在画布上绘制线段时不同属性的组合。

实例1尝试从画布左上角开始绘制,结果发生了一个奇怪的现象。Canvas路径在x轴和y轴方向上都绘制到了起点的外侧。由于这个原因,实例1上面的线看起来比设置的10像素要细些。另外,左上角水平部分中圆形端点也无法看到,它们都被绘制到了屏幕之外的负值坐标区域。此外,lineJoin定义的对角线斜角也没有绘出。

实例2调整了例子1中出现的问题,将起始点离开左上角。这样就绘制出了完整的水平线,并且圆形lineCap和斜角lineJoin都被绘制出来了。

实例3显示了去掉lineCap设置后的默认端点效果,并且将lineJoin调整为圆角。

接下来,深入探讨一下其他绘制路径的方法,包括弧线和曲线,以组合成复杂的图像。

有4种函数可以绘制弧线和曲线。弧线可以是一个整圆,也可以是圆的任何一部分。

1.context.arc()

context.arc()的使用方法如下:

context.arc(x, y, radius, startAngle, endAngle, anticlockwise)

xy定义圆心的位置,radius定义弧线的半径。startAngle和endAngle使用弧度值,而不是角度值。anticlockwise的值可以是true或false,用于定义弧线的方向。例如,绘制一个圆心在(100,100),半径为20的圆(见图2-4),可以使用下面这段drawScreen()代码。

context.arc(100, 100, 20, (Math.PI/180)*0, (Math.PI/180)*360, false);

图2-4 圆

例2-4显示了创建一个简单圆形所必须的代码。

例2-4 圆弧

function drawScreen(){
   context.beginPath();
   context.strokeStyle = "black";
   context.lineWidth = 5;
   context.arc(100, 100, 20, (Math.PI/180)*0, (Math.PI/180)*360, false);

   //整圆
   context.stroke();
   context.closePath();
}

注意,这里应将起始角度(0)和结束角度(360)乘以(Math.PI/180)来将其转换为弧度。使用0和360可以创建一个完整的圆形。

如果不以0和360作为起止点,则可以绘制出一段圆弧。下面这段drawScreen()代码将按顺时针方向画1/4个圆,如图2-5所示。

context.arc(100, 200, 20, (Math.PI/180)*0, (Math.PI/180)*90, false);

图2-5 1/4个圆

如果要画0~90以外的部分(见图2-6),可以将anticlockwise值设置为true。

context.arc(100, 200, 20, (Math.PI/180)*0, (Math.PI/180)*90, true);

图2-6 3/4个圆

2.context.arcTo()

context.arcTo()的使用方法如下:

context.arcTo(x1, y1, x2, y2, radius)

只有最新的一些浏览器支持arcTo方法——或许是因为其能力可以被arc()函数替代的原因。这个函数以给定的半径绘制一条弧线,圆弧的起点与当前路径的位置到(x1,y1)点的直线相切,圆弧的终点与(x1,y1)点到(x2,y2)的直线相切。

context.arcTo方法要求当前路径至少有一个子路径。那么,如果从(0,0)到(100,200)画一条直线,然后创建一条小弧线,这看起来会有点像一个钢丝衣架(没有更好的比喻了),如图2-7所示。

context.moveTo(0,0);
context.lineTo(100, 200);
context.arcTo(350,350,100,100,20);

图2-7 arcTo()示例

贝塞尔曲线要比弧线灵活得多,它有立方和平方两种形式:

在二维空间中,贝塞尔曲线通过“起点”、“终点”以及两个决定曲线走向的“控制”点定义完成。一个普通的立方贝塞尔曲线使用两个点,平方贝塞尔曲线使用一个点。平方贝塞尔曲线是最简单的曲线(见图2-8),只需要终点以及一个控制点即可完成绘制。

context.moveTo(0,0);
context.quadraticCurveTo(100,25,0,50);

图2-8 简单平方贝塞尔曲线

这条曲线从(0,0)开始,到(0,50)结束。用来创建弧线的点位于(100,25),这个点大致上是圆弧的圆心。控制点的x值为100,把弧线拉伸成一个细长的曲线。

立方贝塞尔曲线具有两个控制点,因此带来了更多的选择,从而能够很容易地绘制出经典的“S”形曲线,如图2-9所示。

context.moveTo(150,0);
context.bezierCurveTo(0,125,300,175,150,300);

图2-9 有两个控制点的贝塞尔曲线

也可以将其他Canvas方法配合裁切区域使用,最常见的是arc()函数。

可以创建一个圆形的裁切区域,而不是矩形的裁切区域。

使用Canvas裁切区域可以限制路径及其子路径的绘制区域。首先,通过rect()函数设置一个用来绘图的矩形区域的环境属性;然后,调用clip()函数把用rect()函数定义的矩形设置为裁切区域。现在,无论在当前环境绘制什么内容,它只显示裁切区域以内的部分。这也可以理解成是绘图操作的一种蒙版。例2-5展示了它是如何工作的,裁切结果如图2-10所示。

arc(float x, float y, float radius, float startAngle, 
float endAngle, boolean anticlockwise) 

本例将通过执行 save()和 restore() 函数来实现在红色圆圈周围进行剪裁。如果不这样做,就无法画出蓝色圆。读者不妨把例2-5中的save()and restore()行注释去掉,自己测试一下。

例2-5 Canvas裁切区域

function drawScreen(){

   //在屏幕上绘制一个大方块
   context.fillStyle = "black";
   context.fillRect(10, 10, 200, 200);
   context.save();
   context.beginPath();

   //裁切画布从(0,0)点至50×50的正方形
   context.rect(0, 0, 50, 50);
   context.clip();

   //红色圆
   context.beginPath();
   context.strokeStyle = "red"; 
   context.lineWidth = 5;
   context.arc(100, 100, 100, (Math.PI/180)*0, (Math.PI/180)*360, false);
   //整圆
   context.stroke();
   context.closePath();

   context.restore();

   //再次裁切整个画布
   context.beginPath();
   context.rect(0, 0, 500, 500);
   context.clip();

   //绘制一个没有裁切的蓝线
   context.beginPath();
   context.strokeStyle = "blue";
   context.lineWidth = 5;
   context.arc(100, 100, 50, (Math.PI/180)*0, (Math.PI/180)*360, false);
   //整圆
   context.stroke();
   context.closePath();
}

图2-10 Canvas裁切区域

例2-5首先在画布上画了一个200×200的黑色矩形,然后将Canvas裁切区域设置为rect(0,0,50,50)。clip()函数按照这些参数对画布进行裁切设置,所画的红色圆弧形只能看到在这个矩形以内的部分。最后,将裁切区域调整回rect(0,0,500,500),画一个新的蓝色圆弧。此时,整个圆都能够看到。

提示

合成是指如何精细控制画布上对象的透明度和分层效果。有两个属性可以控制Canvas合成操作:globalAlpha和globalCompositeOperation。

在下面的列表中,“源图形”是指要绘制在画布上的形状,“目标图形”是指显示在画布上的位图。

例2-6显示了这些值如何影响形状绘制,结果如图2-11所示。

例2-6 Canvas合成示例

function drawScreen(){

   //在屏幕上绘制一个大方块
   context.fillStyle = "black"; //
   context.fillRect(10, 10, 200, 200);

   //保留 globalCompositeOperation 原有值不变
   //现在绘制一个红色正方形
   context.fillStyle = "red";
   context.fillRect(1, 1, 50, 50);

   //现在设置为source-over
   context.globalCompositeOperation = "source-over";
   //在旁边再画一个红色正方形
   context.fillRect(60, 1, 50, 50);

   //现在设置为destination-atop
   context.globalCompositeOperation = "destination-atop";
   context.fillRect(1, 60, 50, 50);

   //现在设置globalAlpha
   context.globalAlpha = .5;

   //现在设置为source-atop
   context.globalCompositeOperation = "source-atop";
   context.fillRect(60, 60, 50, 50);
}

图2-11 Canvas合成示例

提示

不幸地是,context.globalCompositeOperation = "destinationatop"在浏览器上不能再正常使用了。

如例2-6所示,这里测试了globalCompositeOperation和globalAlpha Canvas属性。当指定字符串为sourceover时,实际上是将globalCompositeOperation重设回默认值。然后创建了几个红色方形来显示不同的合成选项和组合。注意,destination-atop切换到当前Canvas位图之下的新绘制形状,globalAlpha属性只影响设置之后所画的形状。这意味着,不必为绘制新的形状设定新的透明度而使用save()和restore() Canvas对状态进行操作。

本书将在2.7节讲解一些影响整个画布的变换。因此,如果要变换新绘制的形状,必须使用save()和restore()函数。

画布变换是指用数学方法调整所绘形状的物理属性。缩放和旋转是常用的两个形状变换,本节将做专门讨论。

所有变换都依赖于后台的数学矩阵运算。幸运的是,读者只需使用变换功能而不必去理解这些运算。接下来,本书将讨论如何通过调整Canvas属性来应用旋转和缩放变换。

首先指定画布上的对象面向左侧时处于角度为0的旋转状态(如果对象有“面”,则很重要;如果对象没有“面”,也可将其作为参照)。例如,画一个方框(四边等长),它并没有一个与其他边相比而言初始就朝向左侧的“面”。现在,将其画出来作为参考。

//绘制一个红色正方形
context.fillStyle = "red";
context.fillRect(100,100,50,50);

如果将画布旋转45°,那么需要进行以下操作。将Canvas变换设置为identity(或“reset”)矩阵。

context.setTransform(1,0,0,1,0,0);

由于Canvas使用的是弧度,而不是角度,在设定变换时应将45°角转换成弧度。

var angleInRadians = 45 * Math.PI / 180;
context.rotate(angleInRadians);

1.在调用setTransform()或其他变换函数后,将变换应用到形状和路径上

如果照抄这段代码运行,将发生很有趣的运行结果:屏幕上什么也没有!这是因为只有对画布应用setTransform()函数后才对形状起作用。示例中先绘制了一个正方形,然后设置变换属性。这将不会对这个使正方形发生改变(或者变换)。例2-7给出了能产生预想结果的正确代码顺序,结果如图2-12所示。

例2-7 简单旋转变换

function drawScreen(){

   //绘制一个红色正方形
   context.setTransform(1,0,0,1,0,0);
   var angleInRadians = 45 * Math.PI / 180;
   context.rotate(angleInRadians);
   context.fillStyle = "red";
   context.fillRect(100,100,50,50);
  }

图2-12 简单旋转变换

虽然得到了变换结果,但是估计和读者想要的结果会有所不同。虽然红色的方框旋转了,但是好像画布也跟着一起旋转了。实际上,画布并没有旋转,context.rotate()函数调用后只有部分被绘制出来。那么,为什么这个正方形旋转到了屏幕外?因为旋转原点设在了“nontranslated”(0,0)点,所以导致了正方形从整个画布的左上角旋转。

例2-8提供了一个略微不同的场景:先画一个黑色方块,然后设置旋转变换,最后再画这个红色方块。结果如图2-13所示。

例2-8 旋转以及Canvas状态

function drawScreen(){

   //绘制黑色正方形
   context.fillStyle = "black";
   context.fillRect(20,20,25,25);

   //绘制红色正方形
   context.setTransform(1,0,0,1,0,0);
   var angleInRadians = 45 * Math.PI / 180;
   context.rotate(angleInRadians);
   context.fillStyle = "red";
   context.fillRect(100,100,50,50);
  }

图2-13 旋转以及画布状态

这个黑色小方块没受到旋转的影响,因为只有调用context.rotate()函数之后绘制的形状才会受到影响。

同样,红方块移到了左侧之外。重申一下,这是因为画布不知道旋转的原点在哪里而造成的。如果没有平移到实际的原点,Canvas就会认为就在点(0,0),结果导致context.rotate()函数围绕点(0,0)旋转。这将会使读者领会到下面的内容。

2.只有将原点平移至形状中心,对象才会围着自己转

将例2-8加以改变,使红色正方形能在旋转45°的同时保持位置不变。

首先,设置好控制红色正方形属性的fillRect()函数的变量。虽然不必这样做,但是这会使代码更易于阅读和调整。

var x = 100;
var y = 100;
var width = 50;
var height = 50;

接下来,使用context.translate()函数,将画布原点平移到红色正方形的中心点。这个函数可以将画布原点移到(xy)处。这里将原点x坐标值设为红色正方形左上角的x值(100)加上其一半的宽度。使用前面创建的变量即可控制这个红色正方形的属性,如下所示:

x+0.5*width

接下来,确定原点平移的y坐标值。这次使用左上角的y值和形状的高度值,如下:

y+.05*height

translate()函数语句如下:

context.translate(x+.05*width, y+.05*height)

既然画布已经平移到了正确的原点,下面就可以进行旋转变换了,代码不变。

context.rotate(angleInRadians);

最后,绘制出形状。由于画布原点已经移动到将要绘制形状的位置,因此不能简单重复使用例2-8中同样的数值。现在,将(125,125)作为一切绘制操作的原点。125是将正方形左上角的x值(100)加上其一半宽度(25)得来的。y值同上。translate()方法调用完成。

绘制对象需要从正确的左上角坐标值(xy)开始,进而从原点的x值减去宽的一半,从y值减去高的一半。

context.fillRect(-0.5*width,-0.5*height, width, height);

为什么要这样做?如图2-14所示。

图2-14 新建平移点

试想一下,从左上角开始绘制正方形,如果原点在(125,125),左上角实际上是(100,100)。然而原点已经平移过了,也就是说,(125,125)现在相当于(0,0)。如果在未平移的画布上画这个方块,则应从(−25,−25)点开始。

因此,必须把绘制正方形当成从(0,0)开始,而不是从(125,125)开始。实际绘图的时候,必须使用图2-15所示的坐标。

图2-15 基于平移点绘制

小结:变换需要将原点平移到正方形的中心,以使其围绕自己旋转。绘图的时候,需要使代码将(125,125)当作实际的(0,0)点。如果不平移原点,那么也可以使用(125,125)作为正方形的中心,如图2-14所示。例2-9说明了代码如何运行,结果如图2-16所示。

例2-9 围绕中心点旋转

function drawScreen(){

   //绘制黑色正方形
   context.fillStyle = "black";
   context.fillRect(20,20 ,25,25);

   //绘制红色正方形
   context.setTransform(1,0,0,1,0,0);
   var angleInRadians = 45 * Math.PI / 180;
   var x = 100;
   var y = 100;
   var width = 50;
   var height = 50;
   context.translate(x+.5*width, y+.5*height);
   context.rotate(angleInRadians);
   context.fillStyle = "red";
   context.fillRect(-.5*width,-.5*height , width, height);
  }

图2-16 围绕中心点旋转

再看一个旋转的例子。例2-10在例2-9的基础上增加了4个单独的40×40的正方形,每个稍加旋转,结果如图2-17所示。

例2-10 旋转多个正方形

function drawScreen(){

   //绘制一个红色正方形
   context.setTransform(1,0,0,1,0,0);
   var angleInRadians = 45 * Math.PI / 180;
   var x = 50;
   var y = 100;
   var width = 40;
   var height = 40;
   context.translate(x+.5*width, y+.5*height);
   context.rotate(angleInRadians);
   context.fillStyle = "red";
   context.fillRect(-.5*width,-.5*height , width, height);

   context.setTransform(1,0,0,1,0,0);
   var angleInRadians = 75 * Math.PI / 180;
   var x = 100;
   var y = 100;
   var width = 40;
   var height = 40;
   context.translate(x+.5*width, y+.5*height);
   context.rotate(angleInRadians);
   context.fillStyle = "red";
   context.fillRect(-.5*width,-.5*height , width, height);

   context.setTransform(1,0,0,1,0,0);
   var angleInRadians = 90 * Math.PI / 180;
   var x = 150;
   var y = 100;
   var width = 40;
   var height = 40;
   context.translate(x+.5*width, y+.5*height);
   context.rotate(angleInRadians);
   context.fillStyle = "red";
   context.fillRect(-.5*width,-.5*height , width, height);
   context.setTransform(1,0,0,1,0,0);
   var angleInRadians = 120 * Math.PI / 180;
   var x = 200;
   var y = 100;
   var width = 40;
   var height = 40;
   context.translate(x+.5*width, y+.5*height);
   context.rotate(angleInRadians);
   context.fillStyle = "red";
   context.fillRect(-.5*width,-.5*height , width, height);
}

图2-17 旋转多个正方形

context.scale()函数有两个参数:第一个是x轴的缩放属性,第二个是y轴的缩放属性。一个对象的正常缩放大小数值是1。因此,如果要将一个对象放大两倍,就可以将两个参数都设为2。在drawScreen()中使用下面这段代码可以产生一个红色正方形,如图2-18所示。

context.setTransform(1,0,0,1,0,0);
context.scale(2,2);
context.fillStyle = "red";
context.fillRect(100,100 ,50,50);

图2-18 简单缩放正方形

如果测试这段代码,就会发现缩放的工作方式与旋转差不多。由于没有平移原点来对正方形进行缩放,而仍用画布左上角作为画布原点,因此红色的正方形向右下方移动了。如果从正方形的中心缩放,就需要在缩放之前将原点平移到正方形中心,然后再围绕这个中心点绘图(见例2-9)。例2-11的结果如图2-19所示。

例2-11 从中心点缩放

function drawScreen(){

   //绘制一个红色正方形
   context.setTransform(1,0,0,1,0,0);
   var x = 100;
   var y = 100;
   var width = 50;
   var height = 50;
   context.translate(x+.5*width, y+.5*height);
   context.scale(2,2);
   context.fillStyle = "red";
   context.fillRect(-.5*width,-.5*height , width, height);
  }

图2-19 从中心点缩放

如果对对象进行缩放和旋转操作,Canvas变换可以轻松地组合并生成想要的效果,如图2-20所示。例2-12显示了如何在前面的示例中使用scale(2,2)和rotate(angleInRadians)进行组合变换。

图2-20 缩放和旋转组合

例2-12 缩放和旋转组合

function drawScreen(){
   context.setTransform(1,0,0,1,0,0);
   var angleInRadians = 45 * Math.PI / 180;
   var x = 100;
   var y = 100;
   var width = 50;
   var height = 50;
   context.translate(x+.5*width, y+.5*height);
   context.scale(2,2);
   context.rotate(angleInRadians);
   context.fillStyle = "red";
   context.fillRect(-.5*width,-.5*height , width, height);
  }

例2-13也组合了旋转和缩放,这个例子是对矩形进行操作,如图2-21所示。

例2-13 非正方形对象的缩放和旋转

function drawScreen(){

   //绘制一个红色矩形
   context.setTransform(1,0,0,1,0,0);
   var angleInRadians = 90 * Math.PI / 180;
   var x = 100;
   var y = 100;
   var width = 100;
   var height = 50;
   context.translate(x+.5*width, y+.5*height);
   context.rotate(angleInRadians);
   context.scale(2,2);

   context.fillStyle = "red";
   context.fillRect(-.5*width,-.5*height , width, height);
  }

图2-21 非正方形对象的缩放和旋转

找到任何形状的中心

对矩形或其他形状进行旋转和缩放与对正方形非常类似,实际上只要在缩放、旋转或者组合缩放旋转前将原点平移到形状的中心,都可以得到想要的效果。记住,任何形状的中心点都是半宽的x值和半高的y值!这需要使用边界框理论找到中心点。

图2-22说明了这个理论,尽管不是简单形状,也可以找到包含对象任一点的边界框。图2-22接近正方形,但是同样符合矩形的边界框理论。

图2-22 复杂形状的边界框

本章已经在讨论创建基本和复杂形状时,粗略地介绍了颜色和填充样式。本节将深入探讨形状的着色和填充。除了这些简单的着色和填充外,还有很多可用的不同渐变样式。另外,Canvas还可以使用位图来填充形状(参见第4章)。

Canvas fillStyle属性用来设置画布上形状的基本颜色和填充。fillStyle使用简单的颜色名称。这看起来非常简单,例如:

context.fillStyle = "red";

下面是出自HTML4规范的可用颜色字符串值列表。截止到本书出版前,HTML5还没有对此另行设定。由于没有增加HTML5专属的颜色,HTML4的颜色都可以在HTML5中正确显示。

黑色  Black = #000000
绿色  Green = #008000
银色  Silver = #C0C0C0
石灰色 Lime = #00FF00
灰色  Gray = #808080
橄榄色 Olive = #808000
白色  White = #FFFFFF
黄色  Yellow = #FFFF00
栗色  Maroon = #800000
海蓝色 Navy = #000080
红色  Red = #FF0000
蓝色  Blue = #0000FF
紫色  Purple = #800080
深蓝绿色  Teal = #008080
紫红色 Fuchsia = #FF00FF
浅蓝绿色  Aqua = #00FFFF

提示

所有这些颜色值都可以应用到strokeStyle属性和fillStyle属性中。

当然,使用颜色名称字符串并不是指定一个纯填充的唯一方法。以下是其他一些方法。

(1)以rgb()方法设置填充色。

rgb()方法可以用24位RGB值指定填充色。

context.fillStyle ="rgb(255,0,0)";

这与上面使用red字符串设定的红色是一样的。

(2)以十六进制数字字符串设置填充色。

也可以使用一个十六进制数字字符串设置fillStyle颜色。

context.fillStyle = "#ff0000";

(3)以rgba()方法设置填充色。

rgba()方法可以指定32位色值的填充色,其后8位表示透明度。

context.fillStyle = "rgba(255,0,0,1) ";

透明度范围为1(不透明)~0(透明)。

在画布上创建渐变填充有两个基本选项:线性或径向。线性渐变创建一个水平、垂直或者对角线的填充图案。径向渐变自中心点创建一个放射状填充。下面是它们的一些示例。

1.线性渐变

线性渐变有3种基本样式:水平、垂直和对角线。

(1)线性水平渐变:通过沿对象设置的颜色断点来控制渐变颜色。

例2-14为创建一个简单水平渐变,如图2-23所示。

例2-14 线性水平渐变

function drawScreen(){

   // 水平渐变值必须保持为0
   var gr = context.createLinearGradient(0, 0, 100, 0);

   // 添加颜色断点
   gr.addColorStop(0,'rgb(255,0,0)');
   gr.addColorStop(.5,'rgb(0,255,0)');
   gr.addColorStop(1,'rgb(255,0,0)');

   // 应用fillStyle生成渐变
   context.fillStyle = gr;
   context.fillRect(0, 0,100,100);
  }

图2-23 线性水平渐变

创建一个水平渐变,必须先创建一个变量(gr)来指代一个新的渐变,设置方式如下。

var gr = context.createLinearGradient(0,0,100,0);

createLinearGradient方法调用中的4个值是开始渐变的左上角的x坐标和y坐标,以及结束渐变的右下角的x坐标和y坐标。示例从(0,0)开始,到(100,0)结束。请注意,当创建一个水平渐变的时候,y值都是0,创建垂直渐变的时候正好相反。

一旦定义了渐变的大小,就需要使用两个参数值来加入颜色断点。第一个是相对位置的开始渐变颜色的渐变原点,第二个是渐变的颜色。相对位置的值必须在0.0~1.0之间。

gr.addColorStop(0,'rgb(255,0,0)');
gr.addColorStop(.5,'rgb(0,255,0)');
gr.addColorStop(1,'rgb(255,0,0)');

例2-14中设置的渐变是起点(0)为红色、中心点(0.5)为绿色、终点(1)为红色。这将填充一个“红—绿—红”渐变形状。

接下来,应用context.fillStyle生成刚才创建的渐变。

context.fillStyle = gr;

最后,在画布上创建一个矩形。

context.fillRect(0, 0, 100, 100);

请注意,刚刚创建了一个与渐变大小完全相同的矩形。这里还可以改变输出矩形的大小,方法如下。

context.fillRect(0, 100, 50, 100);
context.fillRect(0, 200, 200, 100);

在例2-14的基础上,例2-15增加了两个新的填充矩形,如图2-24所示。请注意,渐变充满可用空间,最终的颜色填充的区域比定义的渐变区域要大。

例2-15 多个渐变填充对象

function drawScreen(){

   var gr = context.createLinearGradient(0, 0, 100, 0);

   // 添加颜色断点
   gr.addColorStop(0,'rgb(255,0,0)');
   gr.addColorStop(.5,'rgb(0,255,0)');
   gr.addColorStop(1,'rgb(255,0,0)');

   // 应用fillStyle生成渐变
   context.fillStyle = gr;
   context.fillRect(0, 0, 100, 100);
   context.fillRect(0, 100, 50, 100);
   context.fillRect(0, 200, 200, 100);
    }

图2-24 多对象线性水平渐变

① 在边框上应用水平渐变色。

渐变可以应用到任何形状,甚至是形状边框。例2-16使用例2-15中的填充矩形创建了一个strokeRect形状,而不是创建填充矩形。图2-25显示了不同的结果。

例2-16 水平描边渐变

function drawScreen(){

   var gr = context.createLinearGradient(0, 0, 100, 0);

   // 添加颜色断点
   gr.addColorStop(0,'rgb(255,0,0)');
   gr.addColorStop(.5,'rgb(0,255,0)');
   gr.addColorStop(1,'rgb(255,0,0)');
   // 将水平渐变应用到描边
   // 应用fillStyle生成渐变
   context.strokeStyle = gr;
   context.strokeRect(0, 0, 100, 100);
   context.strokeRect(0, 100, 50, 100);
   context.strokeRect(0, 200, 200, 100);
  }

图2-25 水平描边渐变

② 在复杂形状上应用水平渐变色。

读者也可以将线性渐变应用到由点组成的封闭形状,如例2-17所示。如果形状的起止点相同,就是封闭的。

例2-17 复杂形状水平渐变

function drawScreen(){

   var gr = context.createLinearGradient(0, 0, 100, 0);

   //添加颜色断点
   gr.addColorStop(0,'rgb(255,0,0)');
   gr.addColorStop(.5,'rgb(0,255,0)');
   gr.addColorStop(1,'rgb(255,0,0)');

   // 应用fillStyle生成渐变
   context.fillStyle = gr;
   context.beginPath();
   context.moveTo(0,0);
   context.lineTo(50,0);
   context.lineTo(100,50);
   context.lineTo(50,100);
   context.lineTo(0,100);
   // 将水平渐变应用到复杂形状
   context.lineTo(0,0);
   context.stroke();
   context.fill();
   context.closePath();
  }

例2-17使用了context.fill()命令将当前的fillStyle填充到形状中,输出效果如图2-26所示。

图2-26 复杂形状水平渐变

图2-26显示了通过点创建的新形状,只要点是封闭的,填充就会按需要呈现。

(2)垂直渐变色:垂直渐变与水平渐变的创建方式非常类似。不同点在于:y值不全是0,而x值必须全是0。

例2-18显示了例2-17中创建的水平渐变形状显示为垂直渐变的情况,输出结果如图2-27所示。

例2-18 垂直渐变

function drawScreen(){

   var gr = context.createLinearGradient(0, 0, 0, 100);

   //添加颜色断点
   gr.addColorStop(0,'rgb(255,0,0)');
   gr.addColorStop(.5,'rgb(0,255,0)');
   gr.addColorStop(1,'rgb(255,0,0)');

   // 应用fillStyle生成渐变
   context.fillStyle = gr;
   context.beginPath();
   context.moveTo(0,0);
   context.lineTo(50,0);
   context.lineTo(100,50);
   context.lineTo(50,100);
   context.lineTo(0,100);
   context.lineTo(0,0);
   context.stroke();
   context.fill();
   context.closePath();
  }

图2-27 垂直渐变示例

例2-18和例2-17的唯一不同之处在于创建线性渐变的线。

水平渐变(例2-17)如下所示。

var gr = context.createLinearGradient(0, 0, 100, 0);

垂直渐变(例2-18)如下所示。

var gr = context.createLinearGradient(0, 0, 0, 100);

形状边框水平渐变的规则同样适用于垂直渐变。例2-19将例2-18中的形状从填充改成描边,边框效果如图2-28所示。

例2-19 垂直渐变描边

function drawScreen(){

   var gr = context.createLinearGradient(0, 0, 0, 100);

   //添加颜色断点
   gr.addColorStop(0,'rgb(255,0,0)');
   gr.addColorStop(.5,'rgb(0,255,0)');
   gr.addColorStop(1,'rgb(255,0,0)');

   //应用fillStyle生成渐变
   context.strokeStyle = gr;
   context.beginPath();
   context.moveTo(0,0);
   context.lineTo(50,0);
   context.lineTo(100,50);
   context.lineTo(50,100);
   context.lineTo(0,100);
   context.lineTo(0,0);
   context.stroke();
   context.closePath();
  }

图2-28 垂直渐变描边

(3)对角线渐变:读者可以轻松创建一个对角线渐变,只需修改createLinearGradient()函数的第二个x值和y值。

var gr= context.createLinearGradient(0, 0, 100, 100);

为创建一个图2-29所示的完美对角线渐变,可以填充一个与对角线渐变相同大小的正方形。代码如例2-20所示。

例2-20 对角线渐变

function drawScreen(){

   var gr = context.createLinearGradient(0, 0, 100, 100);

   //添加颜色断点
   gr.addColorStop(0,'rgb(255,0,0)');
   gr.addColorStop(.5,'rgb(0,255,0)');
   gr.addColorStop(1,'rgb(255,0,0)');

   //应用fillStyle生成渐变
   context.fillStyle = gr;
   context.beginPath();
   context.moveTo(0,0);
   context.fillRect(0,0,100,100)
   context.closePath();
  }

图2-29 对角线渐变示例

2.径向渐变

径向渐变的定义过程和线性渐变非常类似。尽管径向渐变需要6个参数设置而线性渐变仅需4个,但是它同样采用颜色断点的概念来创建颜色变化。

6个参数用来定义两个圆的圆心和半径。第一个圆是开始圈,第二个圆是结束圈。举例如下。

var gr = context.createRadialGradient(50,50,25,50,50,100);

第一个圆的圆心位于(50,50),半径为25;第二个圆的圆心位于(50,50),半径为100。

这将会创建两个同心圆,然后向线性渐变那样设置颜色断点。

gr.addColorStop(0,'rgb(255,0,0)');
gr.addColorStop(.5,'rgb(0,255,0)');
gr.addColorStop(1,'rgb(255,0,0)');

例2-21将这些代码合并起来,结果如图2-30所示。

例2-21 简单径向渐变

function drawScreen(){

   var gr = context.createRadialGradient(50,50,25,50,50,100);

   //添加颜色断点
   gr.addColorStop(0,'rgb(255,0,0)');
   gr.addColorStop(.5,'rgb(0,255,0)');
   gr.addColorStop(1,'rgb(255,0,0)');

   //应用fillStyle生成渐变
   context.fillStyle = gr;
   context.fillRect(0, 0, 200, 200);
  }

图2-30 简单径向渐变

例2-22将第二个圆远离第一个圆,产生的效果如图2-31所示。

例2-22 复杂径向渐变

function drawScreen(){

   var gr = context.createRadialGradient(50,50,25,100,100,100);

   //添加颜色断点
   gr.addColorStop(0,'rgb(255,0,0)');
   gr.addColorStop(.5,'rgb(0,255,0)');
   gr.addColorStop(1,'rgb(255,0,0)');

   //应用fillStyle生成渐变
   context.fillStyle = gr;
   context.fillRect(0, 0, 200, 200);
}

图2-31 复杂径向渐变

如同线性渐变一样,复杂形状同样可以进行径向渐变填充。例2-23对本章前面的弧形示例应用了径向渐变,结果如图2-32显示。

例2-23 圆形径向渐变

function drawScreen(){

   var gr = context.createRadialGradient(50,50,25,100,100,100);

   //添加颜色断点
   gr.addColorStop(0,'rgb(255,0,0)');
   gr.addColorStop(.5,'rgb(0,255,0)');
   gr.addColorStop(1,'rgb(255,0,0)');

   //应用fillStyle生成渐变
   context.fillStyle = gr;
   context.arc(100, 100, 100, (Math.PI/180)*0, (Math.PI/180)*360, false);
   context.fill();
  }

图2-32 圆形径向渐变

例2-23将例2-22的径向渐变应用到圆上,而不是应用到矩形上。这样就清除了形状背景的红色正方形。

也可以将径向渐变应用到弧形描边,就像填充那样。代码如例2-24所示,效果如图2-23所示。

例2-24 弧形描边渐变

function drawScreen(){

   var gr = context.createRadialGradient(50,50,25,100,100,100);

   //添加颜色断点
   gr.addColorStop(0,'rgb(255,0,0)');
   gr.addColorStop(.5,'rgb(0,255,0)');
   gr.addColorStop(1,'rgb(255,0,0)');

   //应用fillStyle生成渐变
   context.strokeStyle = gr;
   context.arc(100, 100, 50, (Math.PI/180)*0, (Math.PI/180)*360, false)
   context.stroke();
  }

图2-33 弧形描边渐变

例2-24创建了一个比例2-23中小一点的圆,径向渐变将显示在弧形的边框上。如果与例2-23中的圆大小相同,这里将得到一个纯红色填充,因为径向渐变在圆形直径边缘将是纯红色。

本书将在第4章讲解如何在画布上使用位图,这里仅快速介绍如何将图像用作形状填充图案。

填充图案通过createPattern()函数进行初始化。它有两个参数:第一个是Image对象实例,第二个是String表示在形状中如何显示repeat图案。读者可以使用一个加载图像或者整个画布作为形状的填充图案。

有以下4种图像填充类型:

现代的浏览器对这4种类型的支持程度不同,不过,普遍都能支持标准的repeat类型。先来介绍repeat类型,然后简单介绍其他3称类型。

图2-34显示了一个简单位图填充图案的功能测试,这是一个透明背景上的20×20的绿色圆形,存储为fill_20x20.gif文件。

图2-34 填充用的fill_20x20.gif图像

例2-25测试了使用repeat字符串创建一个充满小圆点的正方形,如图2-35所示。

例2-25 使用repeat填充图像文件

function drawScreen(){

   var fillImg = new Image();
   fillImg.src = 'fill_20x20.gif';
   fillImg.onload = function(){

     var fillPattern = context.createPattern(fillImg,'repeat');
     context.fillStyle = fillPattern;
     context.fillRect(0,0,200,200);
   }
}

图2-35 重复填充示例

如果没有加载完成,最好不要使用Image实例。第4章将会进行详细讨论,现在只需简单创建一个在线onload事件处理器功能,在Image就绪之后就会调用此事件。repeat图案字符串非常好地填充到了200×200的正方形中。repeat字符串代码如例2-26所示,结果如图2-36~图2-38所示。

例2-26 使用no-repeat、repeat-x和repeat-y字符串

function drawScreen(){

   var fillImg = new Image();
   fillImg.src = 'fill_20x20.gif';

   fillImg.onload = function(){

     var fillPattern1 = context.createPattern(fillImg,'no-repeat');
     var fillPattern2 = context.createPattern(fillImg,'repeat-x');
     var fillPattern3 = context.createPattern(fillImg,'repeat-y');

     context.fillStyle = fillPattern1;
     context.fillRect(0,0,100,100);

     context.fillStyle = fillPattern3;
     context.fillRect(0,220,100,100);


     context.franslate(0,110);
     context.fillStyle = fillPattern2;
     context.fillRect(0,0,100,100);
   }
}

提示

每种浏览器显示这些图案的方式都不一样,而且总在变化。所以,在开发时要用新浏览器检验一下。

图2-36 no-repeat、repeat-x和repeat-y在Safari浏览器中的效果

图2-37 no-repeat、repeat-x和repeat-y在Firefox浏览器中的效果

图2-38 no-repeat、repeat-x和repeat-y在Chrome浏览器中的效果

当repeat参数使用repeat-x和repeat-y字符串时,只有Firefox看起来差别明显。第4章将更多地讲解位图填充和其他用法的示例。

请注意,如果要让repeat-x填充模式生效,则需要先平移到所绘制点的xy坐标,然后使用是(0,0)作为fillRect函数的xy坐标的参数。

读者可以使用4个参数给画布上的形状添加阴影。与2.9节所讲的填充图案一样,这项功能还没有被所有兼容HTML5的浏览器完全支持。

可以通过设置以下4个Canvas参数来添加阴影:

shadowOffsetX和shadowOffsetY值可以为正值或负值——负值将会在左侧和上方创建阴影,反之将会在底部和右侧创建阴影。shadowBlur属性用来设置阴影模糊效果的程度。这3个参数都不受当前Canvas变换矩阵影响。shadowColor属性可以是任何HTML4颜色的常量字符串——rgb()或rgba()——或者是十六进制数值字符串。

例2-27和图2-39显示了几个不同阴影效果的方块。

图2-39 给对象添加阴影

例2-27 给对象添加阴影

function drawScreen(){

   context.fillStyle = 'red';

   context.shadowOffsetX = -4;
   context.shadowOffsetY = -4;
   context.shadowColor = 'black';
   context.shadowBlur = 4;
   context.fillRect(10,10,100,100);

   context.shadowOffsetX = -4;
   context.shadowOffsetY = -4;
   context.shadowColor = 'black';
   context.shadowBlur = 4;
   context.fillRect(150,10,100,100);

   context.shadowOffsetX = 10;
   context.shadowOffsetY = 10;
   context.shadowColor = 'rgb(100,100,100)';
   context.shadowBlur = 8;
   context.arc(200, 300, 100, (Math.PI/180)*0, (Math.PI/180)*360, false)
   context.fill();
}

如图2-39所示,如果同时调整shadowOffset值和shadowBlur值可以创建不同的阴影。当然,Canvas还可以为由路径和弧形组成的复杂形状创建阴影。

读者已经在第1章中探索了如何刷新画布,第4章还会更深入地探讨。在本章结束之前,还要介绍一个可用于完全清除画布和刷新内容的例子。

使用一个新的背景色简单地填充整个画布,这样就可以清除当前内容,代码如下:

context.fillStyle = '000000'; 
context.fillRect(0,0,theCanvas.width, theCanvas.height)

当画布的宽或高被重置时,当前画布内容就会被移除,代码如下:

var w=theCanvas.width; 
var h=theCanvas.height; 
theCanvas.width=w; 
theCanvas.height=h;

clearRect() 函数可以指定起始点的xy位置以及宽度和高度来清除画布,代码如下:

var w=theCanvas.width; 
var h=theCanvas.height; 
context.clearRect(0,0,w,h);

尝试使用clearRect() 函数来绘制一个路径横跨画布的动画(见例 2-28)。通过使用第1章介绍的 setTimeOut()函数来完成动画。这个函数将重复地调用drawScreen()函数并更新路径的位置。由于这个操作相比在画布上一次绘制一个路径或图形要复杂得多,因此这里给出例子的完整代码,如下:

例2-28 使用clearRect()函数

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chapter 2 Example 28: Animating a Path</title>
<script src="modernizr.js"></script>
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
function eventWindowLoaded() {

  canvasApp();

}

function canvasSupport () {
  return Modernizr.canvas;
}

function canvasApp(){

  if (!canvasSupport()) {
       return;
  }else{
    var theCanvas = document.getElementById('canvas');
    var context = theCanvas.getContext('2d');
  }
  var yOffset=0;

  function drawScreen(){

    context.clearRect(0,0,theCanvas.width,theCanvas.height);

    var currentPath=context.beginPath();
    context.strokeStyle = "red"; 
    context.lineWidth=5;
    context.moveTo(0, 0+yOffset);
    context.lineTo(50, 0+yOffset);
    context.lineTo(50,50+yOffset);
    context.stroke();
    context.closePath();
       yOffset+=1;
}

在例2-28中,首先创建一个命名为yOffset的变量,并赋值为0。下一步,在draw Screen()函数中添加一个画布清除函数。然后绘制路径,并在y轴坐标值yOffset上累加。

如第1章所示,创建一个gameLoop()函数并调用一次,在该函数中使用setTimeout()函数每20ms递归调用自身,这将导致drawScreen()函数反复被调用。在draw Screen()函数的底部,只需每次将yOffset加1,就可以创建路径向下移动的动画效果。

使用Canvas的isPointInPath()函数,可以方便地检测一个点是否是在当前路径中,代码如下:

context.strokeStyle = "red";
context.lineWidth=5;
context.moveTo(0, 0);
context.lineTo(50, 0);
context.lineTo(50,50);
context.stroke();

var isPoint1InPath1=context.isPointInPath(0, 0);
var isPoint1InPath2=context.isPointInPath(10, 10);
console.log("isPoint1InPath1=" + isPoint1InPath1);
console.log("isPoint1InPath2=" + isPoint1InPath2);
context.closePath();

由于第一个点(0,0)在当前路径中,因此控制台会输出true。由于第二个点(10,10)不在路径中,因此控制台会输出false。

提示

目前,这个函数不是在每种浏览器上都有效。每个新版本的浏览器都在不断地提高兼容性。开发者需要在浏览器中针对这个函数是否能被完全兼容进行测试。

进一步挖掘Canvas的规范,会发现了还有一些功能尚未被实现。 DrawCustom FocusRing()函数将应用于Canvas的当前路径,它是用于无障碍化访问的。context.drawSystemFocusRing(element)函数允许在指定的元素周围用当前路径绘制一个焦点环。目前,几乎没有浏览器支持此功能。总有一天,人们可以在Canvas上使用它们,并且通过下面这个函数来检查对焦环能否显示。

var shouldDraw = context.drawCustomFocusRing(theCanvas);

如果返回true,那么当前路径的自定义对焦环可以显示。

本章讨论了很多基础内容,介绍了创建简单和复杂形状的方式,以及如何在画布上绘制和变换这些形状。本章还讨论了如何组合、旋转、缩放、平移、填充这些形状以及为它们创建阴影。不过,HTML5 Canvas探索之旅才刚刚开始。下一章中,本书将讲解如何在画布上创建以及操作文本对象。


相关图书

HTML+CSS+JavaScript完全自学教程
HTML+CSS+JavaScript完全自学教程
零基础入门学习Web开发(HTML5 & CSS3)
零基础入门学习Web开发(HTML5 & CSS3)
HTML CSS JavaScript入门经典 第3版
HTML CSS JavaScript入门经典 第3版
HTML+CSS+JavaScript网页制作 从入门到精通
HTML+CSS+JavaScript网页制作 从入门到精通
从0到1:HTML5 Canvas动画开发
从0到1:HTML5 Canvas动画开发
从零开始:HTML5+CSS3快速入门教程
从零开始:HTML5+CSS3快速入门教程

相关文章

相关课程