游戏设计与开发技术丛书
北京
为什么写这本书
对游戏的热爱使我萌发了写书的念头。漫画和电子游戏是童年最美好的回忆,任天堂的红白机陪伴着我度过了童年最快乐的时光,20 世纪 80 年代,大街小巷的街机室成了孩子们快乐的天堂。随着时光流逝,许多经典的游戏画面已成为过去,但对游戏的热情依然不减,希望能借此书得以慰藉逝去的青春。青春不在,游戏热血永存!
IT 技术可谓日新月异,要想不被社会淘汰,就必须要不断学习,不断充实自己。HTML5是Web技术中提出的新规范。新生的HTML5技术虽然目前还不十分成熟,但我们可见它巨大的发展潜力,相信在未来的几年中,在各大公司不遗余力的强力支持下,HTML5将会逐渐成熟,展现令人着迷的魅力,给互联网带来新一轮的蓬勃发展。
我曾经做过程序研发,当过培训师,每个角色都让我有不同的感悟。做程序研发的时候,主要是使用Java平台,Java让我深刻理解到开源的魅力,开源提供的资源共享机制能让程序工作人员快速成长,让产品得到有效的监督和持久的生命力。做培训师让我能把所学的知识在理解和消化之后分享给他人,把知识传授给学员是一份责任和事业。写这本书的另一个目的是希望能够把所学到的东西分享给大家,独乐乐不如众乐乐。
本书特色
本书全面介绍了HTML5和游戏相关的常用技术,从Web页游戏和HTML5的新特性入手,首先让读者快速了解HTML5规范中的一些新特色,然后详细讲解游戏中占有重要地位的HTML5的Canvas元素和多媒体元素,丰富的HTML5游戏编程理论知识和案例的充分结合会为读者打下扎实的基础。
在理解和掌握前三个章内容的基础上,第4章专门介绍游戏的运行机制,详细实现游戏的核心技术——游戏渲染引擎,让读者彻底理解游戏引擎的原理和技巧。除了常见的Web客户端技巧外,第5章将详细讲解基于HTML5网络游戏的基础。游戏开发中除了渲染外,还会应用到大量的算法,第6章会剖析实现游戏过程中常见的人工智能算法和技巧,使读者能够在游戏开发中得心应手。《愤怒的小鸟》非常红火,其背后的核心就是使用了Box2D物理引擎,第7章会带领读者了解物理引擎的使用,开发出自己的"小鸟"游戏。CSS3特性大大加强了HTML5的表现能力,第8章中CSS3的相关知识会让HTML5游戏开发人员拥有一个强有力的工具。
结合本书实现的渲染引擎、Box2D物理引擎和Node.js网络框架,本书的最后三章分别实现了三个不同种类的游戏,使读者迅速了解游戏开发的过程,积累开发经验。
读者对象
本书适合以下读者。
• 有一定的HTML和JavaScript语言基础,对HTML5 游戏编程有浓厚兴趣的Web 前端开发工程师。
• 有一定的HTML5 游戏开发的基础,从事HTML5游戏开发的工程师。
勘误和支持
虽然我已经多次核对本书以及所有源代码,但由于编写时间仓促,加之个人能力有限,本书难免存在一些错误和欠缺的地方,恳请读者批评指正,以便再版时消除这些错误。如果你有任何意见或者想法请通过xiangfengLf@163.com邮箱以及QQ41144840与我联系。本书的所有源代码通过人民邮电出版社(www.ptpress.com.cn)本书页面免费下载。
致谢
本书能够顺利出版,首先感谢我的父母,是他们给了我生命,让我懂得了生命的价值和意义。感谢我的妻子和孩子,是她们陪着我驱散了每个孤独的夜晚。
感谢HTML5研究小组的田爱娜女士、唐俊开先生在本书的写作过程中给予的大力支持。感谢参与本书部分案例的美术工程师,我的好朋友詹毅,有你,游戏的世界会更美好。此外,感谢互联网提供的各种资料和前辈的经验,相信国产游戏会有着更加灿烂的未来。
谨以此书献给许许多多热爱HTML5游戏的开发工程师,未来有你们会更精彩。
向峰
于2013年8月
从时间上来计算,游戏行业从诞生到现在还不到100年历史,跟其他传统的行业相比,它甚至就像襁褓中的婴儿一样小,但正是这婴儿,正逐渐挑战着众多的传统行业。现在,很多人都会在不同的时刻玩着不同的游戏,也许你正在虚拟的网络游戏中热血澎湃地战斗,也许你正在电子游戏竞技中展现你的人生价值,也许你在忙碌的工作后,玩着切水果游戏不停地发泄,总之,你会感受到,它正在悄然融入到我们的生活当中,正在成为你生活的一部分。
随着新一代Web开发标准——HTML5诞生,各大浏览器厂商和软件厂商都不遗余力地支持 HTML5 标准,加入到 HTML5 的阵营,互联网时代的新一轮革命即将展开,当游戏碰上HTML5将会产生怎样的激烈火花,真是让人感到期待。
从电子游戏载体来说,电子游戏现在基本上分成了3个主要的阵营:第一部分是以电视游戏为主,第二部分以个人电脑游戏为主,第三部分是以手机和平板为主。而从游戏的玩家数量来说,游戏经历了从单机游戏时代到现在的网络游戏时代。随着互联网的普及,以及电脑硬件的飞速发展,互联网游戏正处于高速发展的时期,特别是网页游戏得到了空前的发展。
网页游戏也称Web Game,是一种无端网游,和《魔兽》系列、《星际》系列等传统的网络游戏相比,网页游戏不需要下载客户端,玩家只需要通过电脑打开浏览器即可进行游戏,与传统的大型网游比起来,其优点是无需安装、占据空间小、使用方便等,对于开发人员来说,比开发传统的网络游戏更容易。
网页游戏从最早的 MUD(Multiple User Domain,多用户虚拟空间游戏,玩家爱称"泥巴游戏")发展而来,早期的 MUD 游戏限于技术条件,几乎是纯文字网游,没有图形,全部用文字和字符画来构成。根据维基百科记载,世界上第一款真正意义上的实时多人交互网络 MUD 游戏"MUD1",是在 1978 年,由英国埃塞克斯大学的罗伊·特鲁布肖用 DEC-10编写的。随着Internet的和HTML语言的飞速发展,纯文字类的游戏淡出历史舞台,丰富多彩的带图像的WebGame逐渐兴起。现在的一些2D网页游戏几乎能和传统的网络游戏媲美,比如《可乐吧》、《弹弹堂》、《第九城市》、4399 游戏平台、91wan 游戏平台、1wan 游戏平台等。
由于网页游戏运行的环境在浏览器中,所以常见的开发语言,在客户端主要使用HTML、CSS样式以及JavaScript语言,服务器可以使用诸如C/C++、C#、Java、PHP等传统的服务器端语言。在HTML4时代,HTML语言受到诸如缺乏高效的图像渲染方法、缺乏实时的网络通信方法等技术支持,加上JavaScript运行效率相对一些常用的游戏编程语言 C/C++、Java 低,所以目前比较成熟的网页技术都需要在浏览器中安装一些特殊的插件(Flash Player、Applet、ActiveX、Unity Web Player等)以帮助Web Game 能够高效地运行。就目前来说,Web Game 使用的最广泛的客户端技术主要还是以Flash平台为主,从1995年到现在经过了近20多年的时间,各种关于动画、游戏方面的技术已经非常成熟,所以 Flash 通常作为 Web Game 首选的开发平台。但随着HTML新标准的发布,也就是HTML5的横空出世,也就注定了Flash的路将不会长久。Flash的研发公司Adobe已经于2011年宣布停止Flash后续研发工作,而转向新的HTML5。
自从HTML5新标准发布以来,就引起了互联网技术的新一轮风暴,作为新一代的Web技术的领航者,它受到了各大厂商的追捧,几乎所有的IT大厂商都全力提供对HTML5规范的支持。相对于 HTML4.X 版本而言,HTML5 提供了许多令人激动的新特性,这些新的特性将为HTML5开创新的Web时代提供了坚强的基石。
HTML(Hypertext Markup Language,超文本标记语言)是专门在Internet上传输多媒体的一种语言,正是有了HTML语言的出现,现在的互联网世界才显得丰富多彩,从1993年第一个版本的HTML语言诞生以来,共经历了以下几个重要的发布版本。
(1)HTML(第一版),这是一个非正式的工作版本,于 1993 年 6 月作为 IETF(Internet Engineering Task Force,IETF)草案发布。
(2)HTML2.0,1995年11月作为RFC1866(Request For Comments)发布,RFC是由IETF发布的备忘录。
(3)HTML3.2,1997年1月14日,成为W3C(World Wide Web Consortium,W3C)推荐标准。
(4)HTML4.0,1997年12月18日,W3C推荐标准。
(5)HTML4.01,1999年12月24日,W3C推荐标准。
(6)Web Applications1.0,2004 年作为HTML5 草案的前身由WHATWG(Web Hypertext Application Technology Working Group,以推动HTML5 标准而建立的组织)提出,2007 年被W3C组织批准。
(7)HTML5草案,2008年1月22日,第一份草案正式发布。
(8)HTML5.1,2012年12月17日,W3C的首份规范草案发布。
事实上到现在为止,HTML5还处于发展和完善时期,但诸多HTML5中新增加的功能已经让各大软件厂商鼎立支持。从HTML5 前身的名称(Web Application)我们可以看出HTML5 的决心,HTML不再只是单纯的网站制作语言,而是作为Web应用程序的开发语言应运而生,为了能够承担Web应用程序所能够完成的功能,在不需要安装任何插件的情况下,HTML5中提供了以下激动人心的功能。
(1)canvas画布元素,canvas元素的诞生为HTML5能够支持较高性能的动画和游戏提供了条件。canvas可以直接使用硬件加速完成像素级别的图像渲染,不仅可以完成2D图形渲染,使用WegGL以及Shader语言还可以完成较高性能的3D图形渲染。
(2)多媒体元素,HTML5中提供了专门的audio元素和video元素,用于播放网络音频文件和视频文件,有了这两个多媒体元素,将不再需要单独安装插件就可以进行影音的播放,减少浏览器的污染程度。
(3)地理信息服务,通过HTML5的地理信息服务API我们可以获取到客户端所在的经度和纬度,利用这些信息可以向这个坐标附近的区域提供服务,可应用于地理交通信息查询、基于LBS(Location Based Services)的社交游戏等。
(4)本地存储服务,相对于传统的Cookie微量的本地存储技术,HTML5推出了新的本地存储规范,提供了容量更大,更安全和更易于使用的本地存储方案。
(5)WebSocket通信,弥补了传统的Web应用程序缺乏实时通信的功能,使用WebSocket技术可以在Web应用程序中实现类似于传统的C/S结构应用程序的通信功能,使得在Web环境中构建实时的通信程序提供了可能。
(6)离线存储,HTML5 的离线缓存应用的功能,使客户端即使没有连接到互联网络,也可以在客户端正常使用本地功能。有了这个强大的功能,用户可以更加灵活地控制缓存资源的加载,可以在没有网络信号的情况下使用本地应用。
(7)多线程,HTML5中提供了真正意义上的多线程解决方案,在HTML4的使用过程中,如果遇到客户端需要在后台执行耗时方法,则页面会处于"假死"状态,而在 HTML5 中可以使用多线程解决类似问题。
(8)设备,为了能够适应多种平台(PC、手机、平板),HTML5 提供了 Device 元素,可以让应用程序访问诸如摄象头、麦克风等硬件设备。
总之,这些新增加的特性无疑都是冲着本地应用程序而来,尽管HTML5还处于发展阶段,但已经成为下一代Web开发的标准。
原则上,HTML5应用程序可以在目前任何主流的浏览器(IE、Chrome、Opera、FireFox、Safari等)中执行,但由于HTML5规范目前正处于发展阶段,所以,现今的各种主流浏览器对HTML5规范支持的程度不太一样。根据国际权威的HTML5测试网站http://html5test.com/发布最新一期测试数据来看,目前,对HTML5 支持度最高的浏览器是Maxthon 4.0 和Chrome 26。这里所指的支持度是通过对HTML5的各项新特性诸如canvas、video/audio、新的form元素、WebGL等综合的一个兼容性支持,图1-1显示了各个主流版本浏览器在PC上对HTML5的支持程度。
从兼容性来看,目前Maxthon4.0(傲游)对HTML5的各项支持度最高,但从实际Web页面运行效果来看,综合JavaScript运行速度、DOM渲染速度、动画渲染速度、安全性、综合性能等因素,Chrome是目前较好的选择。同时,由于JavaScript是唯一的Web应用程序的客户端语言(使用带插件的AS和ActiveX等除外),而Chrome拥有最快的JavaScript执行引擎V8,这样就大大提高了Web应用程序客户端的运行效率。所以,从游戏开发的角度来看,目前Chrome无疑也是较好的选择,本书中的所有案例都在Chrome中测试通过。
html5test.com网站主要是针对 HTML5兼容性测试,另外一个对 HTML5综合性能检测权威的网站 http://peacekeeper.futuremark.com 可以针对浏览器进行全方位的测试,从检测结果来看,目前综合性能最高的也是Chrome浏览器,图1-2显示了peacekeeper的检测结果。
在 peacekeeper 首页以漫画的形式展示了目前浏览器的综合评论,打出的标语是"Google Chrome King of the Hill since 2011-09-19 Windows PC",当然,也可以单击"Test your browser GO"来检测本地浏览器的综合性能,图 1-3 是笔者机器的检测分数(为准确测试,最好只打开一个浏览器Tab页,关闭其他耗时的进程)。
原则上来说,使用任何文本编辑工具都可以完成HTML5代码的编写工作,编辑好HTML5代码后保存为.htm或者.html的文件即可,然后可以使用支持HTML5的浏览器查看效果。
工欲善其事,必先利其器。尽管可以直接使用NotePad编写HTML5应用程序,但为了能够提高代码的编写效率和减少出错概率,我们可以使用一些比较常用的IDE工具完成相关程序开发,这里提供几种轻量级和重量级的IDE工具。
1.轻量级开发工具
• EditPlus
EditPlus是一个轻量级的、可替代NotePad的文本编辑工具,最新的3.51的版本执行文件只有2 MB大小。麻雀虽小,五脏俱全,EditPlus功能强大,界面简洁美观,启动速度快;中文支持比较好;支持语法高亮;支持代码折叠;支持代码自动完成(但其功能比较弱),不支持代码提示功能;配置功能强大,且比较容易,扩展也比较强,默认支持HTML、CSS、PHP、ASP、Perl、C/C++、Java、JavaScript和VBScript等语法高亮显示,通过定制语法文件,可以扩展到其他程序语言。EditPlus内置了与Internet的无缝连接,可以在EditPlus的工作区域中直接打开Internet浏览窗口。本书的所有示例代码都是使用该软件编写,图1-4是EditPlus工具的界面。
和EditPlus类似的轻量级的工具还有emEdit、UltraEdit-32等。
可以在官网http://www.editplus.com/下载最新版本。
• 1st JavaScript Editor Pro
从名字上可看出,该软件号称第一的 JavaScript 编辑工具,因为 Web 应用程序主要依靠HTML、CSS以及JavaScript语言,所以JavaScript的重要性可见一斑。由于之前介绍的文本编辑工具虽然支持各种语言的高亮语法显示,但都缺乏语言智能感知功能,对于编写较大型的程序而言不太方便。而本款软件则专门针对 JavaScript 提供了开发、校验和调试功能,除了针对JavaScript有优化外,也可针对其他语言(JavaScript、HTML、CSS、VBScript、PHP、ASP.NET)提供语法加亮显示,也提供了完整的HTML标记、属性、事件以及JavaScript事件、对象、函数等。目前最新的版本是 5.1,完整的安装包只有 7 MB 多,也是一款不可多得的轻量级的JavaScript编辑器。图1-5是JavaScript Editor Pro 工具的界面。
类似的JavaScript编辑工具还有 JavaScript Menu Master、JavaScript Editor 等。
可以在官网http://yaldex.com下载最新版本。
2.重量级开发工具
轻量级工具的特点是免费、体积小、执行速度快,缺点是缺乏完善的编辑环境、完整的项目生命周期管理、版本管理、团队协作管理等,所以在个人制作小型程序上具有一定优势,但在完整的大型软件开发过程中,商业化的重量级的IDE才是首选。以下介绍几款常用的重量级开发工具。
• Eclipse
不用多说了,著名的跨平台开源集成开发环境,由于其优秀的灵活性、高扩展性,加上开源免费,成为了J2EE领域和Android领域开发工具首选的开发平台。Eclipse支持多个操作系统平台,如 Windows、Linux、iOS 等,最初的版本只是为了用于 Java 平台开发,但是在基于Eclipse强大的插件机制下,除了开发Java应用程序,Eclipse也提供了对C/C++(CDT)、PHP、Perl、Ruby、Python、telnet和数据库的开发,成为名副其实的多平台、多语言的程序开发集成环境。
图1-6是Eclipse Juno版本工具的界面,这是Eclipse标准版本的免费IDE工具,如果需要专门针对HTML5和Javascipt开发,可以直接使用基于Eclipse的开发工具Aptana。相对于标准版本的 Eclipse 集成环境,Aptana 几乎就是专为 Web2.0 定制的集成开发环境,提供了非常强大的JavaScript编辑和调试功能,支持最新的各种JavaScript框架(jQuery、Dojo、Prototype、Aflax等)的智能感知功能,除此之外,Aptana也提供了针对Rube、Perl、PHP以及Objective-C等语言的支持,可以通过官网http://www.eclipse.org/downloads/下载最新的Eclipse 标准IDE 工具,在官网http://www.aptana.com/可以下载到最新的Aptana集成环境工具。
• WebStorm
如果觉得Eclipse太过庞大,或者只想针对Web前端进行开发,WebStorm则是另一个非常不错的选择,完整的WebStorm 5.0 的安装版本只有90 MB左右。
同上述的其他优秀的集成开发环境一样,WebStorm提供了对JavaScript和HTML的强有力的支持,号称"The smartest JavaScript IDE",由此可见WebStorm的功能之强大,只是该集成平台只支持Web前端的开发,事实上WebForm是另一个优秀的Java开发平台IntelliJ的简化版。WebStorm开发界面如图1-7所示。
可以通过官网 http://www.jetbrains.com/webstorm/下载最新的 WebStorm 工具,需要注意的是这款优秀的JavaScript IDE是需要收费的。
• Visual Studio
毫无疑问,Windows平台下最佳的开发工具就是Microsoft的Vistual Studio系列,从Visual Studio 10 SP1开始,VS系列提供了对HTML5以及JavaScript部分的智能感知功能。就Web前端开发来说(除开发ASP.NET程序外),相对于其他的Web开发功能稍弱。图1-8是Visual Studio 2008开发界面。
同样地,我们可以从中文的MSDN官网http://msdn.microsoft.com/zh-cn/vstudio下载最新的Vistual Studio版本。
关于常用的HTML5开发工具就介绍到这里,相信大家会根据自己的需要而选择适当的工具进行开发。
准备好HTML5开发工具之后,我都迫不急待地想看看HTML5给我们带来了哪些让人激动不已的新特性,相信,你也会和我一样,对它感到兴奋。由于HTML5新增加了很多元素,这里,我们重点简单介绍一下在游戏中会使用到的一些元素,在介绍每一个新的元素时,我都会列举一些小的例子。现在假定读者有一定的HTML以及JavaScript方面的知识,如果您对这些代码完全不清楚,没关系,后面的章节中会详细地解说。目前,我们只需要知道HTML5能做什么就足够了。
canvas是HTML5中一个非常重要的元素,canvas元素提供了硬件加速机制,能够让我们实现高效的绘图功能。第2章我们会详细介绍canvas功能,这里,我们先来领略一下canvas的魅力。
打开你的编辑工具,输入以下代码:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
</head>
<style type="text/css">
body, div{margin:0px;padding:0px;text-align:center}
#cav{
border:2px solid black;
border-radius:4px;
box-shadow:0px 0px 10px black;
-webkit-box-shadow:0px 0px 10px black;
-moz-box-shadow:0px 0px 10px black;
}
#bk{
margin:10px auto;
width:400px;
height:36px;
}
.bk{
text-align:center;
width:20px;
height:20px;
margin:12px;
display:inline-block;
border:1px dotted gray;
}
</style>
<body>
<div id="bk" align="center"></div>
<canvas id="cav" width="400" height="300"></canvas>
</body>
<script src="../js/jquery.js"></script>
<script charset="utf-8">
//定义画笔颜色
var bColor = ["#000000", "#999999", "#CC66FF", "#FF0000", "#FF9900", "#FFFF00",
"#008000", "#00CCFF"];
//当前画笔颜色
var col="#FF0000";
function initBrush()
{
for(var i=0;i<bColor.length;i++)
{
var bk=$("<span class='bk'></span>")
.css("background-color", bColor[i])
.click(function(){
col = $(this).css("background-color");
});
$("#bk").append(bk)
}
}
function initPainter()
{
//绑定绘图canvas
var can = $("#cav"), self = this, x=0, y=0;
var ctx = can[0].getContext("2d");
ctx.lineWidth = 2;
//绑定鼠标按下时间
can.on("mousedown", function(e){
e.preventDefault();
ctx.strokeStyle = col;
x = e.offsetX,
y = e.offsetY;
//开始路径
ctx.beginPath();
ctx.moveTo(x, y);
//绑定鼠标移动事件
can.on("mousemove", function(e){
var nx = event.offsetX,
ny = event.offsetY;
ctx.lineTo(x, y);
ctx.stroke();
x = nx;
y = ny;
});
//绑定鼠标抬起事件
can.on("mouseup", function(){
//取消鼠标移动事件
can.off("mousemove");
});
})
}
$(document).ready(function(){
initBrush();
initPainter();
})
</script>
</html>
虽然有点长,但当你完成它的时候,我想,一定不会让你失望的,这是一个不错的开始,也许现在你可能还看不太懂,没关系,第2章就会对canvas元素进行详细介绍。以上代码通过canvas元素完成了一个在浏览器中使用的画板,可以通过Chrome浏览器打开它,看到的效果如图1-9所示,你可以选择不同颜色的画笔在画板上进行涂鸦。
除了支持2D渲染技术外,canvas还提供了通过WebGL的方式,利用硬件加速提供3D渲染技术。以下代码使用了开源的three.js的3D引擎,完成了一个简单的旋转方块,注意,该3D效果完全使用WebGL技术,不需要安装任何插件。顺便说一下,three.js是一款完全免费使用的3D引擎,该引擎完全基于WebGL编写,事实上WebGL也是在canvas上进行渲染,目前对WebGL支持比较好的浏览器有Chrome、Firefox以及Safari等。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Cube</title>
<script src="../js/jquery.js"></script>
<!--引入Three.js-->
<script src="three.min.js"></script>
</head>
<style type="text/css">
body, div{margin:0px;padding:0px;text-align:center}
#sce{
border:2px solid black;
border-radius:4px;
width:480px;
height:320px;
margin:10px auto;
box-shadow:0px 0px 10px black;
-webkit-box-shadow:0px 0px 10px black;
-moz-box-shadow:0px 0px 10px black;
}
</style>
<body>
<div id="sce"></canvas>
<script>
$(document).ready(function(){
init();
})
//定义3D场景,摄像机光源等和立方体
var scene, cam, render, cube, light;
//初始化
function init()
{
initScene();
initCube();
animate();
}
function initScene()
{
scene=new THREE.Scene();
cam = new THREE.PerspectiveCamera(45, $("#sce").width()/$("#sce"). height(), 0.1, 2000);
cam.position.x= cam.position.y = cam.position.z=2;
cam.lookAt(new THREE.Vector3(0, 0, 0 ));
render = new THREE.WebGLRenderer({antialias:true});
render.setSize($("#sce").width(), $("#sce").height());
$("#sce").append(render.domElement);
}
function initCube()
{
//加载纹理
var text = THREE.ImageUtils.loadTexture( 'box.gif' );
text.anisotropy = render.getMaxAnisotropy();
//创建几何图形和材质
var g = new THREE.CubeGeometry(1, 1, 1),
m = new THREE.MeshBasicMaterial({ map: text });
cube = new THREE.Mesh(g, m);
scene.add(cube);
}
function animate()
{
requestAnimationFrame(animate);
cube.rotation.y += Math.PI/180;
render.render(scene, cam);
}
</script>
</body>
</html>
整个代码实现了一个绕Y轴旋转的方块,最好使用Chrome浏览器进行浏览,运行效果如图1-10所示。
当然,对于代码的细节暂时可以不用去关注,目前,我们只需要知道使用 WebGL 能做什么就足够了,对于目前的HTML5来说,2D的游戏将会是比3D更好的选择。
正如我们所期待的一样,HTML5中增加了两个多媒体元素<audio>以及<video>,利用这两个多媒体元素,可以实现无插件的播放音频和视频对象。
比如,以下代码可以直接在浏览器中播放mp3音频文件。
<!DOCTYPE html>
<meta charset="utf-8" />
<style type="text/css">
body{text-align:center;}
</style>
<body>
<h2>简单的音视频</h2>
<audio id="a1" src="media/scg.mp3" autoplay mediagroup="gp" controls>
您的浏览器不支持audio元素
</audio><br>
</body>
</html>
在浏览器中可以看到一个带有简单控制面板的音频播放器,如图1-11所示。
HTML5 提供了一组关于地理信息的API,通过这组API 我们可以获取到客户端所在的经度和纬度,利用这些信息可以向这个坐标附近的区域提供服务。这些服务一般指的是基于位置的服务(LBS),LBS能够广泛支持需要动态地理空间信息的应用,从寻找旅馆、急救服务到导航,从社交游戏到生活服务,几乎可以覆盖生活中的所有方面。常见的一些LBS的应用有:位置信息查询、急救服务、道路辅助与导航、社交游戏等。
借助HTML提供的Geolocation API 功能,很容易获取到用户当前的位置,结合这组API,我们可以编写基于LBS的社交类游戏。
同样地,我们给出一段简单的代码,可以获取用户所在的位置信息。
<!DOCTYPE html>
<html>
<head>
<title> Location </title>
<meta charset="utf-8" />
</head>
<body>
<input type="button" id="btnLocation" value="获取位置信息"/>
</body>
<script src="js/jquery.js"></script>
<script>
//检测是否支持地理信息
function isSupportGeo()
{
return (navigator.geolocation!=null)
}
//绑定click事件
$("#btnLocation").click(function(){
if(!isSupportGeo())
{
alert("浏览器不支持地理位置定位!");
}
else
{
navigator.geolocation.getCurrentPosition(doSucc, doFail);
}
});
//处理成功
function doSucc(position)
{
//alert("成功获取地理位置!");
alert("成功获取地理位置,您当前所处的位置位于经度:"+position.coords.longitude+"
纬度:"+position.coords.latitude);
}
//处理失败
function doFail(error)
{
var errors=["拒绝地理信息服务","获取不到地理信息","获取信息超时"]
alert(errors[error.code+1]);
}
</script>
</html>
代码绑定一个"获取位置信息"的按钮,单击按钮将会调用isSupportGeo方法检测浏览器是否支持地理信息,如果支持的话就调用getCurrentPosition获取当前设备的地理信息。需要注意的是,第一次访问地理信息的时候由于个人隐私的问题,浏览器会弹出一个窗口询问用户是否允许让浏览器访问自己的位置,用户可以选择接受或者拒绝,如果拒绝的话将会得到一个错误。
我们知道在传统的Web应用程序开发过程中,当客户端和服务器进行数据交互时,远程服务器需要存储客户端的各种数据,如果需要在客户端保留数据,可以采用"Cookie"的形式,但Cookie 的存储量很小,通常只有4 kB,而且安全性和使用性都还有所欠缺。为了能够满足于HTML5本地应用的迅速发展,弥补HTML在本地数据持久化的弱势,HTML5推出了新的本地存储规范,提供了容量更大、更安全和更易于使用的本地存储方案。
HTML5 中使用 Web Storage 技术进行本地存储,能够在 Web 客户端进行数据存储。WebStorage 曾经属于 HTML5 的规范,目前已经被独立出来形成单独的规范体系。简单来说使用Web本地存储类似于HashMap一样,采用键值对的形式保存数据,而且保存的数据会根据需要持久化在浏览器所在的客户端,具体来说Web Storage又可以分为以下两种。
• localStorage。将数据保存在客户端本地的硬件设备(通常指硬盘,但也可以是其他硬件设备)中,即使浏览器被关闭了,该数据仍然存在,下次打开浏览器访问网站时仍然可以继续使用。
• sessionStorage。将数据保存在session 对象中。session 对象就是会话对象,session 中存储的数据独立于每个客户,该数据会随着浏览器的关闭而消失。
这两者的区别在于,sessionStorage 为临时保存,而 localStorage 为永久保存。localStorage提供了5 MB的存储空间,而sessiongStorage 甚至没有限制,取决于内存的大小。
对于这两种本地存储对象都可以使用以下方法进行数据存取。
• setItem (key, value)。使用键值对的方式保存数据,key为键,value为值,键是一个字符串,而数据则可以是任何类型的JavaScript基本数据类型,包括字符串、Boolean、整数和浮点数。需要注意的是,这些数据在存储时实际上是以字符串保存的。因此在访问数据时需要进行数据类型的转换,也可以直接用 localStorage.key = value 或者localStorage[key] = value 的方式设置,对于复杂的数据类型,可以使用JSON 格式的对象转换成字符串对象存储。
• getItem (key)。根据 key 键值来获取数据,也可以直接用 localStorage.key 或者localStorage[key]获取,需要注意的是,这些数据在取出来时实际上是字符串。因此在访问数据时需要进行数据类型的转换。
• removeItem (key)。清除key键值。
• clear()。清除所有的键值对。
例如,以下简单的代码用于记录用户访问页面的总次数:
<body>
<h2>您是第<span id="ct"></span>次访问本页面</h2>
</body>
<script>
$(document).ready(function(){
//获取访问次数
readCount();
})
//获取访问次数
function readCount()
{
var count=localStorage["count"];
if(count==null){count=1;}
else {count = Number(count)+1;}
$("#ct").text(count);
localStorage["count"] = count;
}
</script>
上面代码中记录了用户访问页面次数,第一次访问时,获取的count为空,设置其为1,随后的访问中获取数据后通过Number函数转成数字进行累加并重新设置到localStorage中。需要注意的是,即使关闭浏览器,下次打开时,数据依然存在,因为localStorage的数据实际上持久化在本地文件中。
一直以来,HTTP 的无状态性和单向性作为常用的应用已经足够,但还无法实现游戏中需要的实时双向通信技术,而HTML5中新增加的WebSocket通信元素则弥补了这一缺陷,使用新的Web sockets 技术对于实现高效的Web 网络游戏提供了基础。
我们可以使用以下代码连接支持Web sockets 协议的服务器端,并发送数据。
var ws = new WebSocket("ws://localhost:9000/socket");
ws.onopen = function(){
try
{
ws.send("Hello, Web Socket");
}
catch(e)
{
console.log("发送消息失败!");
}
}
当然,需要注意的是,以上只是使用WebSocket元素的客户端代码,需要有支持WebSocket协议的服务器端支持,在本书的第5章会详细介绍使用WebSocket的客户端以及Node.js的服务器端通信技术。
Web Worker 技术填补了 JavaScript 中一直所缺少的多线程技术,之前的 HTML 中,进行DOM元素渲染以及执行JavaScript脚本都是在单一的线程中执行,一旦在JavaScript中需要执行比较耗时间的操作,则会阻塞当前线程的执行。所带来的效果就是,当前的浏览器处于无响应状态,直到浏览器会弹出一个提示脚本运行时间过长的信息。这里,我们看一个简单的例子。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
</head>
<style type="text/css">
body, div{margin:0px;padding:0px;text-align:center}
</style>
<body>
<input type="button" id="btn_calc" value="计算圆周率" />
<input id="txt_rt" />
</body>
<script src="../js/jquery.js"></script>
<script charset="utf-8">
$(document).ready(function(){
$("#btn_calc").on("click", function(){
$("#txt_rt").val(getPI(80000000));
})
})
//求圆周率
function getPI(n)
{
var a=0;
for (i=0;i<=n;i++)
{
a=a+4*Math.pow(-1, i)/(2*i+1);
}
return a;
}
</script>
</html>
在该例子中,提供了一个通过级数获取圆周率的函数,参数n表示迭代次数,次数越高,计算越精确。然后提供一个按钮计算圆周率在文本框中显示,在浏览器中运行,我们发现当迭代次数很高的时候,计算量较大,在得到计算结果之前,按钮和文本框都是处于无法响应的状态,这也符合实际情况,因为此时DOM元素的更新被大计算量的程序给阻塞了。如果使用Web Worker则不会出现此情况,我们可以把计算量大的工作放到一个单独的线程中运行,不会阻塞当前线程。
创建后台线程可以使用以下方式:
var worker = new Worker("worker.js")
我们把计算工作放到worker.js文件中,前台可以通过postmessage方法发送消息给Worker线程,请求Worker进行工作,直到Worker线程工作完毕返回结果。
对于前面计算PI的例子,我们可以修改如下,首先,创建worker.js文件,代码如下:
function getPI(n)
{
var a=0;
for (i=0;i<=n;i++)
{
a=a+4*Math.pow(-1, i)/(2*i+1);
}
return a;
}
onmessage = function(e)
{
var result=getPI(e.data);
postMessage(result);
}
worker.js文件中定义了计算PI的函数,我们需要定义一个onmessage函数,当前台通过调用worker对象的postmessage方法时,该回调函数被响应。
然后,把显示结果的页面修改如下:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
</head>
<style type="text/css">
body, div{margin:0px;padding:0px;text-align:center}
</style>
<body>
<input type="button" id="btn_calc" value="计算圆周率" />
<input id="txt_rt" />
</body>
<script src="../js/jquery.js"></script>
<script charset="utf-8">
$(document).ready(function(){
$("#btn_calc").on("click", function(){
var wk = new Worker("worker.js");
//注册返回结果,当woker发送postmessage时,该回调函数响应
wk.onmessage = function(e)
{
$("#txt_rt").val(e.data);
}
//发送计算请求
wk.postMessage(80000000);
})
})
</script>
</html>
这里,首先创建了一个Worker对象,然后在Workder对象中注册一个onmessage回调函数。注意,这个回调函数和 workder.js 文件中的 onmessage 是不同的,这里的回调函数当 woker.js文件中的onmessage函数执行完毕后响应,最后,调用Worker对象的postMessage方法发送计算请求,此时,woker.js文件中的onmessage函数就会执行。
修改成Worker版本后,你会发现,当单击按钮得到计算结果之前,按钮和文本框都是可以响应的,因为此时计算线程被独立出来了。
本章简单介绍了一下网页游戏的现状、HTML5的开发工具和HTML5的一些新的特性,相信您对HTML5有了初步的认识。
从下章开始,我们就会一步一步接触到HTML5的游戏开发世界,准备好你的开发工具,拿起你的武器开始攻占HTML5游戏开发的高地。
首先,我们需要学习的是HTML5中的canvas元素,canvas元素是HTML5中一个非常重要的元素,也是实现游戏的核心,canvas元素的诞生为HTML5 能够支持较高性能的动画和游戏提供了可行性。HTML4 中,如果要完成类似画板和游戏的功能,需要使用 Flash,但 Flash需要在浏览器中安装Flash插件才能提供相应功能。本章围绕canvas元素讲解以下几个主要内容(对于非常熟悉本章内容的读者可略过本章):
• 基本知识;
• 图形API;
• 图像API;
• 文本API;
• 坐标变换。
在HTML4时代,不安装插件的情况下,基于浏览器的绘图组件是最初由微软向W3C递交的VML(Vector Markup Language 矢量标记语言)技术,但未被W3C采纳,只能在IE5.0及其后续版本中使用,后来VML 和PGML(由Adobe 和SUN 提出)合并成SVG(Scalable VectorGraphics,可伸缩矢量图形)规范。SVG技术是基于XML的矢量图形绘制技术,对于普通的图像应用来说足够,但是对于性能要求较高的游戏渲染来说,SVG 支持还不够,而且 SVG 也无法支持像素级别的操作,不能直接使用硬件加速机制,所以,需要有一个更适合游戏运行的高效的组件,这就是canvas组件。
在所有的桌面应用程序的开发平台中,几乎都有canvas组件,canvas组件几乎成了绘图组件的代名词。在基于浏览器的应用开发中,canvas元素最初是由苹果在自己的Mac OS X WebKit中推出,供应用程序和Safari浏览器使用。后来,W3C建议在下一代网络技术中加入该元素以适应新的技术要求,目前几乎所有的浏览器都提供了对canvas的支持。
canvas 元素本质上是在浏览器中提供了一块可绘制的区域,JavaScript 代码可以访问该区域,通过一套完整的API进行二维图像绘制。另外,如果显卡硬件支持3D图形功能,还可以使用canvas绘制3D图形。下面我们看看canvas的使用。
首先,我们来看如何在HTML5中使用canvas元素,使用canvas标签的代码如下:
<!DOCTYPE html>
<html>
<canvas id="can" width="800" height="600">Hello Canvas</canvas>
</html>
以上代码创建了一个id为can的canvas,canvas的宽度为800,高度为600。需要注意的是,创建的canvas在浏览器中看不到,因为它默认是透明的。另外,canvas比较特殊,我们最好不要用css样式定义宽度和高度,直接使用width和height定义就好,默认情况下canvas的width是300,height是150。需要注意的是,canvas的结束标签是必需的,当浏览器不支持canvas元素时,canvas标签中间的文字就会显示出来。
加入了 canvas 元素之后,我们就可以通过 JavaScript,使用 canvas 元素提供的 API 操作canvas,在 canvas 上绘图。在绘图之前,我们需要了解 canvas 元素的坐标系,canvas的坐标系如图2-1所示。
可以看出,对于 canvas 来说,它的坐标系是以左上角为原点,向右延伸是 X 横坐标正方向,向下延伸是 Y 纵坐标正方向,所以原点的坐标是(x,y)=(0,0)。弄清楚坐标系对于以后使用canvas的API意义重大,在绘图的时候需要时刻记住坐标空间。
在定义好了canvas之后,我们就可以使用JavaScript访问canvas元素,使用canvas提供的一系列API。在使用canvas的时候,首先要得到canvas的环境上下文,有了canvas的环境上下文,才能够对canvas进行相应操作,可以通过以下方式获取环境上下文:
<script>
var context = canvasElement.getContext("2d");
</script>
使用canvas元素的getContext ("2d")的方式可以获取到2D图像操作的环境上下文,如果获取的 context 为空,表示该浏览器不支持 canvas。有了 context 变量后就可以在 canvasElement元素上进行 2 维图像操作,另外可以通过 getContext ("webgl")获取 webgl 环境上下文进行 3 维图像操作。
创建canvas和获取了canvas的环境上下文之后,就可以开始进行绘图了。绘图的方式有两类:一类是进行图形操作,另一类是图像操作。本小节主要涉及图形相关的API,要使用canvas的API进行绘图,通常需要进行下列步骤。
(1)获取canvas元素。通过document.getElementById()取得元素。
(2)获取canvas 元素的环境上下文。通过canvas.getContext ("2d")获取2D 图像上下文。
(3)确定绘图模式。使用canvas绘图有两种模式,一种是fill,另外一种是stroke。fill是填充的意思,使用该方式模式进行绘图时候会把颜色填充整个图形,而使用stroke的模式只会进行描边框。
(4)设定绘图样式。通过fillStyle和strokeStyle指定绘图样式,fillStyle和strokeStyle分别对应 fill 模式和 stroke 模式。绘图样式包括绘图的颜色及渐变方式,通常情况下默认的绘图样式颜色是#000000,也就是黑色。
(5)指定线宽。可以通过lineWidth设定绘制的线宽,默认值是1.0像素。
在进行绘制图形之前,需要先理解路径的概念。
canvas中所有的图形可以看成一条路径,这条路径包含0个或者多个子路径。我们可以把路径看成当前canvas中所有图形的集合,而每一个图形就是一条子路径,一条子路径是由一系列点的集合组成。举个例子,我们在canvas中画了一个圆和一条直线,那么可以认为当前canvas中包含两条子路径,一条是圆,一条是直线,这两个图形都是由一个个点集组成,这个圆称为一个闭合的路径,而线是没有闭合的路径。很明显,所谓闭合就是整个图形是封闭的,图形的开始点和结束点相互连接。
事实上,为了提高绘制的效率,当使用 canvas 进行绘图的时候,所有的图形操作都只是往当前子路径上填加图形,并不是真正的调用绘图操作。比如使用 lineTo()进行画线操作,实际上它只是往当前子路径填加一条直线,最终调用stroke或者fill的时候,才是真正进行硬件操作进行绘图。
canvas中常见的创建和渲染路径的方法如表2-1所示。
关于线条的绘制主要包含以下两个常用的方法。
• context.moveTo (x, y):把画笔移动到(x, y)坐标,建立新的子路径。
• context.lineTo (x, y):用于建立上一个点到(x, y)坐标的直线,如果没有上一个点,则等同于moveTo (x, y),把(x ,y)点添加到子路径中。
最后使用stroke()可以对路径进行描边。
使用context.lineTo (x, y)绘制直线,代码如下:
<body>
<canvas id="can" ></canvas>
</body>
<script>
//获取2d上下文
var ctx = can.getContext("2d");
var width = can.width,
height = can.height;
ctx.moveTo(0, 0);
ctx.lineTo(width, height);
ctx.lineWidth=6;
ctx.strokeStyle = "red"
//开始画线
ctx.stroke();
</script>
代码首先获取can元素的环境上下文,获得can元素的宽度和高度,把画笔移动到原点处,然后使用lintTo (width, height),建立一条从原点到右下角的直线,设定线的宽度为6 个像素,笔的颜色是红色,最后通过stroke对整个路径描边。
最后的效果如图2-2所示。
如果,我们需要绘制更复杂的图形,就需要根据一些点的集合,不断的使用lineTo方法,比如下面的代码就绘制了一个向右的箭头:
<body>
<canvas id="can" width="400" height="300" ></canvas>
</body>
<script>
//获取2d上下文
var ctx = can.getContext("2d");
var width = can.width,
height = can.height;
var pts=[[30, 100], [300, 100], [300, 50], [350, 130], [300, 210], [300, 160], [30, 160]]
ctx.strokeStyle="red";
ctx.lineWidth = 2;
ctx.moveTo(pts[0][0], pts[0][1]);
for(var i=1;i<pts.length;i++)
{
ctx.lineTo(pts[i][0], pts[i][1]);
}
ctx.closePath();
ctx.stroke();
</script>
最后的效果如图2-3所示。
这里需要注意的是,这里定义了7个点,在进行stroke之前使用closePath()函数闭合了路径,这时就会把最后一个点和第一个点连接起来,形成一个封闭的图形,当然,closePath并不是必需的。
关于矩形的绘制主要包含以下两个常用的方法。
• rect (x, y, w, h):建立两个子路径,一个是以点(x, y)为左上角,w和h 分别为宽度和高度的矩形,另一个是点(x, y)。这个方法只是建立路径,所以当进行绘制的时候还需要使用stroke()方法描边。
• 最直接的方法是使用 strokeRect (x, y, w, h),该方法会以(x, y)为左上角,(x+width,y+height)为右下角绘制矩形。
• fillRect (x, y, w, h)方法则以(x, y)为左上角,(x+width, y+height)为右下角填充矩形。
• clearRect (x, y, w, h)则用来清除以(x, y)为左上角,(x+width,y+height)为右下角的矩形区域。这个方法在进行动画处理的时候非常有用,因为在连续绘制动态图形的时候,需要先清除画布上的一块区域。
对于图2-4所示的图形,代码如下:
<body>
<h2>画矩形例子</h2>
<canvas id="can" width="400" height="300" ></canvas>
</body>
<script>
//获取2d上下文
var ctx = can.getContext("2d");
var width = can.width,
height = can.height;
//计算最里面矩形左上角坐标,边长为10
var xOff = width*0.5+5, yOff = height*0.5+5;
for(var i=0;i<8;i++)
{
//以最里面矩形为中心,画同心矩形,边长增加20
ctx.strokeRect(xOff-10*i, yOff-10*i, i*20+10, i*20+10);
}
</script>
这段代码使用了strokeRect方法画出了8个同心的矩形。
关于圆弧的绘制主要包含以下两个常用的方法。
• arc (x, y, radius, startAngle, endAngle, anticlockwise):arc 方法用来绘制一段圆弧路径,以(x, y)为圆心位置、radius为半径、startAngle 为起始弧度、endAngle 为终止弧度来画,而在画圆弧时的旋转方向则由最后一个参数 anticlockwise 来指定,如果为 true 就是逆时针,false则为顺时针,如果startAngle和endAngle分别为0和2*Math.PI,则就变成了绘制圆形。
• arcTo (x1, y1, x2, y2, radius):这个函数实际上用来绘制同时和两条直线相切的,半径为radius 的最短圆弧,一条直线以上一个点和(x1, y1)构成,另一条直线以(x1, y1)和(x2, y2)构成。
这两个函数只是把圆弧添加到了路径中,如果绘制,则还需要通过stroke或者fill函数。使用arc方法绘制图2-5所示的8个同心圆的代码如下:
<body>
<h2>画圆形例子</h2>
<canvas id="can" width="400" height="300" ></canvas>
</body>
<script>
//获取2d上下文
var ctx = can.getContext("2d");
var width = can.width,
height = can.height;
//计算圆心
var xOff = width*0.5,
yOff = height*0.5;
for(var i=1;i<8;i++)
{
//以最里面矩形为中心,画同心圆,半径依次增加15
ctx.beginPath();
ctx.arc(xOff, yOff, i*15, 0, Math.PI*2, true);
ctx.closePath();
ctx.stroke();
}
</script>
需要注意的是,代码在 for 循环中,进行绘制圆形之前,使用了 beginPath()方法,beginPath()方法用于清除掉之前的路径。如果不清除的话,那么,每次绘制的时候都会把之前的路径又绘制,这样会降低绘制的效率。所以通常情况下,如果我们决定要绘制一个新的图形,最好先使用 beginPath()清除上一次的路径。
关于贝塞尔曲线的绘制主要包含以下两个常用的方法。
• bezierCurveTo (cp1x, cp1y, cp2x, cp2y, x, y):绘制一条三次贝塞尔曲线,这条曲线的开始点是子路径的最后一个点,结束点是(x, y),而贝塞尔曲线的控制点是(cp1x, cp1y)和(cp2x, cp2y)。
• quadraticCurveTo (cpx, cpy, x, y):绘制一条二次贝塞尔曲线,这条曲线的开始点是子路径的最后一个点,结束点是(x, y),而贝塞尔曲线的控制点是(cpx, cpy)。
贝塞尔曲线是应用非常广泛的函数曲线,通常在计算机图形中用来为平滑曲线建立模型,图2-6分别显示了三次和二次的贝塞尔曲线,区别在于三次的贝塞尔曲线多了一个控制点。
以下代码在canvas中显示了一个可以调节控制点的贝塞尔曲线,c1 和 c2 表示控制点,s和e表示曲线的开始和终止点:
<!DOCTYPE html>
<meta charset="utf-8" />
<style type="text/css">
body{text-align:center;}
#can{border:1px solid black}
</style>
<body>
<h2>贝塞尔曲线</h2>
<canvas id="can" width="400" height="300"></canvas>
</body>
<script>
var ctx = can.getContext("2d");
//定义Point对象
var Point = function(x, y){
this.x = x;
this.y = y;
}
//定义控制点,前面两个是开始和结束点,最后两个是控制点
var cPt =[];
//产生控制点
function createControlPt(x, y)
{
if(cPt.length<4)
{
cPt.push(new Point(x, y));
}
}
//绘制控制点
function drawPt()
{
for(var i=0;i<cPt.length;i++)
{
var c="red";
if(i<2)
{
c = "green";
}
ctx.strokeStyle = c;
ctx.strokeRect(cPt[i].x-5, cPt[i].y-5, 10, 10);
}
}
//判断一个点是否在一个以p2为中心的矩形中
function isInRect(p1, p2, w, h)
{
return p1.x>=p2.x-w&&p1.x<=p2.x+w&&p1.y>=p2.y-h&&p1.y<=p2.y+h;
}
//判断一个点在哪一个控制区域中
function getIdxCpt(p)
{
var idx=-1;
for(var i=0;i<cPt.length;i++)
{
if(isInRect(p, cPt[i], 5, 5))
{
return i;
}
}
return idx;
}
//绘制控制点和起始点连线
function drawBLine()
{
ctx.strokeStyle="gray";
ctx.beginPath();
ctx.moveTo(cPt[0].x, cPt[0].y);
ctx.lineTo(cPt[2].x, cPt[2].y);
ctx.stroke();
ctx.moveTo(cPt[1].x, cPt[1].y);
ctx.lineTo(cPt[3].x, cPt[3].y);
ctx.stroke();
}
//绘制贝塞尔曲线
function drawBei()
{
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.moveTo(cPt[0].x, cPt[0].y);
ctx.bezierCurveTo(cPt[2].x, cPt[2].y, cPt[3].x, cPt[3].y, cPt[1].x, cPt[1].y);
ctx.stroke();
}
//绘制所有的图形
function draw()
{
drawPt();
if(cPt.length>3)
{
drawBLine();
drawBei();
}
}
//设置鼠标点下和移动事件
var selPt = new Point(-1, -1), sIdx;
can.onmousedown = function(e){
var x = e.offsetX, y = e.offsetY;
selPt.x = x;selPt.y = y;
createControlPt(x, y);
draw();
//判断是否点在控制点中
if(cPt.length>3)
{
sIdx=getIdxCpt(selPt);
if(sIdx>=0)
{
can.onmousemove=function(e){
cPt[sIdx].x = e.offsetX;
cPt[sIdx].y = e.offsetY;
ctx.clearRect(0, 0, 400, 300);
draw();
}
}
}
}
can.onmouseup = function(){
this.onmousemove = null;
}
</script>
</html>
注意在绘制所有图形之前一定要使用 clearRect()方法来清除屏幕,因为如果不清除屏幕,将会在屏幕上留下所有的绘制图像。
在进行图形绘制的时候,线条有一些常用的属性会影响到线条的样式。
• lineWidth:该属性用来设置线条的粗细,默认为1个像素大小,小于0 的值将被忽略。
这里有一个比较经典的问题,就是绘制1像素大小的直线。如果绘制1像素大小的线条,看起来像2个像素,当直线呈水平或者垂直方向时,这个现象非常明显,这是为什么呢?W3C在canvas规范中解释到,当使用canvas绘制图形时候,它是由路径向两边扩展的,各占绘制线条宽度的一半,但 canvas 的坐标并不是直接和屏幕上的像素对应,假设需要绘制一条(3, 1)到(3, 5)的直线,把屏幕放大,得到图2-7。
图 2-7 中的每一个格子代表显示屏的一个像素,当绘制(3, 1)到(3, 5)的直线的时候,首先,路径就定位在屏幕中第三列像素和第四列像素的中间位置。此时,绘制 1 像素的时候,就需要从这条路径分别向两边扩展0.5个像素,但实际上显示屏是不可能绘制半个像素的,这个时候就只能同时绘制第三列和第四列两列像素。所以如果需要屏幕绘制一个像素大小的线,只需要把canvas的路径定位到某一个像素的中间位置,这时候刚好向两边扩展为一个像素,如图2-8所示。
所以,如果需要绘制1像素大小的直线,需要把坐标加上0.5的偏移,这时候就显示正常了,当然,如果画大于1像素的或者绘制斜线就没有必要额外处理了。
• lineCap:lineCap用来指定线条两端的端点,常用的值有3个,分别是butt(无端点)、round (圆端点)以及square(方端点),其中默认值是butt,三种样式显示的效果如图2-9所示。
• lineJoin:lingJoin 用来设置两条线连接的方式,常用值有round(圆角)、bevel(斜角)以及miter(尖角),其中miter是默认值,三种样式如图2-10所示。
• miterLimit:当lineJoin为miter 时有效,表示的是斜面长度和线宽的比例,默认为10。
线条的颜色使用stokeStyle属性指定,颜色的值可以使用类似CSS的方式指定,比如红色可以采用以下3种方式:
context.stokeStyle = 'red'
context.stokeStyle = '#ff0000'
context.stokeStyle = 'rgba(255, 0, 0, 1.0)'
前面所介绍的绘图方式都是适用描边处理(stroke),我们可以通过fill方法进行图形填充。
• fill():该方法使用当前的fillStyle 填充当前路径,通过fillStyle = 颜色值可以指定填充的颜色,颜色表示和strokeStyle一样。
以下代码就填充了8个红色的同心圆:
<body>
<h2>填充圆形例子</h2>
<canvas id="can" width="400" height="300" >
</canvas>
</body>
<script>
//获取2d上下文
var ctx = can.getContext("2d");
var width = can.width,
height = can.height;
//计算圆心
var xOff = width*0.5,
yOff = height*0.5;
for(var i=1;i<=8;i++)
{
//以最里面矩形为中心,画同心圆,半径依次减少15
ctx.beginPath();
ctx.fillStyle = "rgba(255, 0, 0, "+(30*i)/500+")";
ctx.arc(xOff, yOff, 120-i*15, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}
</script>
最后的效果如图2-11所示。
除了可以填充纯色以外,canvas还提供了填充渐变色以及填充贴图,先来看看渐变对象。
经常使用Photoshop处理图像的读者知道,在Photoshop中就有这种渐变工具,可以通过拖拉一条辅助线来实现渐变。在canvas中提供的渐变对象有两种,一种是线性渐变,另一种是径向渐变。
• createLinearGradient (x0, y0, x1, y1):创建一个线性的渐变对象,开始点是(x0, y0),结束点是(x1, y1)。
• createRadialGradient (x0, y0, r0, x1, y1, r1):创建一个径向渐变对象,开始点以(x0, y0)为圆心,r0 为半径,结束点以(x1, y1)为圆心,r1为半径。
一旦创建完了渐变对象之后,就可以通过该对象的addColorStop()方法,在渐变的某一点中增加一个颜色值,这个点可以认为是关键点,这样,每个关键点之间的色彩就会出现渐变效果。
• addColorStop(offset, color):offset表示偏移大小,值在0.0~1.0之间,其实就是一个百分比值;color 是使用类似CSS 字符串描述的色彩颜色,比如 addColorStop (0, "red")表示初始关键点是一个红色点。
当使用 fillStyle 属性指定一个渐变对象的时候,就可以使用渐变的方式填充路径了,以下代码以渐变对象填充了两个矩形区域。
<body>
<h2>渐变</h2>
<canvas id="can" width=600 height=300></canvas>
</body>
<script>
var ctx = can.getContext("2d");
var w = 480, h=60;
ctx.beginPath();
//创建线性渐变
var g = ctx.createLinearGradient(0, 0, 480, 0);
//创建径向渐变
var g1 = ctx.createRadialGradient(300, 160, 10, 300, 160, 240);
//设置颜色
g.addColorStop(0, "black");
g.addColorStop(1, "white");
ctx.fillStyle = g;
//绘制矩形
ctx.rect((600-w)*0.5, 30, w, 80);
ctx.fill();
ctx.beginPath();
//定义基本色
var colors=["aqua", "black", "blue", "fuchsia", "gray", "green", "lime", "maroon",
"navy", "olive", "purple", "red", "silver", "teal", "white", "yellow"];
var step = 1/colors.length;
for(var i=0;i<colors.length;i++)
{
g1.addColorStop(i*step, colors[i]);
}
//ctx.arc(300, 200, 100, 0, Math.PI*2, true);
//绘制矩形
ctx.rect((600-w)*0.5, 120, w, 80);
ctx.fillStyle = g1;
ctx.fill();
</script>
效果如图2-12所示。
以上是使用渐变颜色进行填充,另外一种填充方式是使用一张图片作为贴图进行填充,使用的API如下。
• createPattern (image,repetition):image 表示需要填充的图像,可以是img、canvas、video元素等;repetition定义图像按照什么方式贴图,通常的贴图方式有以下几种。
repeat:水平和垂直方向重复贴图,默认值。
repeat-x:水平方向重复贴图。
repeat-y:垂直方向重复贴图。
no-repeat:使用一次贴图。
通过createPattern方法创建了一个模式对象后,就可以通过fillStyle或者strokeStyle等属性指定,然后就可以使用指定的图形进行填充。
以下代码创建了两个分别使用stroke()和fill()填充的图形:
<body>
<h2>Pattern</h2>
<canvas id="can" width="600" height="300"></canvas>
</body>
<script>
var ctx = can.getContext('2d');
var imgSrc =["img/t1.png", "img/f1.png"];
var ctx = can.getContext("2d");
//创建Image对象和pattern对象
for(var i=0;i<imgSrc.length;i++)
{
var img=new Image();
img.src = imgSrc[i];
img.onload = (function(im, i){
var self = im;
return function(){
var p = ctx.createPattern(self, "repeat");
if(i==0)
{
ctx.beginPath();
ctx.fillStyle = p;
ctx.fillRect(38, 38, 520, 232);
}
else
{
ctx.beginPath();
ctx.strokeStyle = p;
ctx.lineWidth = 18;
ctx.strokeRect(28, 28, 540, 250);
}
}
}(img, i));
}
</script>
效果如图2-13所示。
conext中有一些全局的属性,如前面提到的strokeStyle、fillStyle、lineWidth等。当我们进行绘图的时候,有时,在改变这些值之前,需要保存上一次绘图的状态,下次绘制的时候又需要进行恢复,这种情况很常见。当然,不需要我们自己定义一个全局的对象进行保存,context中本身定义了以下方法用于保存和恢复canvas的状态。
• save():把当前绘图状态压到绘图状态堆中。
• restore():弹出绘图状态堆最上面保存的绘图状态。
状态堆中包含以下部分。
• 当前的transformation matrix(换矩阵)前的clipping region(区域)。
• 当前的属性值:fillStyle、font、globalAlpha、globalCompositeOperation、lineCap、lineJoin、lineWidth、miterLimit、shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY、strokeStyle、textAlign、textBaseline。
为了避免本次绘图状态影响到下次绘图,通常情况下在绘图之前,都会使用 contex.save()方法保存当前绘图状态,绘制完成后再使用context.restore()进行恢复。
对于图形的操作,在HTML4时代可以使用SVG进行矢量绘图,而canvas除了支持矢量图形外,还可以直接针对图像以及像素操作,这才是canvas强大的地方。接下来,看看canvas关于图像处理的部分。
除了绘制常用的图形以外,canvas提供了一系列的API能够对图像进行操作,常见的图像API有以下3个方法。
• drawImage (image, dx, dy):把image 图像绘制到画布上(dx, dy)坐标位置。
• drawImage (image, dx, dy, w, h):把image 图像绘制到画布上(dx, dy)坐标位置,图像的宽度是w,高度是h。
• drawImage (image, sx, sy, sw, sh, dx, dy, dw,dh):截取image 图像以(sx, sy)为左上角坐标,宽度为 sw,高度为 sh 的一块矩形区域绘制到画布上(dx, dy)坐标位置,图像宽度是dw,高度是dh。
其中 image 可以是 htmlImageElement 元素、htmlcanvasElement元素、htmlVideoElement元素, htmlVideoElement元素是HTML5中新增加的video视频播放元素。
图 2-14 显示了 drawImage 中源图像和目标canvas之间的关系。
采用后面两种方式进行绘制的时候可以实现图像的放大和缩小。
先看一个简单的例子,我们把<image/>标签中的图像显示到一个canvas中。
<body>
<h2>图片显示</h2>
<img src="img/t1.jpg" id="img1" />
<input type="button" id="btnCopy" value="拷贝图片" /><br>
<canvas id="can" width="400" height="300" ></canvas>
</body>
<script>
function $(id)
{
return document.getElementById(id);
}
$("btnCopy").onclick = function()
{
//获取canvas的上下文
var ctx = $("can").getContext("2d");
//设置canvas的大小
$("can").width = $("img1").width;
$("can").height = $("img1").height;
ctx.drawImage($("img1"), 0, 0);
}
</script>
代码中有一个图片元素、一个按钮和一个 canvas 元素,当点击按钮的时候首先获取了 id为img1的图片的宽度和高度,然后改变canvas的大小,最后使用drawImage方法把img的图片绘制到canvas 元素中,使用drawImage 把img1 绘制到canvas坐标(0, 0)的位置,大小和原图像一样大。
现在,我们可以借助drawImage方法实现一个简单的放大镜效果,效果如图2-15所示。
在这个放大镜效果中,我们可以在任意一张图片上点击,在该位置就会出现一个圆形的放大镜。实现原理也比较简单,首先,需要获取鼠标当前的位置,然后根据放大镜的尺寸,截取图片中相应大小的图片,最后通过drawImage方法把图片中的部分绘制到canvas上面。
这里涉及一个问题,放大镜是圆形的,但实际上如果直接通过canvas绘制,将最终以矩形的方式显示。这里,我们可以使用剪裁的方式进行绘制,所谓剪裁,就是在canvas中指定一块区域,在这个区域之内的都会显示,之外的一律不显示,所以如果我们把剪裁区域设置成圆形就可以了。canvas中使用context.clip()方法定义一个画布的剪裁路径,方法没有任何参数,它会使用当前的路径作为一个剪裁的区域,默认情况下,整个 canvas 本身就是一个剪裁路径。如图2-16所示,图中定义了一个圆形的剪裁区域,在圆形之外的区域将不会显示。
好了,有了以上的基本知识,现在可以开始制作一个放大镜了,具体代码如下:
<body>
<h2>放大镜</h2>
<img src="img/tx1.jpg" id="s1"></img>
</body>
<script>
//根据编号获取对象
function $(id)
{
return document.getElementById(id);
}
//定义放大镜
var Glass = {
bind:function(imgId, zRat)
{
var self=this;
this.canvas = document.createElement("canvas");
this.canvas.style.display="none";
this.canvas.style.position="absolute";
this.ctx = this.canvas.getContext("2d");
this.canvas.width = 100;
this.canvas.height = 100;
this.hEle = $(imgId);
//设置放大比例
this.zRat = zRat|2;
//设置鼠标按下事件
document.body.appendChild(this.canvas);
document.body.onmousedown=function(e){
if(e.srcElement.id==imgId){
e.preventDefault();
draw(e);
//定义绘制方法
function draw(e)
{
//获取鼠标位置
var x = e.pageX, y = e.pageY;
//获取图片相对位置
var exOff = x-self.hEle.offsetLeft,
eyOff = y-self.hEle.offsetTop;
//设置获取图片周围长度
var rLen = 50/self.zRat;
self.copyImg(exOff-rLen, eyOff-rLen, rLen*2, rLen*2);
self.show(x-50, y-50);
}
document.body.onmousemove = draw;
document.body.onmouseup = function(){
self.hide();
document.body.onmousemove = null;
};
}
};
},
copyImg:function(x, y, w, h)
{
this.ctx.arc(50, 50, 50, 0, Math.PI*2, true);
//设置路径剪裁形成圆形
this.ctx.clip();
this.ctx.drawImage(this.hEle, x, y, w, h, 0, 0, 100, 100);
},
show:function(x, y)
{
this.canvas.style.display="block";
this.canvas.style.pixelLeft = x;
this.canvas.style.pixelTop = y;
},
hide:function()
{
this.canvas.style.display="none";
}
};
Glass.bind("s1");
</script>
除了实现静态的效果外,还可以通过drawImage实现动画效果。
来看一个比较复杂的例子,该例子中实现了一个简单的人物动画,先简单了解一下动画的原理。通常我们看到的动画称为逐帧动画,它是利用人眼睛视觉暂留的原理,即物体被移动后其形象在人眼视网膜上还可有约1秒的停留。利用这个原理,我们在一秒钟内如果连续放映20张静态的图片,这样就形成了动画的效果,当然 20 是最基本的要求,如果流畅的话至少要 30帧/秒,也就是说每秒钟放30张静态图片,每一帧图片称为一帧,在第4章的制作游戏引擎中会详细介绍动画机制。
本例子中我们有3张人物行走的图片,这3张图片构成了人物行走的过程,为了提高效率通常情况下制作动画的时候,会把人物的动作放在一张图片中,如图2-17所示(该图截取自游戏《超级玛丽》)。
这张图片包含了玛丽行走的动画,一共由3帧组成,我们在canvas上循环绘制这三张图片就形成了玛丽行走的动画,代码如下:
<body>
<h2>图片动画</h2>
<img id="img1" src="img/mr.png" />
<input id="btnGO" type="button" value="开始" /><br>
<canvas id="c1" width="320" height="200" ></canvas><br>
</body>
<script>
//是否开始动画
var isAnimStart = false, animHandle = null;//动画句柄
//保存每帧动画起始坐标,本例图片共有3帧
var frames=[
[0, 0],
[32, 0],
[64, 0]
];
//定义每帧图像的宽度和高度
var fWidth = 32, fHeight = 32;
function $(id)
{
return document.getElementById(id);
}
//开始
function init()
{
//注册GO按钮事件
$("btnGO").onclick=function()
{
//如果没开始动画,则开始动画
if(!isAnimStart)
{
//获取canvas上下文
var ctx = $("c1").getContext("2d");
//设置当前帧序号
var fIndex = 0;
//找到canvas的中点
var cX = 160, cY = 100;
animHandle = setInterval(function(){
//先清空画布
ctx.clearRect(0, 0, 320, 200);
//绘制当前帧
ctx.drawImage(img1, frames[fIndex][0], frames[fIndex][1], fWidth, fHeight, cX-64, cY-64, fWidth*4, fHeight*4);
//计算下一帧
fIndex++;
if(fIndex>=frames.length)
{
fIndex=0;
}
}, 100)
$("btnGO").value = "停止";
isAnimStart = true;
}
else
{
$("btnGO").value="开始";
clearInterval(animHandle);
isAnimStart = false;
}
}
}
init();
</script>
来看看代码功能,首先定义了几个变量,isAnimStart标记是否已经开始播放动画标记,用来切换播放和停止动画。animHandle保存定时器的句柄,可以通过它关闭定时器,定义frames数组变量记录图片中3个动作帧左上角的起始坐标,在绘制动画的时候,需要根据这个坐标来截取图片中的每帧图像。fWidth和fHeight分别表示每帧图片的宽度和高度,本例中使用的玛丽图像帧是32×32大小。然后定义init初始化方法,在该方法中绑定了开始按钮的事件,为了实现动画的播放,使用了serInterval方法,该方法每100毫秒执行一次,每次读取frames中的一帧图像,并把它显示在画布上,其核心代码是:
//先清空画布
ctx.clearRect(0, 0, 320, 200);
//绘制当前帧
ctx.drawImage(img1, frames[fIndex][0], frames[fIndex][1], fWidth,fHeight, cX-64,
cY-64, fWidth*4, fHeight*4);
这段代码首先使用ctx.clearRect方法清空整个画布,因为在定时器动画中需要每100毫秒更新画布中的内容,那么每次更新时,需要先把上个画面清除掉,然后再进行绘制,否则会把几张动画重叠起来。然后使用了ctx.drawImage方法,把img1图片中的内容复制到canvas上面,复制的时候第1个参数是选取的图片,第2~5的参数分别对应着img1图片上的一块矩形区域,最后4个参数对应着canvas上面的一块矩形区域,实际上就是把图片上的某一矩形区域部分绘制到canvas上面,由于画到canvas上面的宽度和高度分别是fWidth×4和fHeight×4,实际上也就是把原图像放大了4倍。
除了直接对图片元素操作外,canvas还提供了直接对像素元素进行处理的API。
canvas另外一个非常强大的功能就是可以对图像中的任一个像素进行处理。我们知道图像实际上是由很多像素点组成,一幅宽度为320,高度200的图片是由320×200=64 000 个像素点构成,每个像素由red、green、blue三种颜色组成。
canvas提供了以下API让我们可以进行像素操作。
• getImageData (sx, sy, sw, sh):获取canvas 上以(sx, sy)为左上角,宽度为sw,高度为sh的一块矩形区域的像素数据。通过getImageData获取到了imageData对象,该对象有以下3个属性。
width:每行的像素数量。
height:每列的像素数量。
data:存有从canvas中获取的每个像素的RGBA的值,该数组为每个像素保存了四个值,分别是红色、绿色、蓝色和alpha透明度,每个值在0~255之间,数组填充的数据是从上到下,从左到右,比如 imgData.data[0]~imgData.data[3]就保存了canvas 图像中左上角第一个像素点的 RGBA 的数据, imgData.data[4]~imgData.data[7]保存了canvas图像中左上角第二个像素点的RGBA的数据,依次类推,从左到右,从上到下。
• createImageData (sw, sh):创建一个宽度为sw,高度为sh的imageData对象,该对象中所有的像素都是黑色的。
• createImageData (imageData):创建一个 imageData 对象的副本,像素值和imageData的一致。
• putImageData (imageData, dx, dy, [dirtyX, dirtyY, dirtyWidth, dirtyHeight]):在绘图画布上绘制给定的 ImageData 对象。假如脏矩形被提供,则只有在脏矩形上面的像素被绘制。本方法对全局透明、阴影和全局组合等属性均忽略。
data数组中的数据可以获取也可以设置,我们可以把data中的数据进行修改后重新绘制达到修改图像的目的。
利用可以对像素操作的特性,我们可以完成一些简单的图像处理效果,类似于 Photoshop中的滤镜效果,如转成灰度图、浮雕效果等,接下来看看相关的例子。
现在来看一个简单的像素操作的例子,该例子中会把任意一张彩色图片转换成灰度图片,如图2-18所示。
彩色图像要转换成灰度图,需要对图中的每一个像素点进行处理,我们可以通过 getImageData()方法获取每个点的像素值,然后把该点的色彩转成灰度。彩色转灰度的算法很多,这里,我们采用以下算法:
灰度值=(R×30 + G×59 + B×11 + 50) / 100
具体实现代码如下:
<body>
<h2>像素操作</h2>
<img id="img1" src="img/t1.jpg" />
<input id="btnGO" type="button" value="转成灰度图" /><br>
<canvas id="c1" width="320" height="200" ></canvas><br>
</body>
<script>
function $(id)
{
return document.getElementById(id);
}
//开始
function init()
{
//注册GO按钮事件
$("btnGO").onclick=function()
{
c1.width=img1.width;
c1.height = img1.height;
//先把image绘制到canvas上
var ctx = c1.getContext("2d");
ctx.drawImage(img1, 0, 0, c1.width, c1.height);
//获取像素数据
var imgData = ctx.getImageData(0, 0, c1.width, c1.height);
for(var i = 0;i<imgData.data.length;i+=4)
{
//获取RGB像素值
var r = imgData.data[i],
g = imgData.data[i+1],
b = imgData.data[i+2];
//计算灰度值,常用公式 Gray = (R*30 + G*59 + B*11 + 50) / 100
var gray = (r*30+g*59+b*11+50)*0.01;
imgData.data[i] = gray;
imgData.data[i+1] = gray;
imgData.data[i+2] = gray;
}
//最后把imgdata数据绘制到canvas中
ctx.putImageData(imgData, 0, 0);
}
}
init();
代码先通过contex.drawImage把图像绘制到canvas上,需要注意的是,只有canvas上有图像了才能通过getImageData获取canvas的图像数据,然后定义imgData变量得到canvas的图像数据,接着通过循环imgData数据,修改canvas图像中的每个像素数据。因为每个图像数据由4个值构成,所以循环data数据每次加4,然后接着获取每个像素的RGB值,然后通过公式转成灰度。
把彩色信息转成灰度信息后,最后重新设置每个像素的颜色值,就完成了彩色图到灰度图的转变。我们虽然改变了图像的像素值,但是还不能让图像立刻变成我们想要的样子,还需要重新把像素的数据绘制到canvas中,所以我们使用了putImageData方法。
这样就完成了整个的图像转变过程。
接下来,来实现一个浮雕效果,浮雕效果如图2-19所示。
浮雕效果的实现原理也有很多种,常用的算法是把每个点周围的8个点和一个转换矩阵进行卷积操作,得到的值作为该点的新色彩。但计算量过大,这里采用一种相对简单的算法,该算法是这样的,对于任一点的像素来说,新的色彩值等于该点的色彩和右边像素的色彩值相减,然后加上128。
具体实现如下:
<body>
<h2>像素操作</h2>
<img id="img1" src="img/t1.jpg" />
<input id="btnGO" type="button" value="转成浮雕图" />
<canvas id="c1" width="320" height="200" ></canvas><br>
</body>
<script>
function $(id)
{
return document.getElementById(id);
}
//开始
function init()
{
//注册GO按钮事件
$("btnGO").onclick=function()
{
c1.width=img1.width;
c1.height = img1.height;
//先把image绘制到canvas上
var ctx = c1.getContext("2d");
ctx.drawImage(img1, 0, 0, c1.width, c1.height);
//获取像素数据
var imgData = ctx.getImageData(0, 0, c1.width, c1.height);
var iData = imgData.data;
for(var i = 0;i<img1.height-1;i++)
{
for(var j=0;j<img1.width;j++)
{
//获取像素在dataImage起始位置
var start = (i*img1.width+j)<<2;
var r = iData[start]-iData[start+4]+128,
g = iData[start+1]-iData[start+5]+128,
b = iData[start+2]-iData[start+6]+128;
//越界处理
r = (r<0)?0:(r>255)?255:r;
g = (g<0)?0:(g>255)?255:g;
b = (b<0)?0:(b>255)?255:b;
//转灰度图
var g=(r*30+g*59+b*11+50)*0.01;
iData[start] = g;
iData[start+1] = g;
iData[start+2] = g;
}
}
//最后把imgdata数据绘制到canvas中
ctx.putImageData(imgData, 0, 0);
}
}
init();
</script>
需要注意的是,在得到新的像素点色彩值之后,需要进行越界处理,并转成灰度色彩,否则,该像素点还有可能是彩色点。
接下来,来看看坐标变换相关的一些知识。
在绘制图像的过程中,经常可能需要对图像进行旋转、缩放等变形处理,canvas也提供了一系列的API帮助我们完成这些操作。
关于画布的坐标变换,canvas提供了以下常用的API,这些API的操作必须要在绘制之前调用,否则不会产生任何效果。
• translate (x, y):平移,把画布的原点坐标移动到(x, y)位置,x 表示将坐标原点向左移x个像素,y表示将坐标原点向下移动y个像素。正常情况下canvas的原点坐标位于左上角,那么我们可以通过translate方法移动原点的坐标,比如以下代码就把原点坐标移动到了canvas的中间位置:
context.translate(canvas.width*0.5, canvas.height*0.5);
• scale (x, y):缩放,把画布放大。x表示水平方向放大倍数,y表示垂直方向放大倍数,如果需要缩小,将这两个参数设置为0~1之间的数就可以了,比如context.scale (0.5, 0.5)表示把图形缩小一半。
• rotate (angle):把图形进行旋转。angle表示旋转的角度,旋转的中心点是原点,旋转是以顺时针方向进行,angle 设置成负数就是逆时针旋转。需要注意的是 angle 是以弧度表示,比如要顺时针旋转90 度可以使用context.rotate (Math.PI*0.5)。
• transform (m11, m12, m21, m22, dx, dy):把当前的矩阵乘上如图2-20 所示的矩阵,做矩阵叠加操作。
• setTransform (m11, m12, m21, m22, dx, dy):设置当前的变换矩阵为图2-20 所示的矩阵。
以下代码在canvas的中间位置绘制了棵小树:
<body>
<h2>图片显示</h2>
<img src="img/t1.png" id="img1" /><br>
<canvas id="can" width="300" height="200"></canvas>
</body>
<script>
function $(id)
{
return document.getElementById(id);
}
document.body.onload = function()
{
//获取canvas的上下文
var ctx = $("can").getContext("2d");
//获取canvas中心
var cx = $("can").width*0.5,
cy = $("can").height*0.5;
//获取图片的宽高度
var iw = $("img1").width,
ih = $("img1").height;
//平移到中心
ctx.translate(cx, cy);
ctx.drawImage($("img1"), -iw*0.5, -ih*0.5);
}
</script>
代码中,首先计算出canvas 的中心坐标(cx, cy),然后把canvas画布原点移动到点(cx, cy)。由于画布的原点移动到点(cx, cy)位置,那么,drawImage (img, dx, dy)表示的是把图片以(dx, dy)为左上角开始绘制。所以,如果要绘制到中央,需要偏移半个宽度和高度,即使用坐标点(−w/2, −h/2)。最终效果如图2-21 所示,黑线表示canvas 的边框。
rotate 方法描述的是图像绕原点旋转,假设要实现绕某一个图像的中心点旋转,如何实现呢?对了,我们可以先把画布的原点移动到图像的中心点,然后就可以进行旋转操作了。
以下代码绘制了在画布中央,一个绕自身中心旋转并且放大了一倍的小树。
<body>
<h2>图片显示</h2>
<img src="img/t1.png" id="img1" /><br>
<canvas id="can" width="300" height="200"></canvas>
</body>
<script>
function $(id)
{
return document.getElementById(id);
}
//定义旋转角度
var deg = 0;
//绘制图像
function draw()
{
//保存变换矩阵
ctx.save()
//清除屏幕
ctx.clearRect(0, 0, 300, 200);
ctx.translate(cx, cy);
ctx.scale(2, 2);
ctx.rotate(deg*Math.PI/180);
ctx.drawImage($("img1"), -iw*0.5, -ih*0.5);
//恢复变换矩阵
ctx.restore();
}
document.body.onload = function()
{
//获取canvas的上下文
ctx = $("can").getContext("2d");
//获取canvas中心
cx = $("can").width*0.5,
cy = $("can").height*0.5;
//获取图片的宽高度
iw = $("img1").width,
ih = $("img1").height;
window.setInterval(function(){
deg+=2;
draw();
}, 30);
}
</script>
这段代码中,我们定义了一个 draw 方法,用于绘制图像,在绘制图像的过程中,首先保存当前的绘图状态,然后把画布移动到中心,进行旋转和放大,最后恢复画布状态。注意,如果不进行保存和恢复状态的操作,则由于使用这些变换操作都将改变当前变换矩阵的状态,从而影响下一次的变换。
定义了draw方法后,设置了一个定时器,每30毫秒改变全局的角度,形成一个旋转的动画,具体的效果如图2-22所示。
本质上,前面所介绍的 rotate()、scale()、translate()等方法最终都会转换成矩阵的形式进行操作,这种对应如下所示:
translate(x, y) ⇒ transform(1, 0, 0, 1, x, y)
scale(x, y) ⇒ transform(sx, 0, 0, sy, 0, 0)
rotate(deg) ⇒ transform(cos(deg), sin(deg), -sin(deg), cos(deg), 0, 0)
由于本质上这些变换操作都会进行矩阵和三角函数的相关运算,特别是旋转操作,所以这些变换操作将消耗更多的时间,特别是在手持设备上使用更为明显。
canvas中除了可以绘制图形图像外,还可以绘制文字,同时也可以指定文字的字体、大小、对齐方式以及填充文字的纹理。
• fillText (text, x, y, [maxWidth]):在canvas 上填充文字,text表示需要绘制的文字,x、y分别表示绘制在 canvas 上的横、纵坐标,最后一个参数可选,表示显示文字的最大宽度,防止文字显示溢出。
• strokeText (text, x, y, [maxWidth]):在canvas 上描边文字,参数的意义同fillText。相关的文字的一些属性如下所示。
• font:字体属性,可以采用类似CSS中font定义的写法,比如bold 40px sans-serif表示文字是粗体,大小是40像素,字体是sans-serif。
• textAlign:对齐方式,常见的值如表2-2 所示。
• textBaseline:文字对齐基线,常见的值如表2-3 所示。
比如,以下代码显示了Welcome To HTML5的文字:
<body>
<h2>显示文字</h2>
<canvas id="can" width="400" height="300" />
</body>
<script>
function $(id)
{
return document.getElementById(id);
}
//获取canvas的上下文
var ctx = $("can").getContext("2d");
ctx.strokeStyle = "red";
ctx.font = "bold 40px sans-serif";
ctx.strokeText("Welcome To HTML5", 10, 50);
</script>
代码中通过strokeStyle设置文字的颜色为红色,然后通过font设置文字的字体,最后通过strokeText描绘出文字的边框。
在context中还有一些常见的全局属性,做一些了解。
• globalAlpha:透明度,这个值用来设置在画布上绘制的透明度,值的范围从0~1之间,使用这个属性我们可以完成一些常见的效果,比如游戏中常见的淡入淡出效果。
以下代码展示了一张图片淡入,也就是逐渐显示的效果。
<body>
<h2>淡入</h2>
<img src="img/t1.jpg" id="img1" style="display:none" /><br>
<canvas id="can" width="300" height="200"></canvas>
</body>
<script>
function $(id)
{
return document.getElementById(id);
}
function draw()
{
//清除屏幕
ctx.drawImage($("img1"), 0, 0);
}
document.body.onload = function()
{
//获取canvas的上下文
ctx = $("can").getContext("2d");
//获取图片的宽高度
$("can").width = $("img1").width,
$("can").height = $("img1").height;
ctx.globalAlpha = 0;
var tHandle = window.setInterval(function(){
ctx.globalAlpha+=0.05;
draw();
if(ctx.globalAlpha>=0.95)
{
window.clearInterval(tHandle);
}
}, 100);
}
</script>
• globalCompositeOperation:全局混合模式,这个属性定义了如果在画布上绘制多个图像时,图像进行叠加的方式,也称为混合模式,类似于Photoshop这种图像处理软件中图层的叠加模式。表2-4列出了混合模式的可能值,表2-4中,正方形表示目标图像,圆形表示源图像。
在这一小节中,我们将利用前面介绍的知识,来创作一个《你画我猜》游戏中的主要功能。《你画我猜》是一款老少皆宜的多人在线的网络游戏,2012 年风靡一时,玩法其实也来源于生活当中,经常在娱乐节目中出现。通常在节目中是这样玩的,主持人写出一个词语,然后由一个参与者根据这个词语画出相应的图案,由另一个参与者来根据这个图案猜出这个词语,而《你画我猜》就把现实生活中的这个玩法转到了电脑上,玩法就是这么简单。当然,本章还无法开始做出一个网络游戏,哪怕是一个简单的单机游戏,本书第10章有《你画我猜》这个游戏完整的实现,本章将会完成《你画我猜》中的画板部分。没玩过《你画我猜》游戏的,也应该用过Windows的画板吧?好了,下面我们来实现这个画板。
首先,我们来设计一个画板的主页面,关于使用div布局游戏的UI不是本书的重点,我们只关注游戏逻辑的实现,以下代码只是一个参考实现:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>你画我猜</title>
<link href="images/CSS.css" rel="stylesheet" type="text/css">
</head>
<body>
<!--主容器层-->
<div id="main" class="main">
<div>
<div class="tx"></div>
<div class="wz">
<div id="dround"></div>
<!--显示问题区域-->
<span id="question"></span>
</div>
<!--显示时间-->
<div id="qTime" class="time">60</div>
<div class="tc">离开房间<img src="images/edit_undo.png" align="absmiddle"></div>
</div>
<div class="qc"></div>
<div>
<!--绘图主层-->
<div class="hbbox">
<!--绘图区域-->
<div id="hb" class="hb">
<canvas id="paintArea" width="525" height="370" ></canvas>
</div>
</div>
<!--工具选项区-->
<div class="hbr">
<div id="operDiv">
<!--颜色选取区-->
<div id="ys" class="ys"></div>
<!--画笔大小选择区-->
<div id="bc" class="bc"></div>
<!--定义橡皮擦工具-->
<div id="ssx" class="tb">
<div id="btnRub" class="tb2"><img src="images/01945.png"></div>
<div id="btnClear" class="tb3"><img src="images/053753321.gif"></div>
</div>
</div>
<!--消息发布主层-->
<div id="msgArea" class="msgArea"></div>
<div class="bd">
<input type="button" id="btnSendMsg" value="发送"><input type="text" size="13"
id="txtMsg">
</div>
</div>
</div>
</div>
</body>
设计好以上的主页面后,得到如图2-23所示的图,当然,样式部分不是重点,所以忽略,我们需要了解这个画板中和程序相关的部分。
好了,定义好图2-23所示的UI界面后,开始实现功能。
首先,我们把整个画板看成一个画板对象,这个画板对象中有一些基本的属性,如画板的大小,当前画笔使用的颜色、宽度等,于是就有了以下的Paint类:
var Painter = {
//绑定的环境上下文
ctx:null,
//宽度
w:0,
//高度
h:0,
//当前画笔颜色
bColor:null,
//当前画笔大小
bWidth:null
}
画板定义好了以后,我们需要逐渐往里面增加相应的功能代码。
接下来,我们需要动态产生颜色区域以及画笔区域,因为在前面设计 UI 的时候,只给这两个区域留下了空白容器,需要我们通过代码来产生。于是,可以给Painter定义一个初始化画笔的方法,具体的代码如下:
//初始化画笔
initBrush:function()
{
//定义画板颜色
var bColor = ["#000000", "#999999", "#FFFFFF", "#FF0000", "#FF9900", "#FFFF00",
"#008000", "#00CCFF", "#0099FF", "#FF33CC", "#CC66FF", "#FFCCCC", "#6633FF", "#CCFFCC"];
var bDiv = $("#ys"),
self = this;
//产生颜色层
for(var i=0;i<bColor.length;i++)
{
var b = $("<div class='bys'></div>").css("background-color", bColor[i]);
//修改颜色
b.on("click", function(){
//触发更新画板状态事件
self.fire("onPaintUpdate", {"color":$(this).css("background-color")});
});
bDiv.append(b);
}
//绑定画笔大小
var bWidth = [2, 8, 16, 24];
var bcDiv = $("#bc");
for(i = 0;i<bWidth.length;i++)
{
var bw = $("<div class='bwid' data-bidx='"+(i)+"'></div>");
bw.css("background-image", "url(images/bc"+(i+1)+".png)");
//修改画笔大小
bw.on("click", function(){
//触发更新画板状态事件
self.fire("onPaintUpdate", {"width":bWidth[this.getAttribute("data-bidx")]});
});
bcDiv.append(bw);
}
}
以上函数中,我们创建了颜色区域和画笔区域中的选择层部分,当用户鼠标点击颜色选择区域的时候,就会使用fire方法触发一个onPaintUpdate事件,修改当前用户画笔的颜色。同样,当用户鼠标点击画笔选择区域时,也会触发onPaintUpdate事件,修改当前用户画笔的大小。fire方法是Paint类中自定义的一个方法,用于处理画板中的各种自定义事件的触发,具体实现如下:
//触发画板事件
fire:function(eventName, param)
{
if(this[eventName])
{
this[eventName](param);
}
}
本质上,fire方法就是调用相应的事件处理程序,相应的,我们需要添加一个onPatintUpdate方法,相关代码如下:
//画板更新事件,当画板的参数比如画笔颜色,大小改变时触发
onPaintUpdate:function(data)
{
var w=data.width||this.bWidth,
c = data.color||this.bColor;
var param = {"width":w, "color":c};
//设置画笔大小
this.setBrushWidth(w);
//设置画笔颜色
this.setBrushColor(c);
}
在这个更新画笔的方法中,需要根据传入的参数调用 setBrushWidth()和 setBrushColor()方法更新当前画笔的色彩和大小,这两个方法的具体代码如下:
//设置画笔颜色
setBrushColor:function(color)
{
this.bColor=color||"black";
this.ctx.strokeStyle = this.bColor;
},
//设置画笔宽度
setBrushWidth:function(width)
{
this.bWidth=width||1;
this.ctx.lineWidth = this.bWidth;
}
到此为止,关于画笔颜色和大小的选择已经完成,接下来,我们看看如何实现用户在画板上用画笔绘画。
使用画笔在画板上绘制,从原理上来说,canvas上是可以画图形的,所以我们只要在鼠标的移动事件上面做文章就可以了。鼠标在canvas上面移动的时候,可以跟踪当前鼠标的坐标,然后在这些坐标点上绘制点就可以了,这是我们最初的想法,但实际上这种效果并不理想。因为,首先,canvas 并没有提供画点的方法,当然,可以通过绘制一个极小的矩形实现,但这不是主要问题。主要问题在于,虽然我们移动鼠标是连续移动,但电脑没有足够高的灵敏度捕捉鼠标的移动轨迹。事实上,如果采用这种方法,将会在canvas上留下断断续续的点的轨迹,这不是我们要的结果,我们需要的是把这些点用线连起来。所以最终的算法是,捕捉鼠标移动的坐标,然后把当前点和上一个点用线连接起来,最终,就可以达到一个比较好的效果。
根据以上的原理部分,我们在Paint类中增加一个initCanvas()方法,用于初始化画板,绑定鼠标在canvas上面的相关事件,其实现代码如下:
//初始化画板
initCanvas:function()
{
//绑定绘图canvas
var can = $("#paintArea"), self = this;
//绑定鼠标按下时间
can.on("mousedown", function(e){
e.preventDefault();
this.x = e.offsetX,
this.y = e.offsetY;
self.fire("onStartDraw", {"x":this.x, "y":this.y});
//绑定鼠标移动事件
can.on("mousemove", function(e){
var nx = event.offsetX, ny = event.offsetY;
self.fire("onDrawing", {"x":nx, "y":ny});
this.x = nx;
this.y = ny;
});
//绑定鼠标抬起事件
can.on("mouseup", function(){
//取消鼠标移动事件
can.off("mousemove");
});
})
}
这里,我们在canvas中绑定了鼠标按下、移动和抬起事件,在按下和移动的时候,触发了onStartDraw和onDrawing方法,这两个方法主要用于绘制线条,代码如下:
//开始画画事件
onStartDraw:function(data){
//开始路径
this.ctx.beginPath();
this.ctx.moveTo(data.x, data.y);
},
//画画事件
onDrawing:function(data)
{
this.ctx.lineTo(data.x, data.y);
this.ctx.stroke();
}
主要绘制部分的工作,基本都完成了。噢,别忘了,还有一个擦除区域。当我们绘制错误的时候,需要擦除图像,于是,我们可以完成初始化擦除区域的方法如下:
//初始化橡皮擦
initEraser:function()
{
var self=this;
//绑定清除屏幕事件
$("#btnClear").click(function(){
self.clear();
});
//擦除
$("#btnRub").click(function(){
self.setBrushColor("white");
self.setBrushWidth(32);
});
}
好了,大功告成,为了让Paint类开始工作,我们最好写一个init()方法,完成以上全部的初始化工作,以便让用户方便使用,init()方法可参考完整代码部分。
好了,整个Painter对象就完成了,最终,我们得到的Paint对象的完整代码如下:
(function(){
//定义画板对象
var Painter = {
//绑定的环境上下文
ctx:null,
//宽度
w:0,
//高度
h:0,
//当前画笔颜色
bColor:null,
//当前画笔大小
bWidth:null,
//初始化
init:function()
{
var can=$("#paintArea")[0];
this.ctx = can.getContext("2d");
this.w = can.width;
this.h = can.height;
this.setBGColor();
this.setBrushColor();
this.setBrushWidth();
this.ctx.lineCap = "round";
this.ctx.lineJoin = "round";
//初始化画板事件
this.initCanvas();
//初始化画笔颜色
this.initBrush();
//初始化橡皮擦
this.initEraser();
},
//初始化画笔
initBrush:function()
{
//定义画板颜色
var bColor = ["#000000", "#999999", "#FFFFFF", "#FF0000", "#FF9900", "#FFFF00",
"#008000", "#00CCFF", "#0099FF", "#FF33CC", "#CC66FF", "#FFCCCC",
"#6633FF", "#CCFFCC"];
var bDiv = $("#ys"),
self = this;
//产生颜色层
for(var i=0;i<bColor.length;i++)
{
var b=$("<div class='bys'></div>").css("background-color", bColor[i]);
//修改颜色
b.on("click", function(){
//触发更新画板状态事件
self.fire("onPaintUpdate", {"color":$(this).css("background-color")});
});
bDiv.append(b);
}
//绑定画笔大小
var bWidth = [2, 8, 16, 24];
var bcDiv = $("#bc");
for(i = 0;i<bWidth.length;i++)
{
var bw=$("<div class='bwid' data-bidx='"+(i)+"'></div>");
bw.css("background-image", "url(images/bc"+(i+1)+".png)");
//修改画笔大小
bw.on("click", function(){
//触发更新画板状态事件
self.fire("onPaintUpdate", {"width":bWidth[this.getAttribute("data-bidx")]});
});
bcDiv.append(bw);
}
},
//初始化橡皮擦
initEraser:function()
{
var self=this;
//绑定清除屏幕事件
$("#btnClear").click(function(){
self.clear();
});
//擦除
$("#btnRub").click(function(){
self.setBrushColor("white");
self.setBrushWidth(32);
});
},
//设置背景颜色
setBGColor:function(color){
this.ctx.fillStyle = color||"white";
this.ctx.fillRect(0, 0, this.w, this.h);
},
//设置画笔颜色
setBrushColor:function(color)
{
this.bColor=color||"black";
this.ctx.strokeStyle = this.bColor;
},
//设置画笔宽度
setBrushWidth:function(width)
{
this.bWidth=width||1;
this.ctx.lineWidth = this.bWidth;
},
//初始化画板
initCanvas:function()
{
//绑定绘图canvas
var can = $("#paintArea"), self = this;
//绑定鼠标按下时间
can.on("mousedown", function(e){
e.preventDefault();
this.x = e.offsetX,
this.y = e.offsetY;
self.fire("onStartDraw", {"x":this.x, "y":this.y});
//绑定鼠标移动事件
can.on("mousemove", function(e){
var nx = event.offsetX, ny = event.offsetY;
self.fire("onDrawing", {"x":nx, "y":ny});
this.x = nx;
this.y = ny;
});
//绑定鼠标抬起事件
can.on("mouseup", function(){
//取消鼠标移动事件
can.off("mousemove");
});
})
},
//清除canvas
clear:function()
{
this.ctx.clearRect(0, 0, this.w, this.h);
},
//触发画板事件
fire:function(eventName, param)
{
if(this[eventName])
{
this[eventName](param);
}
},
//开始画画事件
onStartDraw:function(data){
//开始路径
this.ctx.beginPath();
this.ctx.moveTo(data.x, data.y);
},
//画画事件
onDrawing:function(data)
{
this.ctx.lineTo(data.x, data.y);
this.ctx.stroke();
},
//画板更新事件,当画板的参数比如画笔颜色,大小改变时触发
onPaintUpdate:function(data)
{
var w=data.width||this.bWidth, c=data.color||this.bColor;
var param = {"width":w,"color":c};
//设置画笔大小
this.setBrushWidth(w);
//设置画笔颜色
this.setBrushColor(c);
}
}
//画板初始化
Painter.init();
window.Painter = Painter;
}())
以上的Paint类在painter.js文件中,最后,完整的主页面代码index.htm如下:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>你画我猜</title>
<link href="images/CSS.css" rel="stylesheet" type="text/css">
</head>
<body>
<!--主容器层-->
<div id="main" class="main">
<div>
<div class="tx"></div>
<div class="wz">
<div id="dround"></div>
<!--显示问题区域-->
<span id="question"></span>
</div>
<!--显示时间-->
<div id="qTime" class="time">60</div>
<div class="tc">离开房间<img src="images/edit_undo.png" align="absmiddle"></div>
</div>
<div class="qc"></div>
<div>
<!--绘图主层-->
<div class="hbbox">
<!--绘图区域-->
<div id="hb" class="hb">
<canvas id="paintArea" width="525" height="370" ></canvas>
</div>
</div>
<!--工具选项区-->
<div class="hbr">
<div id="operDiv">
<!--颜色选取区-->
<div id="ys" class="ys"></div>
<!--画笔大小选择区-->
<div id="bc" class="bc"></div>
<!--定义橡皮擦工具-->
<div id="ssx" class="tb">
<div id="btnRub" class="tb2"><img src="images/01945.png"></div>
<div id="btnClear" class="tb3"><img src="images/053753321.gif"></div>
</div>
</div>
<!--消息发布主层-->
<div id="msgArea" class="msgArea"></div>
<div class="bd">
<input type="button" id="btnSendMsg" value="发送"><input type="text" size="13"
id="txtMsg">
</div>
</div>
</div>
</div>
</body>
<script src="../js/jquery.js" charset="utf-8"></script>
<script src="../js/painter.js" charset="utf-8"></script>
<script>
//检测是否支持HTML5
function isSupportHTML5()
{
return (typeof(Worker) !=="undefined");
}
//初始化
$(document).ready(function(){
if(!isSupportHTML5())
{
alert("您的浏览器不支持HTML5,请更换浏览器!");
}
else
{
$("#main").show();
}
});
</script>
</html>
至此,《你画我猜》的绘制部分已经完成,至于多人游戏的部分,在学习了后面的网络编程部分后,我们会得到一个完整的案例。
本章介绍了canvas的一些常用的操作,作为HTML5的最重要的特性,canvas使得在Web中开发免插件的较高性能的游戏提供了条件,接下来的第3章中,我们介绍HTML5中的多媒体元素。
图书在版编目(CIP)数据
HTML5游戏编程核心技术与实战/向峰编著.--北京:人民邮电出版社,2013.10
(游戏设计与开发技术丛书)
ISBN 978-7-115-32701-7
Ⅰ.①H… Ⅱ.①向… Ⅲ.①超文本标记语言—程序设计 Ⅳ.①TP312
中国版本图书馆CIP数据核字(2013)第181387号
内容提要
这是一本全面介绍HTML5游戏编程的书,在详细阐述HTML5的核心技术基础上,深入讲解游戏的运行机制,剖析游戏的核心——游戏引擎的细节内幕,并以大量完整的游戏实践开发案例为指导,逐步讲解游戏开发中常用的各种技术和方法。
全书一共11章,先后介绍了HTML5的新特性、HTML5中的canvas绘图技术、多媒体技术、游戏运行机制及游戏渲染引擎的开发、HTML5中的网络通信基础和Node.js框架、游戏中常用的算法技巧、物理引擎Box2D创建物理游戏、使用CSS3创建游戏,然后给出一个飞行射击游戏的综合案例。此外,在最后两章分别通过Node.js结合socket.js框架实现了两个游戏。每一章的内容既包括丰富的理论知识,又给出实战性极强的案例。
本书适合有一定HTML和JavaScript语言基础,对HTML5游戏编程有浓厚兴趣的Web前端开发工程师阅读,同样适合有一定的HTML5游戏开发基础的HTML5游戏开发的工程师阅读。
◆编著 向峰
责任编辑 杨海玲
责任印制 程彦红 杨林杰
◆人民邮电出版社出版发行 北京市崇文区夕照寺街14号
邮编 100061 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
北京天宇星印刷厂印刷
◆开本:800×1000 1/16
印张:24
字数:558千字 2013年10月第1版
印数:1-3500册 2013年10月北京第1次印刷
定价:59.00元
读者服务热线:(010)67132692 印装质量热线:(010)67129223
反盗版热线:(010)67171154