HTML5-画布秘籍-全-

HTML5 画布秘籍(全)

原文:zh.annas-archive.org/md5/5BECA7AD01229D44A883D4EFCAD8E67B

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

HTML5 画布正在改变网络上的图形和可视化。由 JavaScript 驱动,HTML5 Canvas API 使 Web 开发人员能够在浏览器中创建可视化和动画,而无需 Flash。尽管 HTML5 Canvas 迅速成为在线图形和交互的标准,但许多开发人员未能充分利用这一强大技术所提供的所有功能。

《HTML5 Canvas Cookbook》首先介绍了 HTML5 Canvas API 的基础知识,然后提供了处理 API 不直接支持的高级技术,如动画和画布交互的方法。最后,它提供了一些最常见的 HTML5 画布应用的详细模板,包括数据可视化、游戏开发和 3D 建模。它将使您熟悉有趣的主题,如分形、动画、物理学、颜色模型和矩阵数学。

通过本书的学习,您将对 HTML5 Canvas API 有扎实的理解,并掌握了创建任何类型的 HTML5 画布应用的技术,仅受想象力的限制。

本书内容

第一章,“开始使用路径和文本”,首先介绍了子路径绘制的基础知识,然后通过探索绘制锯齿和螺旋的算法来深入研究更高级的路径绘制技术。接下来,本章深入探讨了文本绘制,最后探索了分形。

第二章,“形状绘制和合成”,首先介绍了形状绘制的基础知识,并向您展示如何使用颜色填充、渐变填充和图案。接下来,本章深入研究了透明度和合成操作,然后提供了绘制更复杂形状的方法,如云、齿轮、花朵、纸牌花色,甚至是一个完整的矢量飞机,包括图层和阴影。

第三章,“使用图像和视频”,介绍了图像和视频处理的基础知识,向您展示如何复制和粘贴画布的部分,并涵盖了不同类型的像素操作。本章还向您展示了如何将图像转换为数据 URL,将画布绘制保存为图像,并使用数据 URL 加载画布。最后,本章以一个可以用于动态聚焦和模糊图像的像素操作的像素化图像焦点算法结束。

第四章,“掌握变换”,探索了画布变换的可能性,包括平移、缩放、旋转、镜像变换和自由形式变换。此外,本章还详细探讨了画布状态堆栈。

第五章,“使用动画使画布栩栩如生”,首先构建一个Animation类来处理动画阶段,并向您展示如何创建线性运动、二次运动和振荡运动。接下来,它涵盖了一些更复杂的动画,如肥皂泡的振荡、摆动的钟摆和旋转的机械齿轮。最后,本章以创建自己的粒子物理模拟器的方法结束,并提供了在画布内创建数百个微生物以测试性能的方法。

第六章,与画布交互:将事件侦听器附加到形状和区域,首先构建了一个扩展画布 API 的Events类,提供了一种在画布上附加事件侦听器到形状和区域的方法。接下来,该章节涵盖了获取画布鼠标坐标的技术,检测区域事件,检测图像事件,检测移动触摸事件和拖放。该章节最后提供了一个创建图像放大器的方法和另一个创建绘图应用程序的方法。

第七章,创建图表和图表,提供了生产就绪的图表类,包括饼图、条形图、方程图和折线图。

第八章,用游戏开发拯救世界,通过展示如何创建一个名为 Canvas Hero 的整个横向卷轴游戏,让您开始使用画布游戏开发。该章节向您展示如何创建精灵表,创建关卡和边界地图,创建处理英雄、坏人、关卡和英雄生命的类,还向您展示如何使用 MVC(模型视图控制器)设计模式构建游戏引擎。

第九章,介绍 WebGL,首先构建了一个 WebGL 包装类,以简化 WebGL API。该章节通过展示如何创建一个 3D 平面和一个旋转的立方体来介绍 WebGL,还向您展示如何向模型添加纹理和光照。该章节最后展示了如何创建一个完整的 3D 世界,您可以在其中进行第一人称探索。

附录 A,附录 B 和附录 C 讨论了其他特殊主题,如画布支持检测、安全性、画布与 CSS3 过渡和动画,以及移动设备上画布应用的性能。

您需要什么

要开始使用 HTML5 画布,您只需要一个现代浏览器,如 Google Chrome,Firefox,Safari,Opera 或 IE9,以及一个简单的文本编辑器,如记事本。

这本书适合谁

本书面向熟悉 HTML 和 JavaScript 的 Web 开发人员。它适用于初学者和有一定 JavaScript 工作知识的 HTML5 开发人员。

HTML5 画布是什么?

Canvas 最初是由苹果于 2004 年创建的,用于实现 Dashboard 小部件并在 Safari 浏览器中提供图形支持,后来被 Firefox、Opera 和 Google Chrome 采用。如今,画布是下一代 Web 技术的新 HTML5 规范的一部分。

HTML5 画布是一个 HTML 标签,您可以将其嵌入到 HTML 文档中,用于使用 JavaScript 绘制图形。由于 HTML5 画布是一个位图,绘制到画布上的每个像素都会覆盖其下的像素。

这是本书所有 2D HTML5 画布配方的基本模板:

<!DOCTYPE HTML>
<html>
    <head>
        <script>
            window.onload = function(){
                var canvas = document.getElementById("myCanvas");
                var context = canvas.getContext("2d");

                // draw stuff here
            };
        </script>
    </head>
    <body>
        <canvas id="myCanvas" width="578" height="200">
        </canvas>
    </body>
</html>

请注意,画布元素嵌入在 HTML 文档的主体内,并且使用idwidthheight进行定义。JavaScript 使用id引用画布标签,widthheight用于定义绘图区域的大小。一旦使用document.getElementById()访问了画布标签,我们就可以定义一个 2D 上下文:

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

尽管本书大部分内容涵盖了 2D 上下文,但最后一章使用了 3D 上下文来使用 WebGL 渲染 3D 图形。

约定

在本书中,您会发现一些区分不同信息类型的文本样式。以下是一些这些样式的示例,以及它们的含义解释。

文本中的代码词显示如下:“定义Events构造函数。”

代码块设置如下:

var Events = function(canvasId){
    this.canvas = document.getElementById(canvasId);
    this.context = this.canvas.getContext("2d");
    this.stage = undefined;
    this.listening = false;
};

当我们希望引起您对代码块的特定部分的注意时,相关行或项目会以粗体显示:

var Events = function(canvasId){
 this.canvas = document.getElementById(canvasId);
 this.context = this.canvas.getContext("2d");
    this.stage = undefined;
    this.listening = false;
};

新术语重要单词以粗体显示。例如,屏幕上看到的单词,在菜单或对话框中出现在文本中,就像这样:“它在原点处写出文本Hello Logo!

注意

警告或重要说明会出现在这样的框中。

提示

提示和技巧会以这种方式出现。

第一章:开始使用路径和文本

在这一章中,我们将涵盖:

  • 绘制一条线

  • 绘制一条弧线

  • 绘制二次曲线

  • 绘制贝塞尔曲线

  • 绘制锯齿

  • 绘制螺旋

  • 使用文本

  • 使用阴影绘制 3D 文本

  • 释放分形的力量:绘制一棵幽灵树

介绍

本章旨在通过一系列逐渐复杂的任务来演示 HTML5 画布的基本功能。HTML5 画布 API 提供了绘制和样式化不同类型子路径的基本工具,包括线条、弧线、二次曲线和贝塞尔曲线,以及通过连接子路径创建路径的方法。该 API 还提供了对文本绘制的良好支持,具有几种样式属性。让我们开始吧!

绘制一条线

当第一次学习如何使用 HTML5 画布绘制时,大多数人都对绘制最基本和最原始的画布元素感兴趣。这个配方将向您展示如何通过绘制简单的直线来做到这一点。

绘制一条线

如何做...

按照以下步骤绘制一条对角线:

  1. 定义一个 2D 画布上下文并设置线条样式:
window.onload = function(){
  // get the canvas DOM element by its ID
     var canvas = document.getElementById("myCanvas");
  // declare a 2-d context using the getContext() method of the 
  // canvas object
     var context = canvas.getContext("2d");

  // set the line width to 10 pixels
     context.lineWidth = 10;
  // set the line color to blue
     context.strokeStyle = "blue";
  1. 定位画布上下文并绘制线条:
  // position the drawing cursor
     context.moveTo(50, canvas.height - 50);
  // draw the line
     context.lineTo(canvas.width - 50, 50);
  // make the line visible with the stroke color
     context.stroke();
};
  1. 将画布标签嵌入到 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

注意

下载示例代码

您可以从www.html5canvastutorials.com/cookbook运行演示并下载本书的资源,或者您可以从您在www.PacktPub.com购买的所有 Packt 图书的帐户中下载示例代码文件。如果您在其他地方购买了本书,您可以访问www.PacktPub.com/support并注册,以便直接通过电子邮件接收文件。

它是如何工作的...

从前面的代码中可以看出,我们需要等待页面加载完成,然后再尝试通过其 ID 访问画布标签。我们可以通过window.onload初始化器来实现这一点。页面加载完成后,我们可以使用document.getElementById()访问画布 DOM 元素,并通过将2d传递给画布对象的getContext()方法来定义一个 2D 画布上下文。正如我们将在最后两章中看到的,我们还可以通过传递其他上下文(如webglexperimental-webgl等)来定义 3D 上下文。

在绘制特定元素(如路径、子路径或形状)时,重要的是要理解样式可以在任何时候设置,无论是在元素绘制之前还是之后,但是样式必须在元素绘制后立即应用才能生效,我们可以使用lineWidth属性设置线条的宽度,使用strokeStyle属性设置线条颜色。想象一下,这个行为就像我们在纸上画东西时会采取的步骤。在我们开始画之前,我们会选择一个带有特定尖端厚度的彩色标记(strokeStyle)。

现在我们手里有了标记,可以使用moveTo()方法将其定位到画布上:

context.moveTo(x,y);

将画布上下文视为绘图光标。moveTo()方法为给定点创建一个新的子路径。画布左上角的坐标为(0,0),右下角的坐标为(画布宽度,画布高度)。

一旦我们定位了绘图光标,我们可以使用lineTo()方法绘制线条,定义线条终点的坐标:

context.lineTo(x,y);

最后,为了使线条可见,我们可以使用stroke()方法。除非另有规定,默认的描边颜色是黑色。

总结一下,当使用 HTML5 画布 API 绘制线条时,我们应该遵循的典型绘制过程如下:

  1. 样式你的线条(比如选择一个特定尖端厚度的彩色标记)。

  2. 使用moveTo()定位画布上下文(就像把标记放在纸上)。

  3. 使用lineTo()绘制线条。

  4. 使用stroke()使线条可见。

还有更多...

HTML5 画布线条也可以具有三种不同的线帽,包括buttroundsquare。线帽样式可以使用画布上下文的lineCap属性进行设置。除非另有规定,线帽样式默认为 butt。下图显示了三条线,每条线都具有不同的线帽样式。顶部线使用默认的 butt 线帽,中间线使用 round 线帽,底部线使用 square 线帽:

还有更多...

请注意,中间和底部线比顶部线稍长,尽管所有线宽度相等。这是因为 round 线帽和 square 线帽会使线的长度增加,增加的量等于线的宽度。例如,如果我们的线长为 200 像素,宽度为 10 像素,并且使用 round 或 square 线帽样式,那么结果线的长度将为 210 像素,因为每个线帽都会增加 5 像素的线长。

另请参阅...

  • 绘制锯齿

  • *将所有内容放在一起:在第二章中绘制喷气式飞机

绘制一条弧

在使用 HTML5 画布绘制时,有时需要绘制完美的弧。如果你对绘制快乐的彩虹、笑脸或图表感兴趣,这个方法将是你努力的良好起点。

绘制一条弧

如何做...

按照以下步骤绘制简单的弧:

  1. 定义一个 2D 画布上下文并设置弧线样式:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
    context.lineWidth = 15;
    context.strokeStyle = "black"; // line color
  1. 绘制弧:
context.arc(canvas.width / 2, canvas.height / 2 + 40, 80, 1.1 * Math.PI, 1.9 * Math.PI, false);
    context.stroke();
};
  1. 将 canvas 标签嵌入 HTML 文档的 body 中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

工作原理...

我们可以使用arc()方法创建 HTML5 弧,该方法由虚拟圆的圆周部分定义。看一下下面的图表:

工作原理...

虚拟圆由一个中心点和一个半径定义。圆周部分由起始角度、结束角度以及弧是顺时针绘制还是逆时针绘制来定义:

context.arc(centerX,centerY, radius, startingAngle, 
      endingAngle,counterclockwise);

注意,角度从圆的右侧 0π开始,顺时针移动到 3π/2、π、π/2,然后返回 0。对于这个方法,我们使用 1.1π作为起始角度,1.9π作为结束角度。这意味着起始角度略高于虚拟圆左侧的中心,结束角度略高于虚拟圆右侧的中心。

还有更多...

起始角度和结束角度的值不一定要在 0π和 2π之间。实际上,起始角度和结束角度可以是任何实数,因为角度可以在围绕圆圈旋转时重叠。

例如,假设我们将起始角度定义为 3π。这相当于围绕圆圈一周(2π)再围绕圆圈半周(1π)。换句话说,3π等同于 1π。另一个例子,-3π也等同于 1π,因为角度沿着圆圈逆时针旋转一周半,最终到达 1π。

使用 HTML5 画布创建弧的另一种方法是利用arcTo()方法。arcTo()方法生成的弧由上下文点、控制点、结束点和半径定义:

context.arcTo(controlPointX1, controlPointY1, endingPointX,   endingPointY, radius);

arc()方法不同,arcTo()方法依赖于上下文点来定位弧,类似于lineTo()方法。arcTo()方法在创建路径或形状的圆角时最常用。

另请参阅...

  • *在第二章中绘制一个圆

  • *在第五章中制作机械齿轮动画

  • *在第五章中制作时钟动画

绘制二次曲线

在这个配方中,我们将学习如何绘制二次曲线。与其表亲弧线相比,二次曲线提供了更多的灵活性和自然的曲线,是创建自定义形状的绝佳工具。

绘制二次曲线

操作步骤...

按照以下步骤绘制二次曲线:

  1. 定义一个 2D 画布上下文并设置曲线样式:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    context.lineWidth = 10;
    context.strokeStyle = "black"; // line color
  1. 定位画布上下文并绘制二次曲线:
context.moveTo(100, canvas.height - 50);
    context.quadraticCurveTo(canvas.width / 2, -50, canvas.width - 100, canvas.height - 50);
    context.stroke();
};
  1. 将 canvas 标签嵌入 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

工作原理...

HTML5 二次曲线由上下文点、一个控制点和一个结束点定义:

  context.quadraticCurveTo(controlX, controlY, endingPointX,       endingPointY);

查看以下图表:

工作原理...

二次曲线的曲率由三个特征切线定义。曲线的第一部分与一条虚拟线相切,该虚拟线从上下文点开始,到控制点结束。曲线的顶点与从中点 1 开始到中点 2 结束的虚拟线相切。最后,曲线的最后一部分与从控制点开始到结束点结束的虚拟线相切。

另请参阅...

  • 将所有内容放在一起:在第二章中绘制喷气机

  • 解锁分形的力量:绘制一棵幽灵树

绘制贝塞尔曲线

如果二次曲线不能满足您的需求,贝塞尔曲线可能会起作用。贝塞尔曲线也被称为三次曲线,是 HTML5 画布 API 中最先进的曲线。

绘制贝塞尔曲线

操作步骤...

按照以下步骤绘制任意贝塞尔曲线:

  1. 定义一个 2D 画布上下文并设置曲线样式:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    context.lineWidth = 10;
    context.strokeStyle = "black"; // line color
    context.moveTo(180, 130);
  1. 定位画布上下文并绘制贝塞尔曲线:
context.bezierCurveTo(150, 10, 420, 10, 420, 180);
    context.stroke();
};
  1. 将 canvas 标签嵌入 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

工作原理...

HTML5 画布贝塞尔曲线由上下文点、两个控制点和一个结束点定义。与二次曲线相比,额外的控制点使我们对其曲率有更多控制:

  context.bezierCurveTo(controlPointX1, controlPointY1, 
      controlPointX2, controlPointY2, 
      endingPointX, endingPointY);

查看以下图表:

工作原理...

与二次曲线不同,贝塞尔曲线由五个特征切线定义,而不是三个。曲线的第一部分与一条虚拟线相切,该虚拟线从上下文点开始,到第一个控制点结束。曲线的下一部分与从中点 1 开始到中点 3 结束的虚拟线相切。曲线的顶点与从中点 2 开始到中点 4 结束的虚拟线相切。曲线的第四部分与从中点 3 开始到中点 5 结束的虚拟线相切。最后,曲线的最后一部分与从第二个控制点开始到结束点结束的虚拟线相切。

另请参阅...

  • 随机化形状属性:在第二章中绘制一片花海

  • 将所有内容放在一起:在第二章中绘制喷气机

绘制锯齿

在这个配方中,我们将通过迭代连接线子路径来介绍路径绘制,以绘制锯齿路径。

绘制锯齿

操作步骤...

按照以下步骤绘制锯齿路径:

  1. 定义一个 2D 画布上下文并初始化锯齿参数:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    var startX = 85;
    var startY = 70;
    var zigzagSpacing = 60;
  1. 定义锯齿样式并开始路径:
context.lineWidth = 10;
    context.strokeStyle = "#0096FF"; // blue-ish color
    context.beginPath();
    context.moveTo(startX, startY);
  1. 绘制七条连接的锯齿线,然后使用stroke()使锯齿路径可见:
// draw seven lines
    for (var n = 0; n < 7; n++) {
        var x = startX + ((n + 1) * zigzagSpacing);
        var y;

        if (n % 2 == 0) { // if n is even...
            y = startY + 100;
        }
        else { // if n is odd...
            y = startY;
        }
        context.lineTo(x, y);
    }

    context.stroke();
};
  1. 将 canvas 标签嵌入 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

工作原理...

要绘制锯齿,我们可以连接交替的对角线以形成路径。通过设置一个循环来实现,该循环在奇数迭代上向上和向右绘制对角线,在偶数迭代上向下和向右绘制对角线。

在这个示例中需要注意的关键事项是beginPath()方法。这个方法本质上声明正在绘制一个路径,以便每个线段子路径的结束定义下一个子路径的开始。如果不使用beginPath()方法,我们将不得不费力地使用moveTo()来定位每个线段,同时确保前一个线段的结束点与当前线段的起点匹配。正如我们将在下一章中看到的,beginPath()方法也是创建形状的必要步骤。

线连接样式

注意每个线段之间的连接是如何形成尖锐点的。这是因为 HTML5 canvas 路径的线连接样式默认为miter。或者,我们也可以使用画布上下文的lineJoin属性将线连接样式设置为roundbevel

如果您的线段相当细,并且不以陡峭的角度连接,要区分不同的线连接样式可能有些困难。通常,当路径厚度超过 5 像素且线段之间的角度相对较小时,不同的线连接样式会更加明显。

绘制螺旋线

注意,这个示例可能会引起催眠。在这个示例中,我们将通过连接一系列短线段来形成螺旋路径来绘制一个螺旋线。

绘制螺旋线

如何做...

按照以下步骤绘制一个居中的螺旋线:

  1. 定义一个 2D 画布上下文并初始化螺旋参数:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    var radius = 0;
    var angle = 0;
  1. 设置螺旋线样式:
context.lineWidth = 10;
    context.strokeStyle = "#0096FF"; // blue-ish color
    context.beginPath();
    context.moveTo(canvas.width / 2, canvas.height / 2);
  1. 围绕画布中心旋转三次(每次完整旋转 50 次迭代),同时增加半径 0.75,并使用lineTo()从上一个点到当前点绘制一条线段。最后,使用stroke()使螺旋线可见:
for (var n = 0; n < 150; n++) {
        radius += 0.75;
        // make a complete circle every 50 iterations
        angle += (Math.PI * 2) / 50;
        var x = canvas.width / 2 + radius * Math.cos(angle);
        var y = canvas.height / 2 + radius * Math.sin(angle);
        context.lineTo(x, y);
    }

    context.stroke();
};
  1. 将 canvas 标签嵌入 HTML 文档的 body 中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

要使用 HTML5 canvas 绘制螺旋线,我们可以将绘图游标放在画布中心,迭代增加半径和角度,然后从上一个点到当前点绘制一个超短的线段。另一种思考方式是想象自己站在人行道上,手里拿着一支彩色粉笔。弯下腰把粉笔放在人行道上,然后开始围绕中心转圈(不要转得太快,除非你想晕倒)。当你转动时,把粉笔向外移动。几圈之后,你就画出了一个漂亮的小螺旋线。

处理文本

几乎所有的应用程序都需要一些文本来有效地向用户传达信息。这个示例将向您展示如何绘制一个简单的文本字符串,带有一种乐观的欢迎。

处理文本

如何做...

按照以下步骤在 canvas 上写字:

  1. 定义一个 2D 画布上下文并设置文本样式:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    context.font = "40pt Calibri";
    context.fillStyle = "black";
  1. 水平和垂直对齐文本,然后绘制它:
// align text horizontally center
    context.textAlign = "center";
    // align text vertically center
    context.textBaseline = "middle";
    context.fillText("Hello World!", canvas.width / 2, 120);
};
  1. 将 canvas 标签嵌入 HTML 文档的 body 中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

要在 HTML5 canvas 上绘制文本,我们可以使用font属性定义字体样式和大小,使用fillStyle属性定义字体颜色,使用textAlign属性定义水平文本对齐,使用textBaseline属性定义垂直文本对齐。textAlign属性可以设置为leftcenterrighttextBaseline属性可以设置为tophangingmiddlealphabeticideographicbottom。除非另有规定,否则textAlign属性默认为lefttextBaseline属性默认为 alphabetic。

还有更多...

除了fillText()之外,HTML5 canvas API 还支持strokeText()

  context.strokeText("Hello World!", x, y);

这种方法将为文本的周边着色而不是填充。要为 HTML 画布文本设置填充和描边,可以同时使用fillText()strokeText()方法。在渲染描边厚度时,最好先使用fillText()方法,然后再使用strokeText()方法。

另请参阅...

  • 带阴影的 3D 文字绘制

  • 第四章 中创建镜像变换

  • 第四章 中绘制简单的标志并随机化其位置、旋转和比例

带阴影的 3D 文字绘制

如果 2D 文本不能激发你的热情,你可以考虑绘制 3D 文本。尽管 HTML5 画布 API 并没有直接为我们提供创建 3D 文本的手段,但我们可以使用现有的 API 创建自定义的draw3dText()方法。

带阴影的 3D 文字绘制

如何做...

按照以下步骤创建 3D 文本:

  1. 设置画布上下文和文本样式:
  window.onload = function(){
    canvas = document.getElementById("myCanvas");
    context = canvas.getContext("2d");

    context.font = "40pt Calibri";
    context.fillStyle = "black";
  1. 对齐并绘制 3D 文本:
// align text horizontally center
    context.textAlign = "center";
    // align text vertically center
    context.textBaseline = "middle";
    draw3dText(context, "Hello 3D World!", canvas.width / 2, 120, 5);
};
  1. 定义draw3dText()函数,绘制多个文本层并添加阴影:
function draw3dText(context, text, x, y, textDepth){
    var n;

    // draw bottom layers
    for (n = 0; n < textDepth; n++) {
        context.fillText(text, x - n, y - n);
    }

    // draw top layer with shadow casting over
    // bottom layers
    context.fillStyle = "#5E97FF";
    context.shadowColor = "black";
    context.shadowBlur = 10;
    context.shadowOffsetX = textDepth + 2;
    context.shadowOffsetY = textDepth + 2;
    context.fillText(text, x - n, y - n);
}
  1. 在 HTML 文档的主体中嵌入画布标记:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

要使用 HTML5 画布绘制 3D 文本,我们可以将多个相同文本的图层叠加在一起,以创建深度的错觉。在这个示例中,我们将文本深度设置为五,这意味着我们的自定义draw3dText()方法会在一起叠加五个“Hello 3D World!”的实例。我们可以将这些图层着色为黑色,以在文本下方创建黑暗的错觉。

接下来,我们可以添加一个有颜色的顶层来描绘一个朝前的表面。最后,我们可以通过设置画布上下文的shadowColorshadowBlurshadowOffsetXshadowOffsetY属性,在文本下方应用柔和的阴影。正如我们将在后面的示例中看到的,这些属性不仅限于文本,还可以应用于子路径、路径和形状。

释放分形的力量:绘制一棵幽灵树

首先,什么是分形?如果你还不知道,分形是数学与艺术相结合的令人惊叹的结果,可以在构成生活的各种模式中找到。从算法上讲,分形是基于经历递归的方程。在这个示例中,我们将通过绘制一个分叉成两个分支的树干,然后从我们刚刚绘制的两个分支中再绘制两个分支,来创建一个有机的树。

释放分形的力量:绘制一棵幽灵树

如何做...

按照以下步骤绘制使用分形的树:

  1. 创建一个递归函数,绘制一个分叉成两个分支的单个分支,然后递归调用自身,从分叉分支的端点绘制另外两个分支:
function drawBranches(context, startX, startY, trunkWidth, level){
    if (level < 12) {
        var changeX = 100 / (level + 1);
        var changeY = 200 / (level + 1);

        var topRightX = startX + Math.random() * changeX;
        var topRightY = startY - Math.random() * changeY;

        var topLeftX = startX - Math.random() * changeX;
        var topLeftY = startY - Math.random() * changeY;
        // draw right branch
        context.beginPath();
        context.moveTo(startX + trunkWidth / 4, startY);
        context.quadraticCurveTo(startX + trunkWidth / 4, startY - trunkWidth, topRightX, topRightY);
        context.lineWidth = trunkWidth;
        context.lineCap = "round";
        context.stroke();

        // draw left branch
        context.beginPath();
        context.moveTo(startX - trunkWidth / 4, startY);
        context.quadraticCurveTo(startX - trunkWidth / 4, startY -
        trunkWidth, topLeftX, topLeftY);
        context.lineWidth = trunkWidth;
        context.lineCap = "round";
        context.stroke();

        drawBranches(context, topRightX, topRightY, trunkWidth * 0.7, level + 1);
        drawBranches(context, topLeftX, topLeftY, trunkWidth * 0.7, level + 1);
    }
}
  1. 初始化画布上下文,并通过调用drawBranches()开始绘制树分形:
window.onload = function(){
    canvas = document.getElementById("myCanvas");
    context = canvas.getContext("2d");

    drawBranches(context, canvas.width / 2, canvas.height, 50, 0);
};
  1. 在 HTML 文档的主体中嵌入画布标记:
<canvas id="myCanvas" width="600" height="500" style="border:1px solid black;">
</canvas>

它是如何工作的...

要使用分形创建树,我们需要设计定义树的数学特性的递归函数。如果你花一点时间研究一棵树(如果你仔细想想,它们是相当美丽的),你会注意到每个分支都分叉成更小的分支。反过来,这些分支又分叉成更小的分支,依此类推。这意味着我们的递归函数应该绘制一个分叉成两个分支的单个分支,然后递归调用自身,从我们刚刚绘制的两个分支中再绘制两个分支。

现在我们有了创建分形的计划,我们可以使用 HTML5 画布 API 来实现它。绘制一个分叉成两个分支的最简单方法是通过绘制两个二次曲线,这些曲线从彼此弯曲向外。

如果我们对每次迭代使用完全相同的绘图过程,我们的树将会是完全对称且相当无趣的。为了使我们的树看起来更自然,我们可以引入随机变量来偏移每个分支的结束点。

还有更多...

这个配方的有趣之处在于每棵树都是不同的。如果你自己编写这个代码并不断刷新你的浏览器,你会发现每棵树的形成都是完全独特的。你可能还会对调整分支绘制算法以创建不同类型的树,甚至在最小的分支尖端绘制叶子感兴趣。

一些其他很好的分形例子可以在海贝壳、雪花、羽毛、植物、晶体、山脉、河流和闪电中找到。

第二章:形状绘制和复合

在本章中,我们将涵盖:

  • 绘制矩形

  • 绘制圆形

  • 使用自定义形状和填充样式

  • 贝塞尔曲线的乐趣:绘制云

  • 绘制透明形状

  • 使用上下文状态堆栈保存和恢复样式

  • 使用复合操作

  • 使用循环创建图案:绘制齿轮

  • 随机化形状属性:绘制一片花田

  • 创建自定义形状函数:纸牌花色

  • 将所有内容组合在一起:绘制喷气机

介绍

在第一章路径和文本入门中,我们学习了如何绘制子路径,如线条、弧线、二次曲线和贝塞尔曲线,然后学习了如何将它们连接在一起形成路径。在本章中,我们将专注于基本和高级形状绘制技术,如绘制矩形和圆形、绘制自定义形状、填充形状、使用复合操作和绘制图片。让我们开始吧!

绘制矩形

在本示例中,我们将学习如何绘制 HTML5 画布 API 提供的唯一内置形状,即矩形。尽管矩形可能看起来不那么令人兴奋,但许多应用程序以某种方式使用它们,因此您最好熟悉一下。

绘制矩形

如何做...

按照以下步骤在画布上绘制一个简单的居中矩形:

  1. 定义 2D 画布上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 使用rect()方法绘制一个矩形,使用fillStyle属性设置填充颜色,然后使用fill()方法填充形状:
    context.rect(canvas.width / 2 - 100, canvas.height / 2 - 50, 200, 100);
    context.fillStyle = "#8ED6FF";
    context.fill();
    context.lineWidth = 5;
    context.strokeStyle = "black";
    context.stroke();
};
  1. 将画布标签嵌入 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

从前面的代码中可以看出,我们可以使用rect()方法来绘制一个简单的矩形:

context.rect(x,y,width,height);

rect()方法在位置x,y处绘制一个矩形,并使用widthheight定义其大小。在本示例中需要注意的另一件重要的事情是使用fillStylefill()。与strokeStylestroke()类似,我们可以使用fillStyle方法分配填充颜色,并使用fill()填充形状。

提示

请注意,我们在stroke()之前使用了fill()。如果我们在填充形状之前描边形状,填充样式实际上会覆盖描边样式的一半,有效地减半了使用lineWidth设置的线宽样式。因此,最好在使用stroke()之前使用fill()

还有更多...

除了rect()方法,还有两种额外的方法可以用一行代码绘制矩形并应用样式,即fillRect()方法和strokeRect()方法。

fillRect()方法

如果我们打算在使用rect()绘制矩形后填充它,我们可以考虑使用fillRect()方法同时绘制和填充矩形:

context.fillRect(x,y,width,height);

fillRect()方法相当于使用rect()方法后跟fill()。在使用此方法时,您需要在调用它之前定义填充样式。

strokeRect()方法

除了fillRect()方法,我们还可以使用strokeRect()方法一次绘制矩形并描边:

context.strokeRect(x,y,width,height);

strokeRect()方法相当于使用rect()方法后跟stroke()。与fillRect()类似,您需要在调用此方法之前定义描边样式。

提示

不幸的是,HTML5 画布 API 不支持同时填充和描边矩形的方法。个人而言,我喜欢使用rect()方法,并根据需要使用stroke()fill()应用描边样式和填充,因为这更符合自定义形状绘制的一致性。但是,如果您想要在使用这些简写方法之一时同时应用描边和填充矩形,最好使用fillRect()后跟stroke()。如果您使用strokeRect()后跟fill(),您会通过填充覆盖描边样式,使描边线宽减半。

另见...

  • 在第五章中创建线性运动

  • 在第六章中检测区域事件

  • 在第七章中创建条形图

绘制一个圆

尽管 HTML5 画布 API 不支持圆形方法,但我们可以通过绘制完全封闭的弧线来创建一个圆。

绘制一个圆

如何做...

按照以下步骤绘制一个居中在画布上的圆:

  1. 定义一个 2D 画布上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 使用arc()方法创建一个圆,使用fillStyle属性设置颜色填充,然后使用fill()方法填充形状:
    context.arc(canvas.width / 2, canvas.height / 2, 70, 0, 2 * Math.PI, false);
    context.fillStyle = "#8ED6FF";
    context.fill();
    context.lineWidth = 5;
    context.strokeStyle = "black";
    context.stroke();
};
  1. 将画布标签嵌入到 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

工作原理...

正如您可能还记得的那样,我们可以使用arc()方法创建一个弧线,该方法绘制由起始角和结束角定义的圆的一部分。然而,如果我们将起始角和结束角之间的差定义为 360 度(2π),我们将有效地绘制了一个完整的圆:

context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);

另请参阅...

  • 使用循环创建图案:绘制齿轮

  • 将圆形变换为椭圆在第四章中

  • 在第五章中摆动钟摆

  • 在第五章中模拟粒子物理

  • 在第五章中制作动画时钟

  • 在第六章中检测区域事件

  • 在第七章中创建饼图

使用自定义形状和填充样式

在这个配方中,我们将绘制四个三角形,然后用不同的填充样式填充每一个。HTML5 画布 API 提供的填充样式包括颜色填充、线性渐变、径向渐变和图案。

使用自定义形状和填充样式

如何做...

按照以下步骤绘制四个三角形,一个用颜色填充,一个用线性渐变填充,一个用径向渐变填充,一个用图案填充:

  1. 创建一个绘制三角形的简单函数:
function drawTriangle(context, x, y, triangleWidth, triangleHeight, fillStyle){
    context.beginPath();
    context.moveTo(x, y);
    context.lineTo(x + triangleWidth / 2, y + triangleHeight);
    context.lineTo(x - triangleWidth / 2, y + triangleHeight);
    context.closePath();
    context.fillStyle = fillStyle;
    context.fill();
}
  1. 定义一个 2D 画布上下文,并设置三角形的高度、宽度和 y 位置:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    var grd;
    var triangleWidth = 150;
    var triangleHeight = 150;
    var triangleY = canvas.height / 2 - triangleWidth / 2;
  1. 使用颜色填充绘制三角形:
    // color fill (left)
    drawTriangle(context, canvas.width * 1 / 5, triangleY, triangleWidth, triangleHeight, "blue");
  1. 使用线性渐变填充绘制三角形:
    // linear gradient fill (second from left)
    grd = context.createLinearGradient(canvas.width * 2 / 5, triangleY, canvas.width * 2 / 5, triangleY + triangleHeight);
    grd.addColorStop(0, "#8ED6FF"); // light blue
    grd.addColorStop(1, "#004CB3"); // dark blue
    drawTriangle(context, canvas.width * 2 / 5, triangleY, triangleWidth, triangleHeight, grd);
  1. 使用径向渐变填充绘制三角形:
    // radial gradient fill (second from right)
    var centerX = (canvas.width * 3 / 5 +
    (canvas.width * 3 / 5 - triangleWidth / 2) +
    (canvas.width * 3 / 5 + triangleWidth / 2)) / 3;

    var centerY = (triangleY +
    (triangleY + triangleHeight) +
    (triangleY + triangleHeight)) / 3;

    grd = context.createRadialGradient(centerX, centerY, 10, centerX, centerY, 100);
    grd.addColorStop(0, "red");
    grd.addColorStop(0.17, "orange");
    grd.addColorStop(0.33, "yellow");
    grd.addColorStop(0.5, "green");
    grd.addColorStop(0.666, "blue");
    grd.addColorStop(1, "violet");
    drawTriangle(context, canvas.width * 3 / 5, triangleY, triangleWidth, triangleHeight, grd);
  1. 使用图案填充绘制三角形:
    // pattern fill (right)
    var imageObj = new Image();
    imageObj.onload = function(){
        var pattern = context.createPattern(imageObj, "repeat");
        drawTriangle(context, canvas.width * 4 / 5, triangleY, triangleWidth, triangleHeight, pattern);
    };
    imageObj.src = "wood-pattern.png";
}; 
  1. 将画布标签嵌入到 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

工作原理...

正如您可能还记得的那样,我们可以使用beginPath()方法开始一个新路径,使用moveTo()放置我们的绘图光标,然后绘制连续的子路径以形成路径。我们可以通过使用画布上下文的closePath()方法来关闭路径,从而创建一个形状:

context.closePath();

这种方法基本上告诉画布上下文通过连接路径中的最后一个点和路径的起点来完成当前路径。

drawTriangle()方法中,我们可以使用beginPath()开始一个新路径,使用moveTo()定位绘图光标,使用lineTo()绘制三角形的两条边,然后使用closePath()完成三角形的第三条边。

从上面的截图中可以看出,从左边数第二个三角形是用线性渐变填充的。线性渐变可以使用画布上下文的createLinearGradient()方法创建,该方法由起点和终点定义:

var grd=context.createLinearGradient(startX,startY,endX,endY);

接下来,我们可以使用addColorStop()方法设置渐变的颜色,该方法在 0 到 1 的渐变线偏移位置处分配颜色值:

grd.addColorStop(offset,color);

偏移值为 0 的颜色将位于线性渐变的起点,偏移值为 1 的颜色将位于线性渐变的终点。在这个例子中,我们将浅蓝色放在三角形的顶部,深蓝色放在三角形的底部。

接下来,让我们来介绍径向渐变。右侧的第二个三角形填充有一个由六种不同颜色组成的径向渐变。可以使用画布上下文的createRadialGradient()方法创建径向渐变,该方法需要一个起点、起始半径、终点和终点半径:

var grd=context.createRadialGradient(startX,startY,
   startRadius,endX,endY,endRadius);

径向渐变由两个虚拟圆定义。第一个虚拟圆由startXstartYstartRadius定义。第二个虚拟圆由endXendYendRadius定义。与线性渐变类似,我们可以使用画布上下文的addColorStop()方法沿径向渐变线位置颜色。

最后,HTML5 画布 API 提供的第四种填充样式是图案。我们可以使用画布上下文的createPattern()方法创建一个pattern对象,该方法需要一个image对象和一个重复选项:

var pattern=context.createPattern(imageObj, repeatOption);

repeatOption可以选择四个选项之一,repeatrepeat-xrepeat-yno-repeat。除非另有说明,否则repeatOption默认为repeat。我们将在第三章中更深入地介绍图像,使用图像和视频

另请参阅...

  • 将所有内容放在一起:绘制一架喷气机

贝塞尔曲线的乐趣:绘制一朵云

在这个示例中,我们将学习如何通过连接一系列贝塞尔曲线子路径来绘制自定义形状,从而创建一朵蓬松的云。

贝塞尔曲线的乐趣:绘制一朵云

如何做...

按照以下步骤在画布中心绘制一朵蓬松的云:

  1. 定义一个 2D 画布上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 通过连接六个贝塞尔曲线来绘制一朵云:
    var startX = 200;
    var startY = 100;

  // draw cloud shape
    context.beginPath(); 
    context.moveTo(startX, startY);
    context.bezierCurveTo(startX - 40, startY + 20, startX - 40, startY + 70, startX + 60, startY + 70);
    context.bezierCurveTo(startX + 80, startY + 100, startX + 150, startY + 100, startX + 170, startY + 70);
    context.bezierCurveTo(startX + 250, startY + 70, startX + 250, startY + 40, startX + 220, startY + 20);
    context.bezierCurveTo(startX + 260, startY - 40, startX + 200, startY - 50, startX + 170, startY - 30);
    context.bezierCurveTo(startX + 150, startY - 75, startX + 80, startY - 60, startX + 80, startY - 30);
    context.bezierCurveTo(startX + 30, startY - 75, startX - 20, startY - 60, startX, startY);
    context.closePath();
  1. 使用createRadialGradient()方法定义一个径向渐变并填充形状:
  //add a radial gradient
    var grdCenterX = 260;
    var grdCenterY = 80;
    var grd = context.createRadialGradient(grdCenterX, grdCenterY, 10, grdCenterX, grdCenterY, 200);
    grd.addColorStop(0, "#8ED6FF"); // light blue
    grd.addColorStop(1, "#004CB3"); // dark blue
    context.fillStyle = grd;
    context.fill();
  1. 设置线宽并描绘云:
  // set the line width and stroke color
    context.lineWidth = 5;
    context.strokeStyle = "#0000ff";
    context.stroke();
};
  1. 将画布标签嵌入 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;"> 
</canvas>

它是如何工作的...

使用 HTML5 画布 API 绘制一朵蓬松的云,可以连接多个贝塞尔曲线以形成云形的周边。为了营造一个球形表面的幻觉,我们可以使用createRadialGradient()方法创建径向渐变,使用addColorStop()方法设置渐变颜色和偏移,使用fillStyle设置径向渐变为填充样式,然后使用fill()应用渐变。

绘制透明形状

对于需要形状分层的应用程序,通常希望使用透明度。在这个示例中,我们将学习如何使用全局 alpha 合成来设置形状的透明度。

绘制透明形状

如何做...

按照以下步骤在不透明正方形上方绘制一个透明圆:

  1. 定义一个 2D 画布上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 绘制一个矩形:
    // draw rectangle
    context.beginPath();
    context.rect(240, 30, 130, 130);
    context.fillStyle = "blue";
    context.fill();
  1. 使用globalAlpha属性设置画布的全局 alpha,并绘制一个圆:
    // draw circle
    context.globalAlpha = 0.5; // set global alpha
    context.beginPath();
    context.arc(359, 150, 70, 0, 2 * Math.PI, false);
    context.fillStyle = "red";
    context.fill();
}; 
  1. 将画布标签嵌入 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

使用 HTML5 画布 API 设置形状的不透明度,可以使用globalAlpha属性:

context.globalAlpha=[value]

globalAlpha属性接受 0 到 1 之间的任何实数。我们可以将globalAlpha属性设置为1,使形状完全不透明,也可以将globalAlpha属性设置为0,使形状完全透明。

使用上下文状态堆栈来保存和恢复样式

在创建更复杂的 HTML5 画布应用程序时,您会发现自己需要一种方法来恢复到以前的样式组合,这样您就不必在绘图过程的不同点设置和重置几十种样式属性。幸运的是,HTML5 画布 API 为我们提供了访问上下文状态堆栈的方式,允许我们保存和恢复上下文状态。在这个示例中,我们将演示状态堆栈是如何工作的,通过保存上下文状态,设置全局 alpha,绘制一个透明圆,将状态堆栈恢复到设置全局 alpha 之前的状态,然后绘制一个不透明的正方形。让我们来看看!

使用上下文状态堆栈保存和恢复样式

准备好了...

在我们讨论画布状态堆栈之前,您必须了解堆栈数据结构的工作原理(如果您已经了解,可以跳到它是如何工作部分)。

堆栈数据结构是一种后进先出(LIFO)结构。堆栈有三个主要操作-pushpopstack top。当一个元素被推送到堆栈上时,它被添加到堆栈的顶部。当堆栈被弹出时,顶部元素被从堆栈中移除。stack top操作简单地返回堆栈顶部的元素。

准备好了...

看一下前面的图表,它代表了在多个操作中堆栈的状态。在步骤 1 中,我们开始时有一个包含一个元素“a”的堆栈。在步骤 2 中,“b”元素被推送到堆栈上。在步骤 3 中,“c”元素被推送到堆栈上。在步骤 4 中,我们弹出堆栈,这将移除最后推送到堆栈上的元素。由于元素“c”位于堆栈顶部,因此它被移除。在步骤 5 中,我们再次弹出堆栈,这将移除最后推送到堆栈上的元素。由于元素“b”位于堆栈顶部,因此它被移除。

正如我们将在下一节中看到的,堆栈是一个很好的数据结构,用于保存随时间变化的状态,然后通过弹出堆栈来恢复它们。

如何做...

按照以下步骤在透明圆上绘制一个不透明的正方形:

  1. 定义一个 2D 画布上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 画一个矩形:
    // draw rectangle
    context.beginPath();
    context.rect(150, 30, 130, 130);
    context.fillStyle = "blue";
    context.fill();
  1. 使用save()保存上下文状态,使用globalAlpha属性设置画布的全局 alpha,绘制一个圆,然后使用restore()恢复画布状态:
    // wrap circle drawing code with save-restore combination
    context.save();
    context.globalAlpha = 0.5; // set global alpha
    context.beginPath();
    context.arc(canvas.width / 2, canvas.height / 2, 70, 0, 2 * Math.PI, false);
    context.fillStyle = "red";
    context.fill();
    context.restore();
  1. 绘制另一个矩形(将是不透明的),以显示上下文状态已恢复到设置全局 alpha 属性之前的状态:
    // draw another rectangle
    context.beginPath();
    context.rect(canvas.width - (150 + 130), canvas.height - (30 + 130), 130, 130);
    context.fillStyle = "green";
    context.fill();
};
  1. 将 canvas 标签嵌入到 HTML 文档的 body 中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作...

如您在前面的代码中所见,通过将圆形绘制代码包装在 save-restore 组合中,我们实质上是在save()方法和restore()方法之间封装了我们使用的任何样式,以便它们不会影响之后绘制的形状。可以将 save-restore 组合视为一种引入样式作用域的方式,类似于函数在 JavaScript 中引入变量作用域的方式。尽管您可能会说“嗯,这听起来像是一个复杂的方法来将 globalAlpha 设置回 1!” 等一下伙计。在现实世界中,您通常会处理大量不同的样式组合,用于代码的不同部分。在这种情况下,save-restore 组合是救命稻草。在没有 save-restore 组合的情况下编写复杂的 HTML5 画布应用程序,就像使用全局变量在一个大的 JavaScript 代码块中构建复杂的 Web 应用程序一样。天啊!

还有更多...

在第四章中,我们将看到掌握变换,状态堆栈的另一个常见用法是保存和恢复变换状态。

另请参阅...

  • 使用状态堆栈处理多个变换在第四章中

使用复合操作进行工作

在这个示例中,我们将通过创建每种变化的表格来探索复合操作。复合操作对于创建复杂形状、在其他形状下面绘制形状而不是在其上面以及创建其他有趣的效果非常有用。

使用复合操作

准备好了...

以下是 HTML5 画布 API 中可用的每种可能的复合操作的描述,其中红色圆表示源(S),蓝色正方形表示目标(D)。为了进一步加深对复合操作的理解,在阅读每个描述时,有助于查看相应的操作:

操作 描述
source-atop (S atop D) 在两个图像都不透明的地方显示源图像。在目标图像不透明但源图像透明的地方显示目标图像。在其他地方显示透明度。
source-in (S in D) 在源图像和目标图像都不透明的地方显示源图像。在其他地方显示透明度。
source-out (S out D) 在源图像不透明且目标图像透明的地方显示源图像。在其他地方显示透明度。
source-over (S over D, default) 在源图像不透明的地方显示源图像。在其他地方显示目标图像。
destination-atop (S atop D) 在两个图像都不透明的地方显示目标图像。在源图像不透明但目标图像透明的地方显示源图像。在其他地方显示透明度。
destination-in (S in D) 在目标图像和源图像都不透明的地方显示目标图像。在其他地方显示透明度。
destination -out (S out D) 在目标图像不透明且源图像透明的地方显示目标图像。在其他地方显示透明度。
destination -over (S over D) 在目标图像不透明的地方显示目标图像。在其他地方显示目标图像。
lighter (S plus D) 显示源图像和目标图像的总和。
xor (S xor D) 源图像和目标图像的异或。
copy (D is ignored) 显示源图像而不是目标图像。

在撰写本文时,处理复合操作相当棘手,因为五个主要浏览器——Chrome、Firefox、Safari、Opera 和 IE9——对复合操作的处理方式不同。与其向您展示当前支持的复合操作的图表,您应该上网搜索类似"canvas composite operation support by browser"的内容,以查看每个浏览器当前的支持情况,如果您打算使用它们。

如何做...

按照以下步骤创建复合操作的实时表格:

  1. 为画布和文本显示定义样式:
/* select the div child element of the body */
body > div {
    width: 680px;
    height: 430px;
    border: 1px solid black;
    float: left;
    overflow: hidden;
}

canvas {
    float: left;
    margin-top: 30px;
}

div {
    font-size: 11px;
    font-family: verdana;
    height: 15px;
    float: left;
  width: 160px;
}

/* select the 1st, 5th, and 9th label div */
body > div > div:nth-of-type(4n+1) {
    margin-left: 40px;
}
  1. 定义每个正方形和圆的大小和相对距离:
window.onload = function(){
    var squareWidth = 55;
    var circleRadius = 35;
    var rectCircleDistX = 50;
    var rectCircleDistY = 50;
  1. 构建一个复合操作的数组:
    // define an array of composite operations
    var operationArray = [];
    operationArray.push("source-atop"); // 0
    operationArray.push("source-in"); // 1
    operationArray.push("source-out"); // 2
    operationArray.push("source-over"); // 3
    operationArray.push("destination-atop"); // 4
    operationArray.push("destination-in"); // 5
    operationArray.push("destination-out"); // 6
    operationArray.push("destination-over"); // 7
    operationArray.push("lighter"); // 8
    operationArray.push("xor"); // 9
    operationArray.push("copy"); // 10
  1. 执行每个操作并在相应的画布上绘制结果:
    // draw each of the eleven operations
    for (var n = 0; n < operationArray.length; n++) {
        var thisOperation = operationArray[n];
        var canvas = document.getElementById(thisOperation);
        var context = canvas.getContext("2d");

        // draw rectangle
        context.beginPath();
        context.rect(40, 0, squareWidth, squareWidth);
        context.fillStyle = "blue";
        context.fill();

        // set the global composite operation
        context.globalCompositeOperation = thisOperation;

        // draw circle
        context.beginPath();
        context.arc(40 + rectCircleDistX, rectCircleDistY, circleRadius, 0, 2 * Math.PI, false);
        context.fillStyle = "red";
        context.fill();
    }
};
  1. 在 HTML 文档的主体中嵌入每个操作的画布标签:
<body>
    <div>
        <canvas id="source-atop" width="160" height="90">
        </canvas>
        <canvas id="source-in" width="160" height="90">
        </canvas>
        <canvas id="source-out" width="160" height="90">
        </canvas>
        <canvas id="source-over" width="160" height="90">
        </canvas>
        <div>
            source-atop
        </div>
        <div>
            source-in
        </div>
        <div>
            source-out
        </div>
        <div>
            source-over
        </div>
        <canvas id="destination-atop" width="160" height="90">
        </canvas>
        <canvas id="destination-in" width="160" height="90">
        </canvas>
        <canvas id="destination-out" width="160" height="90">
        </canvas>
        <canvas id="destination-over" width="160" height="90">
        </canvas>
        <div>
            destination-atop
        </div>
        <div>
            destination-in
        </div>
        <div>
            destination-out
        </div>
        <div>
            destination-over
        </div>
        <canvas id="lighter" width="160" height="90">
        </canvas>
        <canvas id="xor" width="160" height="90">
        </canvas>
        <canvas id="copy" width="160" height="90">
        </canvas>
        <canvas width="160" height="90">
        </canvas>
        <div>
            lighter
        </div>
        <div>
            xor
        </div>
        <div>
            copy
        </div>
    </div>
</body>

它是如何工作的...

我们可以使用画布上下文的globalCompositeOperation属性来设置复合操作:

context.globalCompositeOperation=[value];

globalCompositeOperaton属性接受十一个值之一,包括source-atopsource-insource-outsource-overdestination-atopdestination-indestination-outdestination-overlighterxorcopySource指的是操作后在画布上绘制的所有内容,destination指的是操作前在画布上绘制的所有内容。除非另有规定,默认的复合操作设置为source-over,这基本上意味着每次在画布上绘制东西时,它都会绘制在已经存在的东西的顶部。

我们可以为每个复合操作创建一个数组,然后循环遍历每个数组,将结果绘制到相应的画布上。对于每次迭代,我们可以绘制一个正方形,设置复合操作,然后绘制一个圆。

使用循环创建图案:绘制齿轮

在这个食谱中,我们将通过迭代绘制径向锯齿来创建一个机械齿轮,然后绘制圆来形成齿轮的主体。

使用循环创建图案:绘制齿轮

如何做...

按照以下步骤在画布中心绘制齿轮:

  1. 定义 2D 画布上下文并设置齿轮属性:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    // gear position
    var centerX = canvas.width / 2;
    var centerY = canvas.height / 2;

  // radius of the teeth tips
    var outerRadius = 95;

  // radius of the teeth intersections
    var innerRadius = 50;

  // radius of the gear without the teeth
    var midRadius = innerRadius * 1.6;

  // radius of the hole
    var holeRadius = 10;

  // num points is the number of points that are required
  // to make the gear teeth.  The number of teeth on the gear
  // are equal to half of the number of points.  In this recipe,
  // we will use 50 points which corresponds to 25 gear teeth.
    var numPoints = 50;
  1. 绘制齿轮齿:

    // draw gear teeth
    context.beginPath();
  // we can set the lineJoinproperty to bevel so that the tips
  // of the gear teeth are flat and don't come to a sharp point
    context.lineJoin = "bevel";

  // loop through the number of points to create the gear shape
    for (var n = 0; n < numPoints; n++) {
        var radius = null;

    // draw tip of teeth on even iterations
        if (n % 2 == 0) {
            radius = outerRadius;
        }
    // draw teeth connection which lies somewhere between
    // the gear center and gear radius
        else {
            radius = innerRadius;
        }

        var theta = ((Math.PI * 2) / numPoints) * (n + 1);
        var x = (radius * Math.sin(theta)) + centerX;
        var y = (radius * Math.cos(theta)) + centerY;

    // if first iteration, use moveTo() to position
    // the drawing cursor
        if (n == 0) {
            context.moveTo(x, y);
        }
    // if any other iteration, use lineTo() to connect sub paths
        else {
            context.lineTo(x, y);
        }
    }

    context.closePath();

  // define the line width and stroke color
    context.lineWidth = 5;
    context.strokeStyle = "#004CB3";
    context.stroke();
  1. 绘制齿轮主体:
    // draw gear body
    context.beginPath();
    context.arc(centerX, centerY, midRadius, 0, 2 * Math.PI, false);

  // create a linear gradient
    var grd = context.createLinearGradient(230, 0, 370, 200);
    grd.addColorStop(0, "#8ED6FF"); // light blue
    grd.addColorStop(1, "#004CB3"); // dark blue
    context.fillStyle = grd;
    context.fill();
    context.lineWidth = 5;
    context.strokeStyle = "#004CB3";
    context.stroke();
  1. 绘制齿轮孔:
    // draw gear hole
    context.beginPath();
    context.arc(centerX, centerY, holeRadius, 0, 2 * Math.PI, false);
    context.fillStyle = "white";
    context.fill();
    context.strokeStyle = "#004CB3";
    context.stroke();
};
  1. 将画布标签嵌入 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

要在 HTML5 画布上绘制齿轮,我们可以从齿轮周围绘制齿。绘制齿轮的一种方法是使用倒角线连接绘制径向锯齿图案。径向锯齿的一个很好的例子是星星,它沿着想象的内圆有五个点,沿着想象的外圆有五个点。要创建一个星星,我们可以设置一个循环,进行 10 次迭代,每个点进行一次迭代。对于偶数次迭代,我们可以沿着外圆绘制一个点,对于奇数次迭代,我们可以沿着内圆绘制一个点。由于我们的星星有 10 个点,每个点之间的间隔为(2π / 10)弧度。

您可能会问自己“星星与齿轮齿有什么关系?”如果我们将这种逻辑扩展到绘制 50 个点的锯齿形状而不是 10 个点,我们将有效地创建了一个具有 25 个楔形齿的齿轮。

一旦处理了齿轮齿,我们可以绘制一个圆,并使用“createLinearGradient()”方法应用线性渐变,然后为齿轮的孔绘制一个较小的圆。

另请参阅...

  • 在第五章中制作机械齿轮

随机化形状属性:绘制一片花海

在这个食谱中,我们将通过创建一片色彩缤纷的花海来拥抱我们内心的嬉皮士。

随机化形状属性:绘制一片花海

如何做...

按照以下步骤在整个画布上绘制随机花朵:

  1. 定义Flower对象的构造函数:
// define Flower constructor
function Flower(context, centerX, centerY, radius, numPetals, color){
    this.context = context;
    this.centerX = centerX;
    this.centerY = centerY;
    this.radius = radius;
    this.numPetals = numPetals;
    this.color = color;
}
  1. 定义一个Flower对象的draw方法,该方法使用for循环创建花瓣,然后绘制一个黄色中心:
// Define Flower draw method
Flower.prototype.draw = function(){
    var context = this.context;
    context.beginPath();

    // draw petals
    for (var n = 0; n < this.numPetals; n++) {
        var theta1 = ((Math.PI * 2) / this.numPetals) * (n + 1);
        var theta2 = ((Math.PI * 2) / this.numPetals) * (n);

        var x1 = (this.radius * Math.sin(theta1)) + this.centerX;
        var y1 = (this.radius * Math.cos(theta1)) + this.centerY;
        var x2 = (this.radius * Math.sin(theta2)) + this.centerX;
        var y2 = (this.radius * Math.cos(theta2)) + this.centerY;

        context.moveTo(this.centerX, this.centerY);
        context.bezierCurveTo(x1, y1, x2, y2, this.centerX, this.centerY);
    }

    context.closePath();
    context.fillStyle = this.color;
    context.fill();

    // draw yellow center
    context.beginPath();
    context.arc(this.centerX, this.centerY, this.radius / 5, 0, 2 * Math.PI, false);
    context.fillStyle = "yellow";
    context.fill();
};
  1. 设置 2D 画布上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 为背景创建绿色渐变:
    // create a green gradation for background
    context.beginPath();
    context.rect(0, 0, canvas.width, canvas.height);
    var grd = context.createLinearGradient(0, 0, canvas.width, canvas.height);
    grd.addColorStop(0, "#1EDE70"); // light green
    grd.addColorStop(1, "#00A747"); // dark green
    context.fillStyle = grd;
    context.fill();
  1. 创建一个花色数组:
    // define an array of colors
    var colorArray = [];
    colorArray.push("red"); // 0
    colorArray.push("orange"); // 1
    colorArray.push("blue"); // 2
    colorArray.push("purple"); // 3
  1. 创建一个生成具有随机位置、大小和颜色的花朵的循环:
    // define number of flowers
    var numFlowers = 50;

    // draw randomly placed flowers
    for (var n = 0; n < numFlowers; n++) {
        var centerX = Math.random() * canvas.width;
        var centerY = Math.random() * canvas.height;
        var radius = (Math.random() * 25) + 25;
        var colorIndex = Math.round(Math.random() * (colorArray.length - 1));

        var thisFlower = new Flower(context, centerX, centerY, radius, 5, colorArray[colorIndex]);
        thisFlower.draw();
    }
};
  1. 将画布标签嵌入 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

这个食谱主要是关于随机化对象属性并使用 HTML5 画布在屏幕上绘制结果。其想法是创建一堆具有不同位置、大小和颜色的花朵。

为了帮助我们创建一片花海,创建一个Flower类非常有用,该类定义了花的属性和绘制花的方法。对于这个食谱,我保持了花瓣数量恒定,尽管您可以自行尝试每朵花的花瓣数量不同。

绘制一朵花实际上与我们以前的食谱“使用循环创建图案:绘制齿轮”非常相似,只是这一次,我们将在圆周围绘制花瓣,而不是锯齿。我发现使用 HTML5 画布绘制花瓣的最简单方法是绘制贝塞尔曲线,其起点连接到终点。贝塞尔曲线的起点和终点在花的中心,控制点在Flower类的“draw()”方法中的每次迭代中定义。

一旦我们的Flower类设置好并准备就绪,我们可以创建一个循环,每次迭代都实例化随机的Flower对象,然后用“draw()”方法渲染它们。

如果你自己尝试这个教程,你会发现每次刷新屏幕时花朵完全是随机的。

创建自定义形状函数:纸牌花色

如果皇家同花顺让你的肾上腺素飙升,那么这个教程适合你。在这个教程中,我们将为黑桃、红心、梅花和方块花色创建绘图函数。

创建自定义形状函数:纸牌花色

如何做…

按照以下步骤绘制黑桃、红心、梅花和方块花色:

  1. 定义 drawSpade()函数,使用四条贝塞尔曲线、两条二次曲线和一条直线绘制黑桃:
function drawSpade(context, x, y, width, height){
    context.save();
    var bottomWidth = width * 0.7;
    var topHeight = height * 0.7;
    var bottomHeight = height * 0.3;

    context.beginPath();
    context.moveTo(x, y);

    // top left of spade          
    context.bezierCurveTo(
        x, y + topHeight / 2, // control point 1
        x - width / 2, y + topHeight / 2, // control point 2
        x - width / 2, y + topHeight // end point
    );

    // bottom left of spade
    context.bezierCurveTo(
        x - width / 2, y + topHeight * 1.3, // control point 1
        x, y + topHeight * 1.3, // control point 2
        x, y + topHeight // end point
    );

    // bottom right of spade
    context.bezierCurveTo(
        x, y + topHeight * 1.3, // control point 1
        x + width / 2, y + topHeight * 1.3, // control point 2
        x + width / 2, y + topHeight // end point
    );

    // top right of spade
    context.bezierCurveTo(
        x + width / 2, y + topHeight / 2, // control point 1
        x, y + topHeight / 2, // control point 2
        x, y // end point
    );

    context.closePath();
    context.fill();

    // bottom of spade
    context.beginPath();
    context.moveTo(x, y + topHeight);
    context.quadraticCurveTo(
        x, y + topHeight + bottomHeight, // control point
        x - bottomWidth / 2, y + topHeight + bottomHeight // end point
    );
    context.lineTo(x + bottomWidth / 2, y + topHeight + bottomHeight);
    context.quadraticCurveTo(
        x, y + topHeight + bottomHeight, // control point
        x, y + topHeight // end point
    );
    context.closePath();
    context.fillStyle = "black";
    context.fill();
    context.restore();
}
  1. 定义 drawHeart()函数,使用四条贝塞尔曲线绘制心形:
function drawHeart(context, x, y, width, height){
    context.save();
    context.beginPath();
    var topCurveHeight = height * 0.3;
    context.moveTo(x, y + topCurveHeight);
    // top left curve
    context.bezierCurveTo(
        x, y, 
        x - width / 2, y, 
        x - width / 2, y + topCurveHeight
    );

    // bottom left curve
    context.bezierCurveTo(
        x - width / 2, y + (height + topCurveHeight) / 2, 
        x, y + (height + topCurveHeight) / 2, 
        x, y + height
    );

    // bottom right curve
    context.bezierCurveTo(
        x, y + (height + topCurveHeight) / 2, 
        x + width / 2, y + (height + topCurveHeight) / 2, 
        x + width / 2, y + topCurveHeight
    );

    // top right curve
    context.bezierCurveTo(
        x + width / 2, y, 
        x, y, 
        x, y + topCurveHeight
    );

    context.closePath();
    context.fillStyle = "red";
    context.fill();
    context.restore();
}
  1. 定义 drawClub()函数,使用四个圆形、两条二次曲线和一条直线绘制梅花:
function drawClub(context, x, y, width, height){
    context.save();
    var circleRadius = width * 0.3;
    var bottomWidth = width * 0.5;
    var bottomHeight = height * 0.35;
    context.fillStyle = "black";

    // top circle
    context.beginPath();
    context.arc(
        x, y + circleRadius + (height * 0.05), 
        circleRadius, 0, 2 * Math.PI, false
    );
    context.fill();

    // bottom right circle
    context.beginPath();
    context.arc(
        x + circleRadius, y + (height * 0.6), 
        circleRadius, 0, 2 * Math.PI, false
    );
    context.fill();

    // bottom left circle
    context.beginPath();
    context.arc(
        x - circleRadius, y + (height * 0.6), 
        circleRadius, 0, 2 * Math.PI, false
    );
    context.fill();

    // center filler circle
    context.beginPath();
    context.arc(
        x, y + (height * 0.5), 
        circleRadius / 2, 0, 2 * Math.PI, false
    );
    context.fill();

    // bottom of club
    context.moveTo(x, y + (height * 0.6));
    context.quadraticCurveTo(
        x, y + height, 
        x - bottomWidth / 2, y + height
    );
    context.lineTo(x + bottomWidth / 2, y + height);
    context.quadraticCurveTo(
        x, y + height, 
        x, y + (height * 0.6)
    );
    context.closePath();
    context.fill();
    context.restore();
}
  1. 定义 drawDiamond()函数,使用四条直线绘制菱形:
function drawDiamond(context, x, y, width, height){
    context.save();
    context.beginPath();
    context.moveTo(x, y);

    // top left edge
    context.lineTo(x - width / 2, y + height / 2);

    // bottom left edge
    context.lineTo(x, y + height);

    // bottom right edge
    context.lineTo(x + width / 2, y + height / 2);

    // closing the path automatically creates
    // the top right edge
    context.closePath();

    context.fillStyle = "red";
    context.fill();
    context.restore();
}
  1. 页面加载时,定义画布上下文,然后使用四个绘图函数来渲染黑桃、红心、梅花和方块:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    drawSpade(context, canvas.width * 0.2, 70, 75, 100);
    drawHeart(context, canvas.width * 0.4, 70, 75, 100);
    drawClub(context, canvas.width * 0.6, 70, 75, 100);
    drawDiamond(context, canvas.width * 0.8, 70, 75, 100);
};
  1. 在 HTML 文档的 body 内嵌入 canvas 标签:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的…

这个教程演示了如何通过组合 HTML5 画布提供的四种主要子路径类型:直线、圆弧、二次曲线和贝塞尔曲线来绘制任何形状。

要绘制黑桃,我们可以连接四条贝塞尔曲线形成顶部部分,然后使用两条二次曲线和一条直线形成底部部分。要绘制红心,我们可以以与黑桃相同的方式连接四条贝塞尔曲线,只是形状的顶点在底部而不是顶部。要创建梅花,我们可以使用圆弧绘制三个圆形作为顶部部分,与黑桃类似,我们可以使用两条二次曲线和一条直线来形成底部部分。最后,要绘制方块,我们可以简单地连接四条直线。

将所有内容放在一起:绘制飞机

在这个教程中,我们将通过使用线条、曲线、形状、颜色、线性渐变和径向渐变来推动 HTML5 画布绘图 API 的极限,绘制出矢量飞机。

将所有内容放在一起:绘制飞机

如何做…

按照以下步骤绘制矢量飞机:

  1. 定义一个 2D 画布上下文,并设置线连接样式:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  var grd;

    context.lineJoin = "round";
  1. 绘制右尾翼:
    // outline right tail wing
    context.beginPath();
    context.moveTo(248, 60); //13
    context.lineTo(262, 45); // 12
    context.lineTo(285, 56); //11
    context.lineTo(284, 59); // 10
    context.lineTo(276, 91); // 9
    context.closePath();
    context.fillStyle = "#495AFE";
    context.fill();
    context.lineWidth = 4;
    context.stroke();

    // right tail wing detail
    context.beginPath();
    context.moveTo(281, 54); // 10
    context.lineTo(273, 84); // 9
    context.closePath();
    context.lineWidth = 2;
    context.stroke();
  1. 绘制右翼:
    // outline right wing
    context.beginPath();
    context.moveTo(425, 159);
    context.lineTo(449, 91); // 4
    context.lineTo(447, 83); // 5
    context.lineTo(408, 67); // 6
    context.lineTo(343, 132); // 7
    context.fillStyle = "#495AFE";
    context.fill();
    context.lineWidth = 4;
    context.stroke();

    // right wing detail
    context.beginPath();
    context.moveTo(420, 158);
    context.lineTo(447, 83); // 4
    context.lineWidth = 2;
    context.stroke();

    context.beginPath();
    context.moveTo(439, 102);
    context.lineTo(395, 81);
    context.lineWidth = 2;
    context.stroke();
  1. 绘制机身和尾部顶部:
    // outline body
    context.beginPath();
    context.moveTo(541, 300); // 1
    context.quadraticCurveTo(529, 252, 490, 228); // 2
    context.quadraticCurveTo(487, 160, 303, 123); // 3

    // outline tail
    context.lineTo(213, 20); // 14
    context.lineTo(207, 22); // 15
    context.bezierCurveTo(208, 164, 255, 207, 412, 271); // 27
    context.lineTo(427, 271); // 28
    context.quadraticCurveTo(470, 296, 541, 300); // 1
    context.closePath();
    grd = context.createLinearGradient(304, 246, 345, 155);
    grd.addColorStop(0, "#000E91"); // dark blue
    grd.addColorStop(1, "#495AFE"); // light blue
    context.fillStyle = grd;
    context.fill();
    context.lineWidth = 4;
    context.stroke();

    // tail detail
    context.beginPath();
    context.moveTo(297, 124);
    context.lineTo(207, 22);
    context.lineWidth = 2;
    context.stroke();
  1. 绘制左尾翼:
    // outline left tail wing
    context.beginPath();
    context.moveTo(303, 121); // 8
    context.lineTo(297, 125); // 8
    context.lineTo(255, 104);
    context.lineWidth = 2;
    context.stroke();

    context.beginPath();
    context.moveTo(212, 80);
    context.lineTo(140, 85); // 18
    context.lineTo(138, 91); // 19
    context.lineTo(156, 105); // 20
    context.lineTo(254, 104);
    context.lineTo(254, 100);
    context.lineWidth = 4;
    context.fillStyle = "#495AFE";
    context.fill();
    context.stroke();

    // left tail wing detail
    context.beginPath();
    context.moveTo(140, 86); // 18
    context.lineTo(156, 100); // 20
    context.lineTo(254, 100);
    context.lineTo(209, 77);
    context.lineWidth = 2;
    context.stroke();
  1. 绘制左翼:
    // outline left wing
    context.beginPath();
    context.moveTo(262, 166); // 22
    context.lineTo(98, 208); // 23
    context.lineTo(96, 215); // 24
    context.lineTo(136, 245); // 25
    context.lineTo(339, 218);
    context.lineTo(339, 215);
    context.closePath();
    context.fillStyle = "#495AFE";
    context.fill();
    context.lineWidth = 4;
    context.stroke();

    // left wing detail
    context.beginPath();
    context.moveTo(98, 210);
    context.lineTo(136, 240); // 25
    context.lineTo(339, 213);
    context.lineWidth = 2;
    context.stroke();

    context.beginPath();
    context.moveTo(165, 235);
    context.lineTo(123, 203);
    context.lineWidth = 2;
    context.stroke();
  1. 绘制侧面细节:
    // side detail
    context.beginPath();
    context.moveTo(427, 271);
    context.lineTo(423, 221);
    context.quadraticCurveTo(372, 175, 310, 155);
    context.lineWidth = 4;
    context.stroke();
  1. 绘制机头细节:
    // nose detail
    context.beginPath();
    context.moveTo(475, 288);
    context.quadraticCurveTo(476, 256, 509, 243);
    context.quadraticCurveTo(533, 268, 541, 300); // 1
    context.quadraticCurveTo(501, 300, 475, 288);
    grd = context.createLinearGradient(491, 301, 530, 263);
    grd.addColorStop(0, "#9D0000"); // dark red
    grd.addColorStop(1, "#FF0000"); // light red
    context.fillStyle = grd;
    context.fill();
    context.lineWidth = 4;
    context.stroke();

    context.beginPath();
    context.moveTo(480, 293);
    context.quadraticCurveTo(480, 256, 513, 246);
    context.lineWidth = 2;
    context.stroke();
  1. 绘制座舱:
    // cockpit detail
    context.beginPath();
    context.moveTo(442, 169);
    context.quadraticCurveTo(419, 176, 415, 200);
    context.quadraticCurveTo(483, 250, 490, 228);
    context.quadraticCurveTo(480, 186, 439, 170);
    context.lineWidth = 4;
    context.stroke();
    grd = context.createRadialGradient(473, 200, 20, 473, 200, 70);
    grd.addColorStop(0, "#E1E7FF"); // dark gray
    grd.addColorStop(1, "#737784"); // light gray
    context.fillStyle = grd;
    context.fill();

    context.beginPath();
    context.moveTo(448, 173);
    context.quadraticCurveTo(425, 176, 420, 204);
    context.lineWidth = 2;
    context.stroke();

    context.beginPath();
    context.moveTo(470, 186);
    context.quadraticCurveTo(445, 190, 440, 220);
    context.lineWidth = 2;
    context.stroke();
  1. 绘制进气口:
    // intake outline
    context.beginPath();
    context.moveTo(420, 265);
    context.lineTo(416, 223);
    context.bezierCurveTo(384, 224, 399, 270, 420, 265);
    context.closePath();
    context.fillStyle = "#001975";
    context.fill();
    context.lineWidth = 4;
    context.stroke();

    context.beginPath();
    context.moveTo(420, 265);
    context.lineTo(402, 253);
    context.lineWidth = 2;
    context.stroke();

    context.beginPath();
    context.moveTo(404, 203);
    context.bezierCurveTo(364, 204, 379, 265, 394, 263);
    context.lineWidth = 2;
    context.stroke();
};
  1. 在 HTML 文档的 body 内嵌入 canvas 标签:
<canvas id="myCanvas" width="650" height="350" style="border:1px solid black;">
</canvas>

它是如何工作的…

这个教程结合了线条、二次曲线、贝塞尔曲线、路径、形状、实心填充、线性渐变和径向渐变的使用。尽管 HTML5 画布相当基础,但它提供了我们绘制出优秀图形所需的一切,包括矢量飞机。

要使用 HTML5 画布绘制飞机,我们可以先在 Adobe Photoshop 或其他带有绘图区域大小等于画布大小的图像编辑器中绘制一架飞机,本例中为 650 x 350 像素。接下来,我们可以使用鼠标在绘图中找到形成飞机形状的主要点,通过悬停在绘图的每条线的端点上记录 x、y 坐标。有了这些坐标,我们可以用 4 像素的线宽绘制飞机的主要轮廓,然后我们可以回去用 2 像素的线宽填充飞机的细节。

提示

最好的做法是首先绘制远离观众的图形部分,因为你在画布上绘制的每个形状都会重叠在前面的形状上。如果你看一下前面的代码,你会注意到右翼先被绘制,然后是飞机的机身,最后是左翼。这是因为右翼离观众最远,而左翼离观众最近。

一旦线条绘制完成,我们可以用纯色填充喷气机,给机身添加线性渐变,给座舱添加径向渐变,使绘画具有一定的深度。最后,我们可以在飞机的机头上添加醒目的红色渐变,为起飞做准备,激发我们的想象力。

第三章:使用图像和视频

在本章中,我们将涵盖:

  • 绘制图像

  • 裁剪图像

  • 复制和粘贴画布的部分

  • 使用视频

  • 获取图像数据

  • 像素操作简介:反转图像颜色

  • 反转视频颜色

  • 将图像颜色转换为灰度

  • 将画布绘制转换为数据 URL

  • 将画布绘制保存为图像

  • 使用数据 URL 加载画布

  • 创建像素化图像焦点

介绍

本章重点介绍 HTML5 画布、图像和视频的另一个非常令人兴奋的主题。除了提供定位、调整大小和裁剪图像和视频的基本功能外,HTML5 画布 API 还允许我们访问和修改每个像素的颜色和透明度。让我们开始吧!

绘制图像

让我们通过绘制一个简单的图像来开始。在本示例中,我们将学习如何加载图像并在画布上的某个位置绘制它。

绘制图像

按照以下步骤在画布中央绘制图像:

如何做...

  1. 定义画布上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 创建一个image对象,将onload属性设置为绘制图像的函数,然后设置图像的源:
    var imageObj = new Image();
    imageObj.onload = function(){
        var destX = canvas.width / 2 - this.width / 2;
        var destY = canvas.height / 2 - this.height / 2;

        context.drawImage(this, destX, destY);
    };
    imageObj.src = "jet_300x214.jpg";
};
  1. 将 canvas 标签嵌入 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

要绘制图像,我们首先需要使用new Image()创建一个image对象。请注意,我们在定义图像的源之前设置了image对象的onload属性。

提示

在设置图像源之前定义加载图像时要执行的操作是一个很好的做法。理论上,如果我们在定义onload属性之前定义图像的源,图像可能会在定义完成之前加载(尽管这很不太可能)。

本示例中的关键方法是drawImage()方法:

context.drawImage(imageObj,destX,destY);

其中imageObjimage对象,destXdestY是我们想要放置图像的位置。

还有更多...

除了使用destXdestY定义图像位置外,我们还可以添加两个额外的参数,destWidthdestHeight来定义图像的大小:

context.drawImage(imageObj,destX,destY,destWidth,destHeight);

在大多数情况下,最好不要使用drawImage()方法调整图像的大小,因为缩放图像的质量会明显降低,类似于使用 HTML 图像元素的宽度和高度属性调整图像大小时的结果。如果图像质量是您关心的问题(为什么你不会关心?),通常最好在创建需要缩放图像的应用程序时使用缩略图图像。另一方面,如果您的应用程序动态缩小和扩展图像,使用drawImage()方法和destWidthdestHeight来缩放图像是一个完全可以接受的方法。

裁剪图像

在本示例中,我们将裁剪图像的一部分,然后将结果绘制到画布上。

裁剪图像

按照以下步骤裁剪图像的一部分并将结果绘制到画布上。

如何做...

  1. 定义画布上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 创建一个图像对象,将onload属性设置为裁剪图像的函数,然后设置图像的源:
    var imageObj = new Image();
    imageObj.onload = function(){
    // source rectangular area
        var sourceX = 550;
        var sourceY = 300;
        var sourceWidth = 300;
        var sourceHeight = 214;

    // destination image size and position
        var destWidth = sourceWidth;
        var destHeight = sourceHeight;
        var destX = canvas.width / 2 - destWidth / 2;
        var destY = canvas.height / 2 - destHeight / 2;

        context.drawImage(this, sourceX, sourceY, sourceWidth, sourceHeight, destX, destY, destWidth, destHeight);
    };
    imageObj.src = "jet_1000x714.jpg";
};
  1. 将 canvas 标签嵌入 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

在上一个示例中,我们讨论了使用drawImage()方法在画布上绘制图像的两种不同方式。在第一种情况下,我们可以传递一个image对象和一个位置,简单地在给定位置绘制图像。在第二种情况下,我们可以传递一个image对象,一个位置和一个大小,在给定位置以给定大小绘制图像。此外,如果我们想要裁剪图像,还可以向drawImage()方法添加六个参数:

Context.drawImage(imageObj,sourceX,sourceY,sourceWidth, sourceHight, sourceHeight,sourceHeight, destX, destY, destWidth, destHeight);

看一下下面的图表:

图像裁剪,步骤是如何工作的...

正如您所看到的,sourceXsourceY指的是源图像中裁剪区域的左上角。sourceWidthsourceHeight指的是源图像中裁剪图像的宽度和高度。destXdestY指的是裁剪图像在画布上的位置,destWidthdestHeight指的是结果裁剪图像的宽度和高度。

提示

如果您不打算缩放裁剪的图像,则destWidth等于sourceWidthdestHeight等于sourceHeight

复制和粘贴画布的部分

在这个示例中,我们将介绍drawImage()方法的另一个有趣用法——复制画布的部分。首先,我们将在画布中心绘制一个梅花,然后我们将复制梅花的右侧,然后粘贴到左侧,然后我们将复制梅花的左侧,然后粘贴到右侧。

复制和粘贴画布的部分

按照以下步骤在画布中心绘制一个梅花,然后将形状的部分复制并粘贴回画布上:

如何做...

  1. 定义画布上下文:
window.onload = function(){
    // drawing canvas and context
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 使用我们在第二章中创建的drawSpade()函数,在画布中心绘制一个梅花,形状绘制和合成
    // draw spade
    var spadeX = canvas.width / 2;
    var spadeY = 20;
    var spadeWidth = 140;
    var spadeHeight = 200;

    // draw spade in center of canvas
    drawSpade(context, spadeX, spadeY, spadeWidth, spadeHeight);
  1. 复制梅花的右半部分,然后使用drawImage()方法将其粘贴到梅花左侧的画布上:
    context.drawImage(
    canvas,         
    spadeX,         // source x
    spadeY,         // source y
    spadeWidth / 2,     // source width
    spadeHeight,       // source height
    spadeX - spadeWidth,  // dest x
    spadeY,         // dest y
    spadeWidth / 2,     // dest width
    spadeHeight        // dest height
  );
  1. 复制梅花的左半部分,然后使用drawImage()方法将其粘贴到梅花右侧的画布上:
    context.drawImage(
    canvas, 
    spadeX - spadeWidth / 2,  // source x   
    spadeY,           // source y
    spadeWidth / 2,       // source width
    spadeHeight,         // source height
    spadeX + spadeWidth / 2,   // dest x
    spadeY,           // dest y
    spadeWidth / 2,       // dest width
    spadeHeight          // dest height
  );
};
  1. 将画布嵌入到 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

要复制画布的一部分,我们可以将canvas对象传递给drawImage()方法,而不是一个image对象:

Context.drawImage(canvas,sourceX,sourceY,sourceWidth, sourceHight, sourceHeight,sourceHeight, destX, destY, destWidth, destHeight);

正如我们将在下一个示例中看到的,我们不仅可以使用drawImage()复制图像或画布的部分,还可以复制 HTML5 视频的部分。

使用视频

尽管 HTML5 画布 API 没有提供像图像那样在画布上绘制视频的直接方法,但我们可以通过从隐藏的视频标签中捕获帧,然后通过循环将它们复制到画布上来处理视频。

使用视频

准备工作...

在开始之前,让我们谈谈每个浏览器支持的 HTML5 视频格式。在撰写本文时,视频格式之争仍在继续,所有主要浏览器——Chrome、Firefox、Opera、Safari 和 IE——继续增加和删除对不同视频格式的支持。更糟糕的是,每当一个主要浏览器增加或删除对特定视频格式的支持时,开发人员就必须重新制定所需的最小视频格式集,以确保其应用程序在所有浏览器中正常工作。

在撰写本文时,三种主要的视频格式是 Ogg Theora、H.264 和 WebM。在本章的视频示例中,我们将使用 Ogg Theora 和 H.264 的组合。在处理视频时,强烈建议您在网上搜索,了解视频支持的当前状态,因为它可能随时发生变化。

还有更多!一旦您决定支持哪些视频格式,您可能需要一个视频格式转换器,将手头的视频文件转换为其他视频格式。一个很好的视频格式转换选项是 Miro Video Converter,它支持几乎任何视频格式的视频格式转换,包括 Ogg Theora、H.264 或 WebM 格式。

Miro Video Converter 可能是目前最常见的视频转换器,尽管您当然可以使用任何其他您喜欢的视频格式转换器。您可以从以下网址下载 Miro Video Converter:www.mirovideoconverter.com/

按照以下步骤将视频绘制到画布上:

如何做...

  1. 创建一个跨浏览器的方法来请求动画帧:
window.requestAnimFrame = (function(callback){
    return window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    window.oRequestAnimationFrame ||
    window.msRequestAnimationFrame ||
    function(callback){
        window.setTimeout(callback, 1000 / 60);
    };
})();
  1. 定义drawFrame()函数,它会复制当前视频帧,使用drawImage()方法将其粘贴到 canvas 上,然后请求新的动画帧来绘制下一帧:
function drawFrame(context, video){
    context.drawImage(video, 0, 0);
    requestAnimFrame(function(){
        drawFrame(context, video);
    });
}
  1. 定义 canvas 上下文,获取视频标签,并绘制第一帧视频:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
    var video = document.getElementById("myVideo");
    drawFrame(context, video);
};
  1. 在 HTML 文档的 body 中嵌入 canvas 和 video 标签:
<video id="myVideo" autoplay="true" loop="true" style="display:none;">
    <source src="img/BigBuckBunny_640x360.ogv" type="video/ogg"/><source src="img/BigBuckBunny_640x360.mp4" type="video/mp4"/>
</video>
<canvas id="myCanvas" width="600" height="360" style="border:1px solid black;">
</canvas>

它是如何工作的...

要在 HTML5 画布上绘制视频,我们首先需要在 HTML 文档中嵌入一个隐藏的视频标签。在这个示例中,以及将来的视频示例中,我使用了 Ogg Theora 和 H.264(mp4)视频格式。

接下来,当页面加载时,我们可以使用跨浏览器的requestAnimFrame()方法尽可能快地捕获视频帧,然后将它们绘制到 canvas 上。

获取图像数据

现在我们知道如何绘制图像和视频,让我们尝试访问图像数据,看看我们可以玩的属性有哪些。

获取图像数据

注意

警告:由于getImageData()方法的安全限制,此示例必须在 Web 服务器上运行。

准备工作...

在开始处理图像数据之前,重要的是我们要了解画布安全和 RGBA 颜色空间。

那么为什么画布安全对于访问图像数据很重要呢?简单来说,为了访问图像数据,我们需要使用画布上下文的getImageData()方法,如果我们尝试从非 Web 服务器文件系统上的图像或不同域上的图像访问图像数据,它将抛出SECURITY_ERR异常。换句话说,如果你要自己尝试这些演示,如果你的文件存储在本地文件系统上,它们将无法工作。你需要在 Web 服务器上运行本章的其余部分。

接下来,由于像素操作主要是改变像素的 RGB 值,我们可能应该在这里介绍 RGB 颜色模型和 RGBA 颜色空间。RGB 代表像素颜色的红色、绿色和蓝色分量。每个分量都是 0 到 255 之间的整数,其中 0 表示没有颜色,255 表示完整的颜色。RGB 值通常表示如下:

rgb(red,green,blue)

以下是用 RGB 颜色模型表示的一些常见颜色值:

rgb(0,0,0) = black
rgb(255,255,255) = white
rgb(255,0,0) = red
rgb(0,255,0) = green
rgb(0,0,255) = blue
rgb(255,255,0) = yellow
rgb(255,0,255) = magenta
rgb(0,255,255) = cyan

除了 RGB,像素还可以有一个 alpha 通道,它指的是像素的不透明度。alpha 通道为 0 是完全透明的像素,alpha 通道为 255 是完全不透明的像素。RGBA 颜色空间简单地指的是 RGB 颜色模型(RGB)加上 alpha 通道(A)。

提示

请注意不要混淆 HTML5 画布像素的 alpha 通道范围(整数 0 到 255)和 CSS 颜色的 alpha 通道范围(小数 0.0 到 1.0)。

按照以下步骤写出图像数据的属性:

如何做...

  1. 定义一个 canvas 上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 创建一个image对象,将onload属性设置为一个绘制图像的函数:
    var imageObj = new Image();
    imageObj.onload = function(){
        var sourceWidth = this.width;
        var sourceHeight = this.height;
        var destX = canvas.width / 2 - sourceWidth / 2;
        var destY = canvas.height / 2 - sourceHeight / 2;
        var sourceX = destX;
        var sourceY = destY;

    // draw image on canvas
        context.drawImage(this, destX, destY);
  1. 获取图像数据,写出其属性,然后在onload定义之外设置image对象的源:
    // get image data from the rectangular area 
    // of the canvas containing the image
        var imageData = context.getImageData(sourceX, sourceY, sourceWidth, sourceHeight);
        var data = imageData.data;

    // write out the image data properties
        var str = "width=" + imageData.width + ", height=" + imageData.height + ", data length=" + data.length;
        context.font = "12pt Calibri";
        context.fillText(str, 4, 14);
    };
    imageObj.src = "jet_300x214.jpg";
};
  1. 将 canvas 标签嵌入 HTML 文档的 body 中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

这个示例的思路是绘制图像,获取其图像数据,然后将图像数据属性写到屏幕上。从前面的代码中可以看到,我们可以使用 canvas 上下文的getImageData()方法获取图像数据:

context.getImageData(sourceX,sourceY,sourceWidth,sourceHeight);

请注意,getImageData()方法只能与 canvas 上下文一起使用,而不能直接使用image对象本身。因此,为了获取图像数据,我们必须先将图像绘制到 canvas 上,然后使用 canvas 上下文的getImageData()方法。

ImageData对象包含三个属性:widthheightdata。从这个食谱开头的截图中可以看到,我们的ImageData对象包含一个宽度属性为 300,一个高度属性为 214,以及一个data属性,它是一个像素信息数组,在这种情况下,长度为 256,800 个元素。说实话,ImageData对象的关键是data属性。data属性包含我们图像中每个像素的 RGBA 信息。由于我们的图像由 300 * 214 = 64,200 像素组成,因此这个数组的长度为 4 * 64,200 = 256,800 个元素。

像素处理简介:反转图像颜色

现在我们知道如何访问图像数据,包括图像或视频中每个像素的 RGBA,我们的下一步是探索像素处理的可能性。在这个食谱中,我们将通过反转每个像素的颜色来反转图像的颜色。

像素处理简介:反转图像颜色

注意

警告:由于getImageData()方法的安全限制,这个食谱必须在 web 服务器上运行。

按照以下步骤反转图像的颜色:

操作步骤...

  1. 定义 canvas 上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 创建一个image对象,并将onload属性设置为绘制图像和获取图像数据的函数:
    var imageObj = new Image();
    imageObj.onload = function(){
        var sourceWidth = this.width;
        var sourceHeight = this.height;
        var sourceX = canvas.width / 2 - sourceWidth / 2;
        var sourceY = canvas.height / 2 - sourceHeight / 2;
        var destX = sourceX;
        var destY = sourceY;
        context.drawImage(this, destX, destY);

        var imageData = context.getImageData(sourceX, sourceY, sourceWidth, sourceHeight);
        var data = imageData.data;
  1. 循环遍历图像中的所有像素并反转颜色:
        for (var i = 0; i < data.length; i += 4) {
            data[i] = 255 - data[i]; // red
            data[i + 1] = 255 - data[i + 1]; // green
            data[i + 2] = 255 - data[i + 2]; // blue
            // i+3 is alpha (the fourth element)
        }
  1. 用处理后的图像覆盖原始图像,然后在onload定义之外设置图像的源:
        // overwrite original image with
        // new image data
        context.putImageData(imageData, destX, destY);
    };
    imageObj.src = "jet_300x214.jpg";
};
  1. 将 canvas 标签嵌入到 HTML 文档的 body 中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

使用 HTML5 画布反转图像的颜色,我们可以简单地循环遍历图像中的所有像素,然后使用颜色反转算法反转每个像素。别担心,这比听起来容易。要反转像素的颜色,我们可以通过从 255 中减去每个值来反转其 RGB 分量中的每一个值,如下所示:

data[i  ] = 255 - data[i  ]; // red
data[i+1] = 255 - data[i+1]; // green
data[i+2] = 255 - data[i+2]; // blue

一旦像素被更新,我们可以使用画布上下文的putImageData()方法重新绘制图像:

context.putImageData(imageData, destX, destY); 

这个方法基本上允许我们使用图像数据而不是drawImage()方法的源图像来绘制图像。

反转视频颜色

这个食谱的目的是演示如何对视频进行像素处理,方法与处理图像的方式基本相同。在这个食谱中,我们将反转一个短视频片段的颜色。

drawImage()方法像素处理工作中反转视频颜色

注意

警告:由于getImageData()方法的安全限制,这个食谱必须在 web 服务器上运行。

按照以下步骤反转视频的颜色:

操作步骤...

  1. 创建一个跨浏览器的方法来请求动画帧:
window.requestAnimFrame = (function(callback){
    return window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    window.oRequestAnimationFrame ||
    window.msRequestAnimationFrame ||
    function(callback){
        window.setTimeout(callback, 1000 / 60);
    };
})();
  1. 定义drawFrame()函数,捕获当前视频帧,反转颜色,将帧绘制在画布上,然后请求一个新的动画帧:
function drawFrame(canvas, context, video){
    context.drawImage(video, 0, 0);

    var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
    var data = imageData.data;

    for (var i = 0; i < data.length; i += 4) {
        data[i] = 255 - data[i]; // red
        data[i + 1] = 255 - data[i + 1]; // green
        data[i + 2] = 255 - data[i + 2]; // blue
        // i+3 is alpha (the fourth element)
    }

    // overwrite original image
    context.putImageData(imageData, 0, 0);

    requestAnimFrame(function(){
        drawFrame(canvas, context, video);
    });
}
  1. 定义画布上下文,获取视频标签,并绘制第一个动画帧:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
    var video = document.getElementById("myVideo");
    drawFrame(canvas, context, video);
};
  1. 将视频和 canvas 元素嵌入到 HTML 文档的 body 中:
<video id="myVideo" autoplay="true" loop="true" style="display:none;">
    <source src="img/BigBuckBunny_640x360.ogv" type="video/ogg"/><source src="img/BigBuckBunny_640x360.mp4" type="video/mp4"/>
</video>
<canvas id="myCanvas" width="640" height="360" style="border:1px solid black;">
</canvas>

它是如何工作的...

与之前的食谱类似,我们可以对视频进行像素处理,方法与处理图像的方式基本相同,因为getImageData()方法从画布上下文获取图像数据,而不管上下文是如何渲染的。在这个食谱中,我们可以简单地反转画布上每个像素的颜色,对应requestAnimFrame()方法提供的每个视频帧。

将图像颜色转换为灰度

在这个食谱中,我们将探讨另一个常见的像素处理算法,将颜色转换为灰度。

requestAnimFrame()方法将图像颜色转换为灰度

注意

警告:由于getImageData()方法的安全限制,这个食谱必须在 web 服务器上运行。

按照以下步骤将图像的颜色转换为灰度:

操作步骤...

  1. 定义 canvas 上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 创建一个image对象,并将onload属性设置为绘制图像并获取图像数据的函数:
    var imageObj = new Image();
    imageObj.onload = function(){
        var sourceWidth = this.width;
        var sourceHeight = this.height;
        var destX = canvas.width / 2 - sourceWidth / 2;
        var destY = canvas.height / 2 - sourceHeight / 2;
        var sourceX = destX;
        var sourceY = destY;

        context.drawImage(this, destX, destY);

        var imageData = context.getImageData(sourceX, sourceY, sourceWidth, sourceHeight);
        var data = imageData.data;
  1. 循环遍历图像中的像素,并使用亮度方程将颜色转换为灰度:
        for (var i = 0; i < data.length; i += 4) {
            var brightness = 0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2];

            data[i] = brightness; // red
            data[i + 1] = brightness; // green
            data[i + 2] = brightness; // blue
            // i+3 is alpha (the fourth element)
        }
  1. 用处理后的图像覆盖原始图像,然后在onload定义后设置图像源:
        // overwrite original image
        context.putImageData(imageData, destX, destY);
    };
    imageObj.src = "jet_300x214.jpg";
};
  1. 将 canvas 元素嵌入 HTML 文档的 body 中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

工作原理...

要将 RGB 颜色转换为灰度渐变,我们需要获取颜色的亮度。我们可以使用亮度方程来获取彩色像素的灰度值。这个方程基于这样一个事实,即人类对绿光最敏感,其次是红光,对蓝光最不敏感:

亮度= 0.34 * R + 0.5 * G + 0.16 * B

为了考虑生理效应,请注意我们已经增加了对绿色值的权重(最敏感),然后是红色值(较不敏感),最后是蓝色值(最不敏感)。

有了这个方程,我们可以简单地循环遍历图像中的所有像素,计算感知亮度,将这个值分配给 RGB 值中的每个值,然后重新绘制图像到画布上。

将画布绘图转换为数据 URL

除了图像数据,我们还可以提取图像数据 URL,它基本上只是一个包含有关画布图像的编码信息的非常长的文本字符串。如果我们想要将画布绘图保存在本地存储或离线数据库中,数据 URL 非常方便。在这个示例中,我们将绘制一个云形状,获取其数据 URL,然后将其插入到 HTML 页面中,以便我们可以看到它的样子。

按照以下步骤将画布绘图转换为数据 URL:

如何做...

  1. 定义画布上下文并绘制云形状:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    var startX = 200;
    var startY = 100;

    // draw cloud shape
    context.beginPath();
    context.moveTo(startX, startY);
    context.bezierCurveTo(startX - 40, startY + 20, startX - 40, startY + 70, startX + 60, startY + 70);
    context.bezierCurveTo(startX + 80, startY + 100, startX + 150, startY + 100, startX + 170, startY + 70);
    context.bezierCurveTo(startX + 250, startY + 70, startX + 250, startY + 40, startX + 220, startY + 20);
    context.bezierCurveTo(startX + 260, startY - 40, startX + 200, startY - 50, startX + 170, startY - 30);
    context.bezierCurveTo(startX + 150, startY - 75, startX + 80, startY - 60, startX + 80, startY - 30);
    context.bezierCurveTo(startX + 30, startY - 75, startX - 20, startY - 60, startX, startY);
    context.closePath();

    context.lineWidth = 5;
    context.fillStyle = "#8ED6FF";
    context.fill();
    context.strokeStyle = "#0000ff";
    context.stroke();
  1. 使用canvas对象的toDataURL()方法获取画布的数据 URL:
    // save canvas image as data url (png format by default)
    var dataURL = canvas.toDataURL();
  1. 将(长)数据 URL 插入到<p>标签中,以便我们可以看到它:
    // insert url into the HTML document so we can see it
    document.getElementById("dataURL").innerHTML = "<b>dataURL:</b> " + dataURL;
};
  1. 将 canvas 标签嵌入 HTML 文档的 body 中,并创建一个<p>标签,用于存储数据 URL:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>
<p id="dataURL" style="width:600px;word-wrap: break-word;">
</p>

工作原理...

这个示例的关键是toDataURL()方法,它将画布绘图转换为数据 URL:

var dataURL = canvas.toDataURL();

运行此演示时,您将看到一个非常长的数据 URL,看起来像这样:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlg
AAAD6CAYAAAB9LTkQAAAgAElEQVR4Xu3dXbAUxd3H8f+5i09
VrEjuDlRFBSvoo1ETD/HmEcQIXskRc6FViaA+N7woRlNJUDQm4
kueeiS+INz4wEGfilwocLxSUASvDMf4XokpQbFKuAtYSdWT3PXz
/885C3t2Z3dndntme3q+W7UehN2e7k/3sj96enpGhAcCCCCAAAI
IIICAV4ERr6VRGAIIIIAAAggggIAQsBgECCCAAAIIIICAZwECl
mdQikMAAQQQQAABBAhYjAEEEEAAAQQQQMCzAAHLMyjFIYAAAgg
ggAACBCzGAAIIIIAAAggg4FmAgOUZlOIQQAABBBBAAAECFmMAA
QQQQAABBBDwLEDA8gxKcQgggAACCCCAAAGLMYAAAggggAACCHgWI
GB5BqU4BBBAAAEEEECAgMUYQAABBBBAAAEEPAsQsDyDUhwCCCCAA
AIIIEDAYgwggAACCCCAAAKeBQhYnkEpDgEEEEAAAQQQIGAxBhBAA
AEEEEAAAc8CBCzPoBSHAAIIIIAAAggQsBgDCCCAAAIIIICAZwECl
mdQikMAAQQQQAABBAhYjAEEEEAAAQQQQMCzAAHLMyjFIYAAAgggg
AACBCzGAAIIIIAAAggg4FmAgOUZlOIQQAABBBBAAAECFmMAAQQQQ
AABBBDwLEDA8gxKcQgggAACCCCAAAGLMYAAAggggAACCHgWIGB5
BqU4BBBAAAEEEECAgMUYQAABBBBAAAEEPAsQsDyDUhwCCCCAAAI
IIEDAYgwggAACCCCAAAKeBQhYnkEpDgEEEEAAAQQQIGAxBhBAAA
EEEEAAAc8CBCzPoBSHAAIIIIAAAggQsBgDCCCAAAIIIICAZwECl
mdQikMAAQQQQAABBAhYjAEEEEAAAQQQQMCzAAHLMyj

在这里看到的只是整个数据 URL 的一小部分。URL 中需要注意的重要部分是非常开始的部分,以data:image/png;base64开头。这意味着数据 URL 是一个 PNG 图像,由 base 64 编码表示。

与图像数据不同,图像数据 URL 是特殊的,因为它是一个字符串,可以与本地存储一起存储,或者可以传递到 Web 服务器以保存在离线数据库中。换句话说,图像数据用于检查和操作构成图像的每个单独像素,而图像数据 URL 旨在用于存储画布绘图并在客户端和服务器之间传递。

将画布绘图保存为图像

除了将画布绘图保存在本地存储或离线数据库中,我们还可以使用图像数据 URL 将画布绘图保存为图像,以便用户可以将其保存到本地计算机。在这个示例中,我们将获取画布绘图的图像数据 URL,然后将其设置为image对象的源,以便用户可以右键单击并将图像下载为 PNG。

按照以下步骤将画布绘图保存为图像:

如何做...

  1. 定义画布上下文并绘制云形状:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    // draw cloud
    context.beginPath(); // begin custom shape
    context.moveTo(170, 80);
    context.bezierCurveTo(130, 100, 130, 150, 230, 150);
    context.bezierCurveTo(250, 180, 320, 180, 340, 150);
    context.bezierCurveTo(420, 150, 420, 120, 390, 100);
    context.bezierCurveTo(430, 40, 370, 30, 340, 50);
    context.bezierCurveTo(320, 5, 250, 20, 250, 50);
    context.bezierCurveTo(200, 5, 150, 20, 170, 80);
    context.closePath(); // complete custom shape
    context.lineWidth = 5;
    context.fillStyle = "#8ED6FF";
    context.fill();
    context.strokeStyle = "#0000ff";
    context.stroke();
  1. 获取数据 URL:
    // save canvas image as data url (png format by default)
    var dataURL = canvas.toDataURL();
  1. 将图像标签的源设置为数据 URL,以便用户可以下载它:
    // set canvasImg image src to dataURL
    // so it can be saved as an image
    document.getElementById("canvasImg").src = dataURL;
};
  1. 将 canvas 标签嵌入 HTML 文档的 body 中,并添加一个图像标签,其中将包含画布绘图:
<canvas id="myCanvas" width="578" height="200">
</canvas>
<p>
    Image:
</p>
<img id="canvasImg" alt="Right click to save me!">

工作原理...

在画布上绘制完某些内容后,我们可以创建一个用户可以保存的图像,方法是使用toDataURL()方法获取图像数据 URL,然后将image对象的源设置为数据 URL。一旦图像加载完成(因为图像是直接加载的,不需要向 Web 服务器发出请求,所以几乎是瞬间完成的),用户可以右键单击图像将其保存到本地计算机。

使用数据 URL 加载画布

要使用数据 URL 加载画布,我们可以通过创建一个带有数据 URL 的image对象并使用我们的好朋友drawImage()将其绘制到画布上来扩展上一个示例。在这个示例中,我们将通过创建一个简单的 Ajax 调用来从文本文件获取数据 URL,然后使用该 URL 将图像绘制到画布上。当然,在现实世界中,您可能会从本地存储获取图像数据 URL,或者通过调用数据服务来获取。

按照以下步骤使用数据 URL 加载画布绘图:

操作步骤...

  1. 定义loadCanvas()函数,该函数以数据 URL 作为输入,定义画布上下文,使用数据 URL 创建一个新的图像,然后在加载完成后将图像绘制到画布上:
function loadCanvas(dataURL){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    // load image from data url
    var imageObj = new Image();
    imageObj.onload = function(){
        context.drawImage(this, 0, 0);
    };

    imageObj.src = dataURL;
}
  1. 进行一个 AJAX 调用,以获取存储在服务器上的数据 URL,然后在接收到响应时使用响应文本调用loadCanvas()
window.onload = function(){
    // make ajax call to get image data url
    var request = new XMLHttpRequest();
    request.open("GET", "dataURL.txt", true);
    request.onreadystatechange = function(){
        if (request.readyState == 4) { 
            if (request.status == 200) { // successful response
                loadCanvas(request.responseText);
            }
        }
    };
    request.send(null);
};
  1. 将 canvas 标签嵌入到 HTML 文档的 body 中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

工作原理...

要从 Web 服务器获取图像数据 URL,我们可以设置一个 AJAX 调用(异步 JavaScript 和 XML)来向 Web 服务器发出请求并获取数据 URL 作为响应。当我们得到状态码 200 时,这意味着请求和响应成功,我们可以从request.responseText获取图像数据 URL,然后将其传递给loadCanvas()函数。然后,该函数将创建一个新的image对象,将其源设置为数据 URL,然后在加载完成后将图像绘制到画布上。

创建一个像素化图像焦点

寻找一种时髦的方法来聚焦图像?像素化图像焦点怎么样?在这个示例中,我们将通过循环一个像素化算法来探索图像像素化的艺术,直到完全聚焦。

创建像素化图像焦点

注意

警告:由于getImageData()方法的安全限制,必须在 Web 服务器上运行此示例。

按照以下步骤创建一个逐渐聚焦图像的像素化函数:

操作步骤...

  1. 定义focusImage()函数,该函数根据像素化值去像素化图像:
function focusImage(canvas, context, imageObj, pixelation){
    var sourceWidth = imageObj.width;
    var sourceHeight = imageObj.height;
    var sourceX = canvas.width / 2 - sourceWidth / 2;
    var sourceY = canvas.height / 2 - sourceHeight / 2;
    var destX = sourceX;
    var destY = sourceY;

    var imageData = context.getImageData(sourceX, sourceY, sourceWidth, sourceHeight);
    var data = imageData.data;

    for (var y = 0; y < sourceHeight; y += pixelation) {
        for (var x = 0; x < sourceWidth; x += pixelation) {
            // get the color components of the sample pixel
            var red = data[((sourceWidth * y) + x) * 4];
            var green = data[((sourceWidth * y) + x) * 4 + 1];
            var blue = data[((sourceWidth * y) + x) * 4 + 2];

            // overwrite pixels in a square below and to
            // the right of the sample pixel, whos width and
            // height are equal to the pixelation amount
            for (var n = 0; n < pixelation; n++) {
                for (var m = 0; m < pixelation; m++) {
                    if (x + m < sourceWidth) {
                        data[((sourceWidth * (y + n)) + (x + m)) * 4] = red;
                        data[((sourceWidth * (y + n)) + (x + m)) * 4 + 1] = green;
                        data[((sourceWidth * (y + n)) + (x + m)) * 4 + 2] = blue;
                    }
                }
            }
        }
    }

    // overwrite original image
    context.putImageData(imageData, destX, destY);
}
  1. 定义画布上下文、决定图像聚焦速度的 fps 值、相应的时间间隔和初始像素化量:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
    var fps = 20; // frames / second
    var timeInterval = 1000 / fps; // milliseconds

    // define initial pixelation.  The higher the value,
    // the more pixelated the image is.  The image is
    // perfectly focused when pixelation = 1;
    var pixelation = 40;
  1. 创建一个新的image对象,将onload属性设置为创建一个定时循环的函数,该函数调用focusImage()函数并递减每次调用的像素化值,直到图像聚焦,然后在onload定义之外设置图像源:
    var imageObj = new Image();
    imageObj.onload = function(){
        var sourceWidth = imageObj.width;
        var sourceHeight = imageObj.height;
        var destX = canvas.width / 2 - sourceWidth / 2;
        var destY = canvas.height / 2 - sourceHeight / 2;

        var intervalId = setInterval(function(){
            context.drawImage(imageObj, destX, destY);

            if (pixelation < 1) {
                clearInterval(intervalId);
            }
            else {
                focusImage(canvas, context, imageObj, pixelation--);
            }
        }, timeInterval);
    };
    imageObj.src = "jet_300x214.jpg";
};
  1. 将 canvas 标签嵌入到 HTML 文档的 body 中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

工作原理...

在进入像素化算法之前,让我们定义像素化。当人眼可以检测到构成图像的单个像素时,图像就会出现像素化。老式视频游戏图形和被放大的小图像是像素化的很好的例子。通俗地说,如果我们将像素化定义为构成图像的像素可见的条件,这就意味着像素本身相当大。事实上,像素越大,图像就越像素化。我们可以利用这一观察结果来创建像素化算法。

要创建一个像素化图像的算法,我们可以对图像进行颜色采样,然后用超大像素代替。由于像素需要是正方形的,我们可以构造 1 x 1(标准像素大小)、2 x 2、3 x 3、4 x 4 等像素大小。像素越大,图像看起来就越像素化。

到目前为止,我们的方法只是简单地循环遍历data属性中的所有像素,并用简单的算法转换它们,而没有太关注哪些像素正在被更新。然而,在这个方法中,我们需要通过查看基于 x,y 坐标的图像特定区域来检查样本像素。我们可以使用以下方程式根据 x,y 坐标来挑选出像素的 RGBA 分量:

var red = data[((sourceWidth * y) + x) * 4];
var green = data[((sourceWidth * y) + x) * 4 + 1];
var blue = data[((sourceWidth * y) + x) * 4 + 2];

有了这些方程,我们可以使用setInterval()在一段时间内渲染一系列像素化的图像,其中每个连续的像素化图像都比上一个图像少像素化,直到像素化值等于 0,图像恢复到原始状态。

第四章:掌握变换

在本章中,我们将涵盖:

  • 转换画布上下文

  • 旋转画布上下文

  • 缩放画布上下文

  • 创建镜像变换

  • 创建自定义变换

  • 剪切画布上下文

  • 使用状态堆栈处理多个变换

  • 将圆形变换为椭圆

  • 旋转图像

  • 绘制一个简单的标志并随机化其位置、旋转和缩放

介绍

本章将揭示画布变换的威力,它可以极大地简化复杂的绘图,并提供新的功能,否则我们将无法拥有。到目前为止,我们一直在屏幕上直接定位元素的 x 和 y 坐标。如果您已经计算出复杂绘图的每个点的坐标,然后后来决定整个绘图需要重新定位、旋转或缩放,这可能很快成为一个问题。画布变换通过使开发人员能够在不必重新计算构成绘图的每个点的坐标的情况下,转换、旋转和缩放画布的整个部分来解决这个问题。此外,画布变换还使开发人员能够旋转和缩放图像和文本,这是没有变换不可能的。让我们开始吧!

转换画布上下文

在这个示例中,我们将学习如何执行 HTML5 画布 API 提供的最基本和最常用的变换——平移。如果您对变换术语不熟悉,“平移”只是一种花哨的说法,意思是“移动”。在这种情况下,我们将把上下文移动到画布上的新位置。

转换画布上下文

如何做...

按照以下步骤绘制一个移动到画布中心的平移矩形:

  1. 定义画布上下文和矩形的尺寸:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    var rectWidth = 150;
    var rectHeight = 75;
  1. 将上下文转换为画布的中心:
    // translate context to center of canvas
    context.translate(canvas.width / 2, canvas.height / 2);
  1. 绘制一个中心位于平移画布上下文左上角的矩形:
    context.fillStyle = "blue";
    context.fillRect(-rectWidth / 2, -rectHeight / 2, rectWidth, rectHeight);
};
  1. 在 HTML 文档的 body 内嵌入 canvas 标签:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

它是如何工作的!

画布上下文转换,步骤是如何工作的...

HTML5 画布变换的思想是以某种方式变换画布上下文,然后在画布上绘制。在这个示例中,我们已经将画布上下文平移,使得上下文的左上角移动到画布的中心:

context.translate(tx,ty);

tx参数对应于水平平移,ty参数对应于垂直平移。一旦上下文被变换,我们就可以在画布上下文的左上角上绘制一个居中的矩形。最终结果是一个被平移的矩形,它被移动到画布的中心。

旋转画布上下文

HTML5 画布 API 提供的下一种类型的变换,也可以说是最方便的变换,是旋转变换。在这个示例中,我们将首先使用平移变换来定位画布上下文,然后使用rotate()方法来旋转上下文。

旋转画布上下文

如何做...

按照以下步骤绘制一个旋转的矩形:

  1. 定义画布上下文和矩形的尺寸:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    var rectWidth = 150;
    var rectHeight = 75;
  1. 将画布上下文平移,然后将其旋转 45 度:
    // translate context to center of canvas
    context.translate(canvas.width / 2, canvas.height / 2);

    // rotate context 45 degrees clockwise
    context.rotate(Math.PI / 4);
  1. 绘制矩形:
    context.fillStyle = "blue";
    context.fillRect(-rectWidth / 2, -rectHeight / 2, rectWidth, rectHeight);
};
  1. 在 HTML 文档的 body 内嵌入 canvas 标签:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

它是如何工作的!

它是如何工作的...

为了定位和旋转矩形,我们可以将画布上下文转换为画布的中心,就像我们在上一个示例中所做的那样,然后我们可以使用旋转变换来旋转画布上下文,这将使上下文围绕上下文的左上角旋转:

canvas.rotate(theta);

参数theta以弧度表示,变换将上下文顺时针旋转。一旦上下文被平移和旋转,我们就可以在画布上下文的左上角上绘制一个居中的矩形。最终结果是一个以画布为中心的旋转矩形。

提示

请注意,我们通过链接两种不同的变换(平移和旋转)来实现了这个结果。HTML5 画布 API 提供的三种变换都会将一个变换矩阵应用于当前状态。例如,如果我们连续应用三次将画布上下文向右移动 10 像素的平移,最终结果将是向右移动 30 像素的平移。

如果我们想要围绕不同的点旋转矩形,比如说矩形的右下角,我们可以简单地在画布上下文的原点处绘制矩形的右下角。

在创建复杂的 HTML5 画布绘图时,平移和旋转是最常用的变换链。正如我们将在下一章中看到的那样,旋转在动画形状围绕轴旋转时非常有用。

参见...

  • 在第五章中摆动钟摆

  • 在第五章中制作机械齿轮的动画

  • 在第五章中制作时钟的动画

缩放画布上下文

除了平移和旋转之外,HTML5 画布 API 还为我们提供了一种缩放画布上下文的方法。在这个示例中,我们将使用scale()方法缩小画布上下文的高度。

缩放画布上下文

如何做...

按照以下步骤绘制一个缩放的矩形:

  1. 定义画布上下文和矩形的尺寸:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    var rectWidth = 150;
    var rectHeight = 75;
  1. 平移画布上下文,然后将画布上下文的高度缩小 50%:
    // translate context to center of canvas
    context.translate(canvas.width / 2, canvas.height / 2);

    // scale down canvas height by half
    context.scale(1, 0.5);
  1. 绘制一个中心位于画布上下文左上角的矩形:
    context.fillStyle = "blue";
    context.fillRect(-rectWidth / 2, -rectHeight / 2, rectWidth, rectHeight);
};
  1. 将画布标签嵌入 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

工作原理...

要缩放画布上下文,我们可以简单地使用缩放变换:

context.scale(sx,sy);

在上下文的默认状态下,sxsy参数被标准化为11。正如您所期望的那样,sx参数对应于水平比例,sy参数对应于垂直比例。

在这个示例中,我们通过将sy参数设置为0.5来将垂直上下文缩小了 50%。另一方面,如果我们将sy分配给大于1的值,上下文将垂直拉伸。正如我们将在下一个示例中看到的,如果我们将sxsy值分配为负值,我们将水平或垂直地翻转画布上下文,从而创建一个镜像变换。

参见...

  • 在第五章中振荡气泡

创建镜像变换

缩放变换的另一个有趣用途是它能够垂直或水平地镜像画布上下文。在这个示例中,我们将水平镜像画布上下文,然后写出一些倒序的文本。

创建镜像变换

如何做...

按照以下步骤将文本写成倒序:

  1. 定义画布上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 平移画布上下文,然后使用负的x值水平翻转上下文:
    // translate context to center of canvas
    context.translate(canvas.width / 2, canvas.height / 2);

    // flip context horizontally
    context.scale(-1, 1);
  1. 写出“Hello World!”:
    context.font = "30pt Calibri";
    context.textAlign = "center";
    context.fillStyle = "blue";
    context.fillText("Hello World!", 0, 0);
};
  1. 将画布标签嵌入 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

工作原理...

要使用 HTML5 画布 API 创建镜像变换,我们可以在使用画布上下文的scale方法时将sxsy赋予负值:

context.scale(-sx,-sy);

在这个示例中,我们将画布上下文平移到画布的中心,然后通过应用scale()变换的-sx值来水平翻转上下文。

创建自定义变换

如果您想执行除平移、缩放或旋转之外的自定义变换,HTML5 画布 API 还提供了一种方法,允许我们定义一个自定义变换矩阵,该矩阵可以应用于当前上下文。在这个示例中,我们将手动创建一个平移变换,以演示transform()方法的工作原理。

创建自定义变换

如何做...

按照以下步骤执行自定义变换:

  1. 定义矩形的画布上下文和尺寸:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    var rectWidth = 150;
    var rectHeight = 75;
  1. 通过手动平移画布上下文应用自定义变换:
    // translation matrix:
    //  1  0  tx              
    //  0  1  ty
    //  0  0  1  
    var tx = canvas.width / 2;
    var ty = canvas.height / 2;

    // apply custom transform
    context.transform(1, 0, 0, 1, tx, ty); 
  1. 绘制矩形:
    context.fillStyle = "blue";
    context.fillRect(-rectWidth / 2, -rectHeight / 2, rectWidth, rectHeight);
};
  1. 将画布元素嵌入到 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

在本示例中,我们通过将自定义平移变换矩阵应用于上下文状态来创建了自定义平移变换。变换矩阵只是一个二维矩阵,可以用来将当前矩阵转换为新矩阵。可以使用画布上下文的transform()方法将自定义变换应用于上下文状态:

context.transform(a,b,c,d,e,f);

其中参数abcdef对应于变换矩阵的以下组成部分:

自定义变换执行步骤的工作原理...

这里,x'y'是应用变换后的新矩阵xy分量。平移变换的变换矩阵如下所示:

自定义变换执行步骤的工作原理...

其中tx是水平平移,ty是垂直平移。

还有更多...

除了transform()方法之外,还可以使用画布上下文的setTransform()方法设置变换矩阵,该方法将变换矩阵应用于当前上下文状态:

context.setTransform(a,b,c,d,e,f);

如果您想直接使用经过公式化的变换矩阵设置上下文的变换矩阵,而不是通过一系列变换获得相同的结果,那么这种方法可能会很有用。

倾斜画布上下文

在本示例中,我们将使用画布上下文的transform()方法从水平方向对画布上下文进行自定义剪切变换,利用了我们从画布上下文的transform()方法中学到的知识。

倾斜画布上下文

如何做...

按照以下步骤绘制一个倾斜的矩形:

  1. 定义矩形的画布上下文和尺寸:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    var rectWidth = 150;
    var rectHeight = 75;
  1. 平移画布上下文,然后对上下文应用自定义剪切变换:
    // shear matrix:
    //  1  sx  0              
    //  sy  1  0
    //  0  0  1  

    var sx = 0.75; // 0.75 horizontal shear
    var sy = 0; // no vertical shear
    // translate context to center of canvas
    context.translate(canvas.width / 2, canvas.height / 2);

    // apply custom transform
    context.transform(1, sy, sx, 1, 0, 0); 
  1. 绘制矩形:
    context.fillStyle = "blue";
    context.fillRect(-rectWidth / 2, -rectHeight / 2, rectWidth, rectHeight);
};
  1. 将画布元素嵌入到 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

要使画布上下文倾斜,可以应用以下变换矩阵:

它是如何工作的...

我们可以使用transform()方法和以下参数:

context.transform(1,sy,sx,1,0,0);

我们增加sx的值,上下文水平倾斜就越大。我们增加sy的值,上下文垂直倾斜就越大。

使用状态堆栈处理多个变换

现在我们已经很好地掌握了 HTML5 画布 API 的变换,我们现在可以进一步探索画布状态堆栈,并了解它在变换方面对我们有什么作用。在第二章中,形状绘制和合成,我们介绍了状态堆栈,这是画布 API 的一个非常强大但有时被忽视的属性。尽管画布状态堆栈可以帮助管理样式,但它最常见的用法是保存和恢复变换状态。在本示例中,我们将在每次变换之间保存画布状态,并在恢复每个状态后绘制一系列矩形,以查看效果。

使用状态堆栈处理多个变换

如何做...

按照以下步骤构建具有四种不同状态的状态堆栈,然后在弹出每个状态后绘制一个矩形:

  1. 定义矩形的画布上下文和尺寸:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");

    var rectWidth = 150;
    var rectHeight = 75;
  1. 将当前变换状态,即默认状态,推入状态堆栈,并平移上下文:
    context.save(); // save state 1
    context.translate(canvas.width / 2, canvas.height / 2);
  1. 将当前变换状态,即已平移的状态,推入堆栈,并旋转上下文:
    context.save(); // save state 2
    context.rotate(Math.PI / 4);
  1. 将当前变换状态,即已平移和旋转的状态,推入堆栈,并缩放上下文:
    context.save(); // save state 3
    context.scale(2, 2);
  1. 绘制一个蓝色的矩形:
  // draw the rectangle
    context.fillStyle = "blue";
    context.fillRect(-rectWidth / 2, -rectHeight / 2, rectWidth, rectHeight);
  1. 从状态堆栈中弹出当前状态以恢复先前的状态,然后绘制一个红色的矩形:
    context.restore(); // restore state 3
    context.fillStyle = "red";
    context.fillRect(-rectWidth / 2, -rectHeight / 2, rectWidth, rectHeight);
  1. 从状态堆栈中弹出当前状态以恢复先前的状态,然后绘制一个黄色的矩形:
    context.restore(); // restore state 2
    context.fillStyle = "yellow";
    context.fillRect(-rectWidth / 2, -rectHeight / 2, rectWidth, rectHeight);
  1. 从状态堆栈中弹出当前状态以恢复先前的状态,然后绘制一个绿色的矩形:
    context.restore(); // restore state 1
    context.fillStyle = "green";
    context.fillRect(-rectWidth / 2, -rectHeight / 2, rectWidth, rectHeight);
};
  1. 将画布标签嵌入 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

工作原理...

这个方法执行了一系列三次变换,平移、旋转和缩放变换,同时使用save()操作将每个变换状态推送到状态堆栈上。当绘制蓝色矩形时,它被居中、旋转和缩放。此时,状态堆栈有四个状态(从底部到顶部):

  1. 默认状态

  2. 已翻译状态

  3. 已翻译和旋转状态

  4. 当前状态(已翻译、旋转和缩放状态)

绘制蓝色矩形后,我们使用restore()方法弹出状态堆栈中的顶部状态,并将画布上下文恢复到第三个状态,其中画布上下文被平移和旋转。然后绘制红色矩形,您会看到它已经被平移和旋转,但没有被缩放。接下来,我们再次使用restore()方法弹出状态堆栈中的顶部状态,并恢复第二个状态,其中画布上下文仅被平移。然后我们绘制一个黄色的矩形,它确实只是被平移。最后,我们再次调用restore()方法,弹出状态堆栈中的顶部状态,并返回到默认状态。当我们绘制绿色矩形时,它出现在原点,因为没有应用任何变换。

提示

使用状态堆栈,我们可以在变换状态之间跳转,这样我们就不必不断地将状态重置为默认状态,然后分别对每个元素进行平移。此外,我们还可以使用保存-恢复组合来封装一小段代码的变换,而不会影响后面绘制的形状。

将圆形变成椭圆

缩放变换最常见的应用之一是将圆水平或垂直拉伸以创建椭圆。在这个方法中,我们将通过平移画布上下文、水平拉伸它,然后绘制一个圆来创建一个椭圆。

将圆形变成椭圆

如何做...

按照以下步骤绘制一个椭圆:

  1. 定义画布上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 将当前变换状态(默认状态)推送到状态堆栈上:
    context.save(); // save state
  1. 定义圆的尺寸:
    var centerX = 0;
    var centerY = 0;
    var radius = 50;
  1. 将画布上下文平移到画布的中心,然后缩放上下文宽度以向外伸展:
    context.translate(canvas.width / 2, canvas.height / 2);
    context.scale(2, 1);
  1. 绘制圆:
    context.beginPath();
    context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
  1. 恢复先前的变换状态,即默认状态,并从状态堆栈中弹出当前的变换状态:
    context.restore(); // restore original state
  1. 对椭圆应用样式:
    context.fillStyle = "#8ED6FF";
    context.fill();
    context.lineWidth = 5;
    context.strokeStyle = "black";
    context.stroke();
};
  1. 将画布标签嵌入 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

工作原理...

要使用 HTML5 画布 API 绘制椭圆,我们可以简单地使用translate()方法将上下文平移到所需的位置,使用scale()方法在垂直或水平方向上拉伸上下文,然后绘制圆。在这个方法中,我们已经将画布上下文水平拉伸,以创建一个宽度是高度两倍的椭圆。

因为我们想要对椭圆应用描边样式,我们可以使用保存-恢复组合来封装用于创建椭圆的变换,以便它们不会影响椭圆后面的样式。

如果您自己尝试这个方法,并且删除save()restore()方法,您会发现椭圆顶部和底部的线条厚度为 5 像素,椭圆两侧的线条厚度为 10 像素,因为描边样式也随着圆形在水平方向被拉伸。

另请参阅...

  • 在第五章中振荡一个气泡*

旋转图像

在这个食谱中,我们将通过平移和旋转画布上下文来旋转图像,然后在变换后的上下文上绘制图像。

旋转图像

如何做...

按照以下步骤旋转图像:

  1. 定义一个画布上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 创建一个新的image对象并设置其onload属性:
    var imageObj = new Image();
    imageObj.onload = function(){
  1. 当图像加载时,将上下文转换到画布的中心,逆时针旋转上下文 45 度,然后绘制图像:
        // translate context to center of canvas
        context.translate(canvas.width / 2, canvas.height / 2);

        // rotate context by 45 degrees counter clockwise
        context.rotate(-1 * Math.PI / 4);
        context.drawImage(this, -1 * imageObj.width / 2, -1 * imageObj.height / 2);
    };
  1. 设置图像的来源:
    imageObj.src = "jet_300x214.jpg";
};
  1. 在 HTML 文档的 body 中嵌入 canvas 标签:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

要旋转图像,我们可以简单地使用translate()方法定位画布上下文,使用rotate()方法旋转上下文,然后使用drawImage()方法绘制图像。

还有更多...

值得注意的是,除了旋转图像之外,与图像一起使用的另一个常见变换是镜像变换。要镜像图像,我们可以将上下文转换到所需的位置,使用scale(-1,1)水平反转上下文,或者使用scale(1,-1)垂直反转上下文,然后使用drawImage()绘制图像。

另请参阅...

  • 创建镜像变换食谱

绘制一个简单的标志并随机化其位置、旋转和比例

这个食谱的目的是通过转换复杂的形状来演示变换的实际用途。在这种情况下,我们的复杂形状将是一个标志,它只是一些文本,下面有几条波浪线。当我们想要转换、旋转或缩放复杂的形状时,变换非常有用。开发人员经常创建函数,在原点绘制复杂的东西,然后使用变换将其移动到屏幕上的某个位置。在这个食谱中,我们将在屏幕上绘制五个随机位置、旋转和缩放的标志。

绘制一个简单的标志并随机化其位置、旋转和比例

如何做...

按照以下步骤绘制五个随机位置、旋转和缩放的标志:

  1. 定义drawLogo()函数,通过写出文本并在其下方绘制两条波浪线来绘制一个简单的标志:
function drawLogo(context){
    // draw Hello Logo! text
    context.beginPath();
    context.font = "10pt Calibri";
    context.textAlign = "center";
    context.textBaseline = "middle";
    context.fillStyle = "blue";
    context.fillText("Hello Logo!", 0, 0);
    context.closePath();

  // define style for both waves
    context.lineWidth = 2;
    context.strokeStyle = "blue";

    // draw top wave
    context.beginPath();
    context.moveTo(-30, 10);
    context.bezierCurveTo(-5, 5, 5, 15, 30, 10);
    context.stroke();

    // draw bottom wave
    context.beginPath();
    context.moveTo(-30, 15);
    context.bezierCurveTo(-5, 10, 5, 20, 30, 15);
    context.stroke();
}
  1. 定义getRandomX()函数,返回 0 到画布宽度之间的随机X值:
function getRandomX(canvas){
    return Math.round(Math.random() * canvas.width);
}
  1. 定义getRandomY()函数,返回 0 到画布高度之间的随机Y值:
function getRandomY(canvas){
    return Math.round(Math.random() * canvas.height);
}
  1. 定义getRandomSize()函数,返回 0 到 5 之间的随机大小:
function getRandomSize(){
    return Math.round(Math.random() * 5);
}
  1. 定义getRandomAngle()函数,返回 0 到 2π之间的随机角度:
function getRandomAngle(){
    return Math.random() * Math.PI * 2;
}
  1. 定义画布上下文:
window.onload = function(){
    var canvas = document.getElementById("myCanvas");
    var context = canvas.getContext("2d");
  1. 创建一个循环,绘制五个随机位置、旋转和缩放的标志:
    // draw 5 randomly transformed logos
    for (var n = 0; n < 5; n++) {
        context.save();
        // translate to random position
        context.translate(getRandomX(canvas), getRandomY(canvas));

        // rotate by random angle
        context.rotate(getRandomAngle());

        // scale by random size
        var randSize = getRandomSize();
        context.scale(randSize, randSize);

        // draw logo
        drawLogo(context);
        context.restore();
    }
};
  1. 在 HTML 文档的 body 中嵌入 canvas 标签:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

首先,要绘制我们简单的标志,我们可以创建一个名为drawLogo()的函数,它在原点写出文本Hello Logo!,然后使用bezierCurveTo()方法为每个波绘制两条波浪线。

接下来,要绘制五个随机位置、旋转和缩放的标志,我们可以创建一些实用函数,返回位置、旋转和缩放的随机值,然后创建一个for循环,每次迭代使用保存-恢复组合来引入状态范围,执行三次变换,然后使用drawLogo()方法绘制标志。如果你自己尝试这个食谱,你会发现每次刷新屏幕时,五个标志的位置、旋转和缩放都不同。

第五章:通过动画让画布活跃起来

在本章中,我们将涵盖:

  • 创建一个动画类

  • 创建线性运动

  • 创建加速度

  • 创建振荡

  • 振荡气泡

  • 摆动钟摆

  • 动画机械齿轮

  • 动画时钟

  • 模拟粒子物理

  • 创建微观生命形式

  • 压力测试画布并显示 FPS

介绍

在本书的前半部分,我们介绍了 HTML5 画布的基本功能,包括路径绘制、形状绘制、图像和视频处理以及变换。本章重点介绍动画,这不是 HTML5 画布 API 的一部分。尽管 API 没有提供动画功能,但我们肯定可以创建一个动画类,用于支持动画项目。我们将涵盖基本的运动类型,包括线性运动、加速度和振荡,并利用所学知识创建一些真正令人惊叹的演示。让我们开始吧!

创建一个动画类

由于 HTML5 画布 API 没有提供动画方法,我们必须为处理动画阶段创建自己的动画类。本教程将介绍动画的基础知识,并为我们未来的动画项目提供一个动画类。

准备好了...

由于浏览器和计算机硬件并非完全相同,因此重要的是要了解每个动画的最佳 FPS(每秒帧数)值取决于浏览器、计算机硬件和动画算法。因此,开发人员很难弄清楚每个用户的最佳 FPS 值是多少。幸运的是,浏览器现在正在实现window对象的requestAnimationFrame方法,该方法可以自动确定动画的最佳 FPS(谢天谢地)。正如我们将在本章后面看到的,流畅动画的典型 FPS 值在 40 到 60 帧之间。

准备好了...

看一下前面的图表。要创建动画,我们首先需要初始化舞台上的对象。我们可以将画布称为“舞台”,因为画布上的移动对象可以看作是舞台上的“演员”。此外,舞台的类比使我们感到画布中的东西正在发生,而不仅仅是静静地坐在那里。一旦我们的对象初始化完成,我们就可以开始一个动画循环,更新舞台,清除画布,重绘舞台,然后请求一个新的动画帧。

由于这种行为可以定义任何类型的动画,所以我们创建一个处理这些步骤的动画类对我们来说是有意义的。

操作方法...

按照以下步骤创建一个动画类,该类将支持本章的动画示例:

  1. 定义Animation构造函数并创建一个跨浏览器的requestAnimationFrame方法:
var Animation = function(canvasId){
    this.canvas = document.getElementById(canvasId);
    this.context = this.canvas.getContext("2d");
    this.t = 0;
    this.timeInterval = 0;
    this.startTime = 0;
    this.lastTime = 0;
    this.frame = 0;
    this.animating = false;

    // provided by Paul Irish
    window.requestAnimFrame = (function(callback){
        return window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.oRequestAnimationFrame ||
        window.msRequestAnimationFrame ||
        function(callback){
            window.setTimeout(callback, 1000 / 60);
        };
    })();
};
  1. 定义getContext()方法:
Animation.prototype.getContext = function(){
    return this.context;
};
  1. 定义getCanvas()方法:
Animation.prototype.getCanvas = function(){
    return this.canvas;
};
  1. 定义clear()方法,清除画布:
Animation.prototype.clear = function(){
    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
};
  1. 定义setStage()方法,设置stage()函数。该函数将为每个动画帧执行:
Animation.prototype.setStage = function(func){
    this.stage = func;
};
  1. 定义isAnimating()方法:
Animation.prototype.isAnimating = function(){
    return this.animating;
};
  1. 定义getFrame()方法,返回帧数:
Animation.prototype.getFrame = function(){
    return this.frame;
};
  1. 定义start()方法,开始动画:
Animation.prototype.start = function(){
    this.animating = true; 
    var date = new Date();
    this.startTime = date.getTime();
    this.lastTime = this.startTime;

    if (this.stage !== undefined) {
        this.stage();
    }

    this.animationLoop();
};
  1. 定义stop()方法,停止动画:
Animation.prototype.stop = function(){
    this.animating = false;
};
  1. 定义getTimeInterval()方法,返回上一帧和当前帧之间的毫秒时间:
Animation.prototype.getTimeInterval = function(){
    return this.timeInterval;
};
  1. 定义getTime()方法,返回动画运行的毫秒时间:
Animation.prototype.getTime = function(){
    return this.t;
};
  1. 定义getFps()方法,返回动画的当前 FPS:
Animation.prototype.getFps = function(){
    return this.timeInterval > 0 ? 1000 / this.timeInterval : 0;
};
  1. 定义animationLoop()方法,处理动画循环:
Animation.prototype.animationLoop = function(){
    var that = this;

    this.frame++;
    var date = new Date();
    var thisTime = date.getTime();
    this.timeInterval = thisTime - this.lastTime;
    this.t += this.timeInterval;
    this.lastTime = thisTime;

    if (this.stage !== undefined) {
        this.stage();
    }

    if (this.animating) {
        requestAnimFrame(function(){
            that.animationLoop();
        });
    }
};

工作原理...

Animation类的思想是通过封装和隐藏动画所需的所有逻辑,简化我们的动画项目,例如提供帧之间的时间间隔,处理动画循环和清除画布。

Animation类的关键在于Animation构造函数中,我们设置了window对象的requestAnimFrame方法。这个方法充当了requestAnimationFrame的跨浏览器实现,允许用户的浏览器决定动画的最佳 FPS。FPS 是完全动态的,并且会在整个动画过程中发生变化。

我们的Animation类还提供了一些方便的方法,比如“getTimeInterval()”,它返回自上一个动画帧以来的毫秒数,“getTime()”方法返回动画自启动以来运行的毫秒数,“start()”方法启动动画,“stop()”方法停止动画,“clear()”方法清除画布。

现在我们已经有一个可以投入使用的Animation类,本章中的其余动画以及您未来的动画项目都将变得轻而易举。

创建线性运动

在这个示例中,我们将通过创建一个简单的线性运动动画来尝试我们的Animation类,将一个盒子从画布的左侧移动到右侧:

创建线性运动

如何做…

按照以下步骤将一个盒子从画布的一侧移动到另一侧:

  1. 链接到Animation类:
<head>
    <script src="img/animation.js">
    </script>
  1. 实例化一个Animation对象并获取画布上下文:
    <script>
        window.onload = function(){
            var anim = new Animation("myCanvas");
            var canvas = anim.getCanvas();
            var context = anim.getContext();
  1. 定义盒子的线性速度并创建一个包含盒子位置和大小的box对象:
            var linearSpeed = 100; // pixels / second
            var box = {
                x: 0,
                y: canvas.height / 2 - 25,
                width: 100,
                height: 50
            };
  1. 设置“stage()”函数,更新盒子的位置,清除画布并绘制盒子:
        anim.setStage(function(){
            // update
            var linearDistEachFrame = linearSpeed * this.getTimeInterval() / 1000;

            if (box.x < canvas.width - box.width) {
                box.x += linearDistEachFrame;
            }
            else {
                anim.stop();
            }

            // clear
            this.clear();

            // draw
            context.beginPath();
            context.fillStyle = "blue";
            context.fillRect(box.x, box.y, box.width, box.height);
        });
  1. 开始动画:
        anim.start();
    };
    </script>
</head>
  1. 将画布嵌入到 HTML 文档的主体中:
<body>
    <canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
    </canvas>
</body>

它是如何工作…

要创建简单的线性运动,首先我们需要实例化一个新的Animation对象,然后获取画布和上下文。接下来,我们可以定义盒子的速度,对于这个示例,我们将速度设置为每秒 100 像素,并且可以创建一个包含盒子位置和大小的box对象。

现在我们的盒子已经初始化,我们可以定义“stage()”函数,该函数将在动画循环中执行。对于每个动画循环,我们可以通过首先计算盒子在上一帧和当前帧之间移动的距离,然后通过添加它移动的距离来更新盒子的 x 位置。一旦盒子到达画布的边缘,我们可以通过调用“stop()”来停止动画。

最后,一旦“stage()”函数被定义,我们可以使用“start()”方法开始动画。

另请参阅…

  • 在第二章中绘制一个矩形

创建加速度

现在我们已经掌握了动画的基础知识,让我们尝试一些更复杂的东西,通过重力加速一个盒子向下移动。

startAnimation()方法创建加速度

如何做…

按照以下步骤在画布顶部绘制一个盒子,由于重力的作用而向下移动:

  1. 链接到Animation类:
<head>
    <script src="img/animation.js">
    </script>
  1. 实例化一个Animation对象并获取画布上下文:
    <script>
        window.onload = function(){
            var anim = new Animation("myCanvas");
            var canvas = anim.getCanvas();
            var context = anim.getContext();
  1. 定义重力并创建一个包含盒子位置、x 和 y 速度以及大小的box对象:
            var gravity = 2; // pixels / second²
            var box = {
                x: canvas.width / 2 - 50,
                y: 0,
                vx: 0,
                vy: 0,
                width: 100,
                height: 50
            };
  1. 设置“stage()”函数,更新盒子,清除画布并绘制盒子:
            anim.setStage(function(){
                // update
        if (this.getTime() > 1000) {
                    var speedIncrementEachFrame = gravity * anim.getTimeInterval() / 1000; // pixels / second
                    box.vy += speedIncrementEachFrame;
                    box.y += box.vy * this.getTimeInterval();

                    if (box.y > canvas.height - box.height) {
                        box.y = canvas.height - box.height;
                        this.stop();
                    }
        }

                // clear
                this.clear();

                // draw
                context.beginPath();
                context.fillStyle = "blue";
                context.fillRect(box.x, box.y, box.width, box.height);
            });
  1. 开始动画:
            anim.start(); 
        };
    </script>
</head>
  1. 将画布嵌入到 HTML 文档的主体中:
<body>
    <canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
    </canvas>
</body>

它是如何工作的…

要创建加速度,我们可以增加盒子的速度,更新盒子的位置,清除画布,然后绘制盒子。

我们可以通过添加由于重力引起的速度变化来计算盒子每帧的新 y 速度,这被设置为每秒 2 像素/秒²:

var speedIncrementEachFrame = gravity * anim.getTimeInterval() / 1000; // pixels / second
box.vy += speedIncrementEachFrame;

接下来,我们可以通过添加自上一帧以来移动的距离来计算框的新 y 位置:

box.y += box.vy * this.getTimeInterval();

换句话说,y 位置的变化等于框的速度乘以时间的变化(时间间隔)。

最后,我们可以添加一个条件来检查框是否已经到达画布的底部,如果是,我们可以使用stop()方法停止动画。

注意

当施加力到一个物体或粒子时,加速度特别有用。一些施加力的例子包括重力、空气阻力、阻尼、地板摩擦和电磁力。对于需要大量物理学的强烈动画,您可能考虑寻找一个开源矢量库,以帮助处理 x 和 y 方向的速度和加速度。

另请参阅...

  • 在第二章中绘制一个矩形

创建振荡

在这个配方中,我们将探讨第三种主要类型的运动——振荡。一些振荡的好例子是挂在弹簧上的弹簧、振荡气泡或来回摆动的摆。

振荡气泡

如何做...

按照以下步骤来使框来回振荡:

  1. 链接到Animation类:
<head>
    <script src="img/animation.js">
    </script>
  1. 实例化一个Animation对象并获取画布上下文:
    <script>
        window.onload = function(){
            var anim = new Animation("myCanvas");
            var canvas = anim.getCanvas();
            var context = anim.getContext();
  1. 创建一个包含框的位置和大小的box对象:
            var box = {
                x: 250,
                y: canvas.height / 2 - 25,
                width: 100,
                height: 50
            };
  1. 定义谐波振荡方程所需的参数:
            var centerX = canvas.width / 2 - box.width / 2;
            var amplitude = 150; // pixels
            var period = 2000; // ms
  1. 设置stage()函数,根据谐波振荡方程更新框的位置,清除画布,然后绘制框:
            anim.setStage(function(){
        // update
        box.x = amplitude * Math.sin(anim.getTime() * 2 * Math.PI / period) + centerX;

        // clear
        this.clear();

        // draw
                context.beginPath();
                context.rect(box.x, box.y, box.width, box.height);
                context.fillStyle = "blue";
                context.fill();
            });
  1. 开始动画:
            anim.start();
        };
    </script>
</head>
  1. 将画布嵌入到 HTML 文档的主体中:
<body>
    <canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
    </canvas>
</body>

工作原理...

页面加载后,我们可以实例化一个新的Animation对象,然后获取画布和上下文。

接下来,我们可以创建一个box对象,定义框的位置和大小,然后定义谐波振荡方程所需的变量:

x(t) = A * sin (t * 2π / T + Φ) + x0

对于这个配方,我们将振幅A设置为150,周期T设置为2秒,偏移x0和相位差Φ设置为0

对于每个动画帧,我们可以利用谐波振荡方程来更新框的位置,清除画布,然后使用rect()方法绘制框。

最后,我们可以使用start()方法开始动画。

另请参阅...

  • 在第二章中绘制一个矩形

振荡气泡

在这个配方中,我们将使用谐波振荡和画布变换的原理来创建一个逼真的振荡气泡。

振荡气泡

如何做...

按照以下步骤创建一个在空中漂浮的逼真的振荡气泡:

  1. 链接到Animation类:
<head>
    <script src="img/animation.js">
    </script>
  1. 实例化一个Animation对象并获取画布上下文:
    <script>
        window.onload = function(){
            // instantiate new animation object
            var anim = new Animation("myCanvas");
            var context = anim.getContext();
            var canvas = anim.getCanvas();
  1. 设置stage()函数,更新气泡的宽度和高度比例,清除画布,缩放画布上下文,然后绘制气泡:
            anim.setStage(function(){
                // update
                var widthScale = Math.sin(this.getTime() / 200) * 0.1 + 0.9;
                var heightScale = -1 * Math.sin(this.getTime() / 200) * 0.1 + 0.9;

                // clear
                this.clear();

                //draw
                context.beginPath();
                context.save();
                context.translate(canvas.width / 2, canvas.height / 2);
                context.scale(widthScale, heightScale);
                context.arc(0, 0, 65, 0, 2 * Math.PI, false);
                context.restore();
                context.fillStyle = "#8ED6FF";
                context.fill();
                context.lineWidth = 2;
                context.strokeStyle = "#555";
                context.stroke();

                context.beginPath();
                context.save();
                context.translate(canvas.width / 2, canvas.height / 2);
                context.scale(widthScale, heightScale);
                context.arc(-30, -30, 15, 0, 2 * Math.PI, false);
                context.restore();
                context.fillStyle = "white";
                context.fill();
            });
  1. 开始动画:
            anim.start();
        };
    </script>
</head>
  1. 将画布标签嵌入到 HTML 文档的主体中:
<body>
    <canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
    </canvas>
</body>

工作原理...

在我们讨论振荡气泡之前,首先介绍如何使用画布变换来在 x 和 y 方向上拉伸气泡是一个好主意。要绘制水平拉伸的气泡,我们可以将上下文转换到画布的中心,水平缩放上下文,然后绘制气泡。要绘制垂直拉伸的气泡,我们可以将其转换到画布的中心,垂直缩放上下文,然后绘制气泡。

为了使气泡振荡,我们需要交替改变画布的缩放方向,使水平缩放和垂直缩放始终等于一个常数,在我们的例子中是 1.8,这样气泡的体积保持不变。一旦建立了这种关系,我们就可以使用谐波振荡方程来振荡气泡的 x 和 y 缩放。

当页面首次加载时,我们可以实例化一个新的Animation对象并获取画布和上下文。接下来,我们可以设置stage()函数,负责更新气泡,清除画布,然后为每个动画帧绘制气泡。为了更新每一帧的气泡,我们可以使用谐波振荡方程来计算气泡的水平和垂直缩放。接下来,我们可以清除画布,然后使用arc()方法绘制气泡。

最后,一旦stage()函数设置好,我们就可以用start()方法开始动画。

另请参阅...

  • 在第二章中绘制圆形

  • 在第四章中缩放画布上下文

  • 在第四章中将圆形变成椭圆

摆动钟摆

与气泡示例不同,这个示例中的钟摆的宽度和高度不随时间变化,而是钟摆的角度随时间变化。

摆动钟摆

如何做...

按照以下步骤来摆动钟摆:

  1. 链接到Animation类:
<head>
    <script src="img/animation.js">
    </script>
  1. 实例化一个新的Animation对象并获取画布上下文:
    <script>
        window.onload = function(){
            var anim = new Animation("myCanvas");
            var canvas = anim.getCanvas();
            var context = anim.getContext();
  1. 定义钟摆的属性:
            var amplitude = Math.PI / 4; // 45 degrees
            var period = 4000; // ms
            var theta = 0;
            var pendulumLength = 250;
            var pendulumWidth = 10;
            var rotationPointX = canvas.width / 2;
            var rotationPointY = 20;
  1. 设置stage()函数,更新钟摆的角度,清除画布,然后绘制钟摆:
            anim.setStage(function(){
                // update
                theta = (amplitude * Math.sin((2 * Math.PI * this.getTime()) / period)) + Math.PI / 2;

                // clear
                this.clear();

                // draw top circle
                context.beginPath();
                context.arc(rotationPointX, rotationPointY, 15, 0, 2 * Math.PI, false);
                context.fillStyle = "#888";
                context.fill();

                // draw top inner circle
                context.beginPath();
                context.arc(rotationPointX, rotationPointY, 10, 0, 2 * Math.PI, false);
                context.fillStyle = "black";
                context.fill();

                // draw shaft
                context.beginPath();
                var endPointX = rotationPointX + (pendulumLength * Math.cos(theta));
                var endPointY = rotationPointY + (pendulumLength * Math.sin(theta));
                context.beginPath();
                context.moveTo(rotationPointX, rotationPointY);
                context.lineTo(endPointX, endPointY);
                context.lineWidth = pendulumWidth;
                context.lineCap = "round";
                context.strokeStyle = "#555";
                context.stroke();

                // draw bottom circle
                context.beginPath();
                context.arc(endPointX, endPointY, 40, 0, 2 * Math.PI, false);
                var grd = context.createLinearGradient(endPointX - 50, endPointY - 50, endPointX + 50, endPointY + 50);
                grd.addColorStop(0, "#444");
                grd.addColorStop(0.5, "white");
                grd.addColorStop(1, "#444");
                context.fillStyle = grd;
                context.fill();
            });
  1. 开始动画:
            anim.start();
        };
    </script>
</head>
  1. 将画布嵌入 HTML 文档的主体中:
<body>
    <canvas id="myCanvas" width="600" height="330" style="border:1px solid black;">
    </canvas>
</body>

工作原理...

当页面加载时,我们可以实例化一个新的Animation对象,然后获取画布和上下文。接下来,我们可以定义钟摆的属性,包括角振幅、周期、初始角度θ、钟摆长度、宽度和旋转中心。

一旦我们的钟摆初始化完成,我们可以设置stage()函数,它将使用谐波振荡方程更新钟摆角度,清除画布,然后立即重新绘制钟摆。

我们可以通过在旋转点绘制一对圆圈,从旋转点到钟摆重物绘制粗线来形成轴,然后在线的末端绘制一个大圆圈,具有漂亮的对角灰色渐变,以营造抛光表面的 illusio。

一旦stage()函数设置好,我们就可以用start()方法开始动画。

另请参阅...

  • 在第一章中绘制直线

  • 在第二章中绘制圆形

  • 在第二章中使用自定义形状和填充样式

动画机械齿轮

对于那些懂机械和工程的人,这个是给你们的。在这个示例中,我们将创建一个相互连接的旋转齿轮系统。

动画机械齿轮

如何做...

按照以下步骤来动画一个相互连接的齿轮系统:

  1. 链接到Animation类:
<head>
    <script src="img/animation.js">
    </script>
  1. 定义Gear类的构造函数:
    <script>
        function Gear(config){
            this.x = config.x;
            this.y = config.y;
            this.outerRadius = config.outerRadius;
            this.innerRadius = config.innerRadius;
            this.holeRadius = config.holeRadius;
            this.numTeeth = config.numTeeth;
            this.theta = config.theta;
            this.thetaSpeed = config.thetaSpeed;
            this.lightColor = config.lightColor;
            this.darkColor = config.darkColor;
            this.clockwise = config.clockwise;
            this.midRadius = config.outerRadius - 10;
        }
  1. 定义Gear类的draw方法,绘制gear对象:
        Gear.prototype.draw = function(context){
            context.save();
            context.translate(this.x, this.y);
            context.rotate(this.theta);

            // draw gear teeth
            context.beginPath();
            // we can set the lineJoin property to bevel so that the tips
            // of the gear teeth are flat and don't come to a sharp point
            context.lineJoin = "bevel";

            // loop through the number of points to create the gear shape
            var numPoints = this.numTeeth * 2;
            for (var n = 0; n < numPoints; n++) {
                var radius = null;

                // draw tip of teeth on even iterations
                if (n % 2 == 0) {
                    radius = this.outerRadius;
                }
                // draw teeth connection which lies somewhere between
                // the gear center and gear radius
                else {
                    radius = this.innerRadius;
                }

                var theta = ((Math.PI * 2) / numPoints) * (n + 1);
                var x = (radius * Math.sin(theta));
                var y = (radius * Math.cos(theta));

                // if first iteration, use moveTo() to position
                // the drawing cursor
                if (n == 0) {
                    context.moveTo(x, y);
                }
                // if any other iteration, use lineTo() to connect sub paths
                else {
                    context.lineTo(x, y);
                }
            }

            context.closePath();

            // define the line width and stroke color
            context.lineWidth = 5;
            context.strokeStyle = this.darkColor;
            context.stroke();

            // draw gear body
            context.beginPath();
            context.arc(0, 0, this.midRadius, 0, 2 * Math.PI, false);

            // create a linear gradient
            var grd = context.createLinearGradient(-1 * this.outerRadius / 2, -1 * this.outerRadius / 2, this.outerRadius / 2, this.outerRadius / 2);
            grd.addColorStop(0, this.lightColor); 
            grd.addColorStop(1, this.darkColor); 
            context.fillStyle = grd;
            context.fill();
            context.lineWidth = 5;
            context.strokeStyle = this.darkColor;
            context.stroke();

            // draw gear hole
            context.beginPath();
            context.arc(0, 0, this.holeRadius, 0, 2 * Math.PI, false);
            context.fillStyle = "white";
            context.fill();
            context.strokeStyle = this.darkColor;
            context.stroke();
            context.restore();
        };
  1. 实例化一个Animation对象并获取画布上下文:
        window.onload = function(){
            var anim = new Animation("myCanvas");
            var canvas = anim.getCanvas();
            var context = anim.getContext();
  1. 构建一个gear对象的数组:
            var gears = [];

            // add blue gear
            gears.push(new Gear({
                x: 270,
                y: 105,
                outerRadius: 90,
                innerRadius: 50,
                holeRadius: 10,
                numTeeth: 24,
                theta: 0,
                thetaSpeed: 1 / 1000,
                lightColor: "#B1CCFF",
                darkColor: "#3959CC",
                clockwise: false
            }));

            // add red gear
            gears.push(new Gear({
                x: 372,
                y: 190,
                outerRadius: 50,
                innerRadius: 15,
                holeRadius: 10,
                numTeeth: 12,
                theta: 0.14,
                thetaSpeed: 2 / 1000,
                lightColor: "#FF9E9D",
                darkColor: "#AD0825",
                clockwise: true
            }));

            // add orange gear
            gears.push(new Gear({
                x: 422,
                y: 142,
                outerRadius: 28,
                innerRadius: 5,
                holeRadius: 7,
                numTeeth: 6,
                theta: 0.35,
                thetaSpeed: 4 / 1000,
                lightColor: "#FFDD87",
                darkColor: "#D25D00",
                clockwise: false
            }));
  1. 设置stage()函数,更新每个齿轮的旋转,清除画布,然后绘制齿轮:
            anim.setStage(function(){
                // update
                for (var i = 0; i < gears.length; i++) {
                    var gear = gears[i];
                    var thetaIncrement = gear.thetaSpeed * this.getTimeInterval();
                    gear.theta += gear.clockwise ? thetaIncrement : -1 * thetaIncrement;
                }

                // clear
                this.clear();

                // draw
                for (var i = 0; i < gears.length; i++) {
                    gears[i].draw(context);
                }
            });
  1. 开始动画:
            anim.start();
        };
    </script>
</head>
  1. 将画布嵌入 HTML 文档的主体中:
<body>
    <canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
    </canvas>
</body>

工作原理...

要创建一个旋转齿轮系统,我们可以重用第二章中的齿轮绘制过程,并创建一个Gear类,该类具有一些额外的属性,如齿数、颜色、θ和θ速度。θ定义了齿轮的角位置,θSpeed定义了齿轮的角速度。我们还可以在Gear类中添加一个clockwise属性,该属性定义了齿轮旋转的方向。

页面加载后,我们可以实例化一个新的Animation对象并获取画布和上下文。接下来,我们可以通过实例化Gear对象并将其推入齿轮数组来初始化一些齿轮。现在我们的舞台已经初始化,我们可以设置stage()函数,该函数将更新每个齿轮的角度,清除画布,然后使用Gear类的draw()方法绘制每个齿轮。

现在stage()函数已经设置好了,我们可以使用start()方法开始动画。

另请参阅...

  • 绘制一个圆在第二章中

  • 使用循环创建图案:绘制齿轮在第二章中

时钟动画

对于那些在开发酷炫项目时陷入恍惚状态,时间似乎消失的人,这个是给你的。在这个示例中,我们将创建一个漂亮的动画时钟,以提醒我们网络空间之外的真实世界时间。

时钟动画

如何做...

按照以下步骤在时钟上动画时针、分针和秒针:

  1. 链接到Animation类:
<head>
    <script src="img/animation.js">
    </script>
  1. 实例化一个Animation对象,获取画布上下文,并定义时钟半径:
    <script>
        window.onload = function(){
            var anim = new Animation("myCanvas");
            var canvas = anim.getCanvas();
            var context = anim.getContext();
            var clockRadius = 75;
  1. 设置stage()函数,该函数获取当前时间,计算时针、分针和秒针的角度,清除画布,然后绘制时钟:
            anim.setStage(function(){

                // update
                var date = new Date();
                var hours = date.getHours();
                var minutes = date.getMinutes();
                var seconds = date.getSeconds();

                hours = hours > 12 ? hours - 12 : hours;

                var hour = hours + minutes / 60;
                var minute = minutes + seconds / 60;

        // clear
        this.clear();

        // draw
                var context = anim.getContext();
                context.save();
                context.translate(canvas.width / 2, canvas.height / 2);

                // draw clock body
                context.beginPath();
                context.arc(0, 0, clockRadius, 0, Math.PI * 2, true);

                var grd = context.createLinearGradient(-clockRadius, -clockRadius, clockRadius, clockRadius);
                grd.addColorStop(0, "#F8FCFF"); // light blue
                grd.addColorStop(1, "#A1CCEE"); // dark blue
                context.fillStyle = grd;
                context.fill();

                // draw numbers  
                context.font = "16pt Calibri";
                context.fillStyle = "#024F8C";
                context.textAlign = "center";
                context.textBaseline = "middle";
                for (var n = 1; n <= 12; n++) {
                    var theta = (n - 3) * (Math.PI * 2) / 12;
                    var x = clockRadius * 0.8 * Math.cos(theta);
                    var y = clockRadius * 0.8 * Math.sin(theta);
                    context.fillText(n, x, y);
                }

                context.save();

                // apply drop shadow
                context.shadowColor = "#bbbbbb";
                context.shadowBlur = 5;
                context.shadowOffsetX = 1;
                context.shadowOffsetY = 1;

                // draw clock rim
                context.lineWidth = 3;
                context.strokeStyle = "#005EA8";
                context.stroke();

                context.restore();

                // draw hour hand
                context.save();
                var theta = (hour - 3) * 2 * Math.PI / 12;
                context.rotate(theta);
                context.beginPath();
                context.moveTo(-10, -4);
                context.lineTo(-10, 4);
                context.lineTo(clockRadius * 0.6, 1);
                context.lineTo(clockRadius * 0.6, -1);
                context.fill();
                context.restore();

                // minute hand
                context.save();
                var theta = (minute - 15) * 2 * Math.PI / 60;
                context.rotate(theta);
                context.beginPath();
                context.moveTo(-10, -3);
                context.lineTo(-10, 3);
                context.lineTo(clockRadius * 0.9, 1);
                context.lineTo(clockRadius * 0.9, -1);
                context.fill();
                context.restore();

                // second hand
                context.save();
                var theta = (seconds - 15) * 2 * Math.PI / 60;
                context.rotate(theta);
                context.beginPath();
                context.moveTo(-10, -2);
                context.lineTo(-10, 2);
                context.lineTo(clockRadius * 0.8, 1);
                context.lineTo(clockRadius * 0.8, -1);
                context.fillStyle = "red";
                context.fill();
                context.restore();

                context.restore();
            });
  1. 开始动画:
            anim.start();
        };
    </script>
</head>
  1. 将画布嵌入到 HTML 文档的主体中:
<body>
    <canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
    </canvas>
</body>

工作原理...

页面加载时,我们可以实例化一个新的Animation对象,然后获取画布和上下文。接下来,我们可以开始定义stage()函数,该函数负责更新时钟、清除画布,然后为每个动画循环绘制时钟。

在代码的更新部分,我们可以实例化一个新的Date()对象,然后获取小时、分钟和秒。接下来,我们可以调整小时和分钟,以表示 12 小时制时间(上午和下午)。

清除画布后,我们可以开始绘制时钟:

  • 使用translate()方法将画布上下文转换到画布的中心

  • 使用arc()方法绘制主体

  • 创建一个循环,使用fillText()方法在边缘绘制时钟的数字

  • 使用shadowOffsetXshadowOffsetY属性应用阴影

  • 通过stroke()方法描绘时钟边缘

  • 通过旋转画布上下文并绘制一个最厚的梯形来绘制每个时钟指针,其最厚的一端位于中心。

最后,一旦stage()函数设置好了,我们就可以使用start()方法开始动画。

另请参阅...

  • 使用文本在第一章

  • 绘制一个圆在第二章中

  • 使用自定义形状和填充样式在第二章中

模拟粒子物理学

现在我们已经介绍了古典物理学的基础知识,让我们把它们整合起来。在这个示例中,我们将通过模拟重力、边界条件、碰撞阻尼和地板摩擦来模拟粒子物理学。

模拟粒子物理学

如何做...

按照以下步骤在画布内启动一个粒子,并观察它在墙上弹跳、逐渐因重力落到地板上,然后因地板摩擦而减速停止的弹道:

  1. 链接到Animation类:
<head>
    <script src="img/animation.js">
    </script>
  1. 定义applyPhysics()函数,它以粒子作为输入,并根据重力、碰撞阻尼和地板摩擦等物理变量更新其位置和速度:
        function applyPhysics(anim, particle){
            // physics globals
            var gravity = 1500; // pixels / second²
            var collisionDamper = 0.8; // 80% velocity lost when collision occurs
            var floorFriction = 100; // pixels / second²
            var timeInterval = anim.getTimeInterval();
            var canvas = anim.getCanvas();

            // gravity
            particle.vy += gravity * timeInterval / 1000;

            // position
            particle.y += particle.vy * timeInterval / 1000;
            particle.x += particle.vx * timeInterval / 1000;

            // floor condition
            if (particle.y > (canvas.height - particle.radius)) {
                particle.y = canvas.height - particle.radius;
                particle.vy *= -1;
                particle.vy *= collisionDamper;
            }

            // floor friction
            if (particle.y == canvas.height - particle.radius) {
                if (particle.vx > 0.1) {
                    particle.vx -= floorFriction * timeInterval / 1000;
                }
                else if (particle.vx < -0.1) {
                    particle.vx += floorFriction * timeInterval / 1000;
                }
                else {
                    particle.vx = 0;
                }
            }

            // ceiling  condition
            if (particle.y < (particle.radius)) {
                particle.y = particle.radius;
                particle.vy *= -1;
                particle.vy *= collisionDamper;
            }

            // right wall condition
            if (particle.x > (canvas.width - particle.radius)) {
                particle.x = canvas.width - particle.radius;
                particle.vx *= -1;
                particle.vx *= collisionDamper;
            }

            // left wall condition
            if (particle.x < (particle.radius)) {
                particle.x = particle.radius;
                particle.vx *= -1;
                particle.vx *= collisionDamper;
            }
        }
  1. 实例化一个新的Animation对象并获取画布上下文:
        window.onload = function(){
            var anim = new Animation("myCanvas");
            var canvas = anim.getCanvas();
            var context = anim.getContext();
  1. 使用位置、x 和 y 速度以及半径初始化一个particle对象:
            var particle = {
                x: 10,
                y: canvas.height - 10,
                vx: 600, // px / second
                vy: -900, // px / second
                radius: 10
            };
  1. 设置stage()函数,通过将其传递给applyPhysics()函数来更新粒子,清除画布,然后绘制粒子:
            anim.setStage(function(){
                // update
                applyPhysics(this, particle);

                // clear
                this.clear();

                // draw 
                context.beginPath();
                context.arc(particle.x, particle.y, particle.radius, 0, 2 * Math.PI, false);
                context.fillStyle = "blue";
                context.fill();
            });
  1. 开始动画:
            anim.start();
        };
    </script>
</head>
  1. 在 HTML 文档的 body 内嵌入画布标签:
<body>
    <canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
    </canvas>
</body>

工作原理...

模拟粒子物理学,我们需要处理每一帧粒子的 x 和 y 位置以及粒子在 x 和 y 方向的速度。理解粒子物理模拟的关键是要记住,粒子在系统中的运动是基于作用在粒子上的所有力的总和。在我们的情况下,重力将使粒子向下移动,与墙壁、天花板和地板的碰撞将根据碰撞阻尼常数减少粒子的速度,地板摩擦将在粒子在地板上滚动时减少其水平速度。

首先,当页面加载时,我们可以实例化一个新的Animation对象,然后获取画布和上下文。接下来,我们可以初始化一个具有位置、初始速度和大小的粒子。现在我们已经在舞台上初始化了演员(粒子),我们可以设置stage()函数,该函数将更新粒子,清除画布,然后为每个动画帧绘制粒子。

更新逻辑发生在applyPhysics()函数内,该函数接收对Animation对象的引用,以及particle对象。applyPhysics()函数遍历一系列条件,更新粒子的位置和速度。

在调用applyPhysics()函数并更新粒子后,我们可以清除画布,然后通过绘制一个简单的圆来绘制粒子,其半径等于粒子的半径。

最后,一旦stage()函数被设置,我们可以使用start()方法开始动画。

还有更多...

如果你真的想要变得花哨,甚至可以添加额外的力,比如空气阻力。作为一个经验法则,你添加到粒子模拟中的力越多,它就越像真实的生命。你可以尝试不同的初始位置和速度,看看不同的抛射路径。

另请参阅...

  • 在第二章中绘制一个圆形

创建微观生命形式

你是否曾在显微镜中看到微生物,并观察它们如何摇摆?这个配方受到微生物的外星世界的启发。在这个配方中,我们将创建 100 个随机微生物,并让它们在画布上自由移动。

创建微观生命形式

操作步骤...

按照以下步骤在画布内创建摇摆的微生物:

  1. 链接到Animation类:
<head>
    <script src="img/animation.js">
    </script>
  1. 定义getRandColor()函数,返回一个随机颜色:
    <script>
        function getRandColor(){
            var colors = ["red", "orange", "yellow", "green", "blue", "violet"];
            return colors[Math.floor(Math.random() * colors.length)];
        }
  1. 定义getRandTheta()函数,返回一个随机角度:
        function getRandTheta(){
            return Math.random() * 2 * Math.PI;
        }
  1. 定义updateMicrobes()函数,通过为每个微生物添加一个新的头部段并生成随机角度,然后移除尾部段来更新microbe对象:
        function updateMicrobes(anim, microbes){
            var canvas = anim.getCanvas();
            var angleVariance = 0.2;

            for (var i = 0; i < microbes.length; i++) {
                var microbe = microbes[i];
                var angles = microbe.angles;

        /*
         * good numNewSegmentsPerFrame values:
         * 60fps -> 1
         * 10fps -> 10 
         * 
         * for a linear relationship, we can use the equation:
         * n = mf + b, where n = numNewSegmentsPerFrame and f = FPS
         * solving for m and b, we have:
         * n = (-0.18)f + 11.8
         */
                var numNewSegmentsPerFrame = Math.round(-0.18 * anim.getFps() + 11.8);

                for (var n = 0; n < numNewSegmentsPerFrame; n++) {
                    // create first angle if no angles
                    if (angles.length == 0) {
                        microbe.headX = canvas.width / 2;
                        microbe.headY = canvas.height / 2;
                        angles.push(getRandTheta());
                    }

                    var headX = microbe.headX;
                    var headY = microbe.headY;
                    var headAngle = angles[angles.length - 1];

                    // create new head angle
                    var dist = anim.getTimeInterval() / (10 * numNewSegmentsPerFrame);
                    // increase new head angle by an amount equal to
                    // -0.1 to 0.1
                    var newHeadAngle = headAngle + ((angleVariance / 2) - Math.random() * angleVariance);
                    var newHeadX = headX + dist * Math.cos(newHeadAngle);
                    var newHeadY = headY + dist * Math.sin(newHeadAngle);

                    // change direction if collision occurs
                    if (newHeadX >= canvas.width || newHeadX <= 0 || newHeadY >= canvas.height || newHeadY <= 0) {
                        newHeadAngle += Math.PI / 2;
                        newHeadX = headX + dist * Math.cos(newHeadAngle);
                        newHeadY = headY + dist * Math.sin(newHeadAngle);
                    }

                    microbe.headX = newHeadX;
                    microbe.headY = newHeadY;
                    angles.push(newHeadAngle);

                    // remove tail angle
                    if (angles.length > 20) {
                        angles.shift();
                    }
                }
            }
        }
  1. 定义drawMicrobes()函数来绘制所有的微生物:
        function drawMicrobes(anim, microbes){
            var segmentLength = 2; // px
            var context = anim.getContext();

            for (var i = 0; i < microbes.length; i++) {
                var microbe = microbes[i];

                var angles = microbe.angles;
                context.beginPath();
                context.moveTo(microbe.headX, microbe.headY);

                var x = microbe.headX;
                var y = microbe.headY;

                // start with the head and end with the tail
                for (var n = angles.length - 1; n >= 0; n--) {
                    var angle = angles[n];

                    x -= segmentLength * Math.cos(angle);
                    y -= segmentLength * Math.sin(angle);
                    context.lineTo(x, y);
                }

                context.lineWidth = 10;
                context.lineCap = "round";
                context.lineJoin = "round";
                context.strokeStyle = microbe.color;
                context.stroke();
            }
        }
  1. 实例化一个Animation对象并获取画布上下文:
        window.onload = function(){
            var anim = new Animation("myCanvas");
            var canvas = anim.getCanvas();
            var context = anim.getContext();
  1. 初始化 100 个微生物:
            // init microbes
            var microbes = [];
            for (var n = 0; n < 100; n++) {
                // each microbe will be an array of angles
                microbes[n] = {
                    headX: 0,
                    headY: 0,
                    angles: [],
                    color: getRandColor()
                };
            }
  1. 设置stage()函数,通过调用updateMicrobes()函数来更新微生物,清除画布,然后通过调用drawMicrobes()函数来绘制微生物:
            anim.setStage(function(){
                // update
                updateMicrobes(this, microbes);

                // clear
                this.clear();

                // draw
                drawMicrobes(this, microbes);
            });
  1. 开始动画:
            anim.start();
        };
    </script>
</head>
  1. 在 HTML 文档的 body 内嵌入画布:
<body>
    <canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
    </canvas>
</body>

工作原理...

要创建一个微生物,我们可以绘制一系列连接的段,以创建一个类似蛇的短生物。我们可以将微生物表示为一个包含头部位置和角度数组的对象。这些角度表示段之间的角度。

这个示例初始化了 100 个随机化的微生物,并将它们放在画布的中心。我们的stage()函数包含updateMicrobes()drawMicrobes()函数。

updateMicrobes()函数循环遍历所有微生物对象,为每个微生物添加一个新的头部段,并删除每个微生物的尾部段。这样,每个微生物的段在移动时会摆动。当微生物的头部碰到画布的边缘时,它的角度将增加 90 度,以便它反弹回画布区域。

drawMicrobes()函数循环遍历所有microbe对象,将绘图光标定位在每个微生物的头部,然后根据每个段的角度绘制 20 条线段。

另请参阅...

  • 在第一章中绘制螺旋

  • 在第六章中创建一个绘图应用程序

强调画布并显示 FPS

在看到上一个示例之后,你可能会想“我们可以动画化多少微生物?”这个问题的直接答案是肯定的。由于 HTML5 画布的 2D 上下文不是硬件加速的,而且我们的动画纯粹由 JavaScript 驱动,所以肯定有一个点,当浏览器加班工作时,它会开始变得吃力。为了说明这一点,我们可以绘制我们动画的 FPS,并观察屏幕上微生物数量与 FPS 值之间的关系。

强调画布并显示 FPS

如何做...

按照以下步骤来强调画布并显示 FPS:

  1. 链接到Animation类:
<head>
    <script src="img/animation.js">
    </script>
  1. 定义drawFps()函数,在画布的右上角绘制 FPS 值:
        function drawFps(anim, fps){
            var canvas = anim.getCanvas();
            var context = anim.getContext();

            context.fillStyle = "black";
            context.fillRect(canvas.width - 100, 0, 100, 30);

            context.font = "18pt Calibri";
            context.fillStyle = "white";
            context.fillText("fps: " + fps.toFixed(1), canvas.width - 93, 22);
        }
  1. 定义getRandColor()函数,返回一个随机颜色:
    <script>
        function getRandColor(){
            var colors = ["red", "orange", "yellow", "green", "blue", "violet"];
            return colors[Math.floor(Math.random() * colors.length)];
        }
  1. 定义getRandTheta()函数,返回一个随机的θ:
        function getRandTheta(){
            return Math.random() * 2 * Math.PI;
        }
  1. 定义updateMicrobes()函数,通过为每个微生物添加一个具有随机生成角度的新头部段来更新microbe对象,然后删除尾部段:
        function updateMicrobes(anim, microbes){
            var canvas = anim.getCanvas();
            var angleVariance = 0.2;

            for (var i = 0; i < microbes.length; i++) {
                var microbe = microbes[i];
                var angles = microbe.angles;

                /*
              * good numNewSegmentsPerFrame values:
              * 60fps -> 1
              * 10fps -> 10 
              * 
              * for a linear relationship, we can use the equation:
              * n = mf + b, where n = numNewSegmentsPerFrame and f = FPS
              * solving for m and b, we have:
              * n = (-0.18)f + 11.8
              */

                var numNewSegmentsPerFrame = Math.round(-0.18 * anim.getFps() + 11.8);

                for (var n = 0; n < numNewSegmentsPerFrame; n++) {
                    // create first angle if no angles
                    if (angles.length == 0) {
                        microbe.headX = canvas.width / 2;
                        microbe.headY = canvas.height / 2;
                        angles.push(getRandTheta());
                    }

                    var headX = microbe.headX;
                    var headY = microbe.headY;
                    var headAngle = angles[angles.length - 1];

                    // create new head angle
                    var dist = anim.getTimeInterval() / (10 * numNewSegmentsPerFrame);
                    // increase new head angle by an amount equal to
                    // -0.1 to 0.1
                    var newHeadAngle = headAngle + ((angleVariance / 2) - Math.random() * angleVariance);
                    var newHeadX = headX + dist * Math.cos(newHeadAngle);
                    var newHeadY = headY + dist * Math.sin(newHeadAngle);

                    // change direction if collision occurs
                    if (newHeadX >= canvas.width || newHeadX <= 0 || newHeadY >= canvas.height || newHeadY <= 0) {
                        newHeadAngle += Math.PI / 2;
                        newHeadX = headX + dist * Math.cos(newHeadAngle);
                        newHeadY = headY + dist * Math.sin(newHeadAngle);
                    }

                    microbe.headX = newHeadX;
                    microbe.headY = newHeadY;
                    angles.push(newHeadAngle);

                    // remove tail angle
                    if (angles.length > 20) {
                        angles.shift();
                    }
                }
            }
        }
  1. 定义drawMicrobes()函数,绘制所有的微生物:
        function drawMicrobes(anim, microbes){
            var segmentLength = 2; // px
            var context = anim.getContext();

            for (var i = 0; i < microbes.length; i++) {
                var microbe = microbes[i];

                var angles = microbe.angles;
                context.beginPath();
                context.moveTo(microbe.headX, microbe.headY);

                var x = microbe.headX;
                var y = microbe.headY;

                // start with the head and end with the tail
                for (var n = angles.length - 1; n >= 0; n--) {
                    var angle = angles[n];

                    x -= segmentLength * Math.cos(angle);
                    y -= segmentLength * Math.sin(angle);
                    context.lineTo(x, y);
                }

                context.lineWidth = 10;
                context.lineCap = "round";
                context.lineJoin = "round";
                context.strokeStyle = microbe.color;
                context.stroke();
            }
        }
  1. 实例化一个Animation对象并获取画布上下文:
        window.onload = function(){
            var anim = new Animation("myCanvas");
            var canvas = anim.getCanvas();
            var context = anim.getContext();
  1. 初始化 1,500 个微生物:
            // init microbes
            var microbes = [];
            for (var n = 0; n < 1500; n++) {
                // each microbe will be an array of angles
                microbes[n] = {
                    headX: 0,
                    headY: 0,
                    angles: [],
                    color: getRandColor()
                };
            }
  1. 设置stage()函数,该函数更新微生物,每 10 帧更新一次 FPS 值,清除画布,然后绘制微生物和 FPS 值:
            var fps = 0;

            anim.setStage(function(){
                // update
                updateMicrobes(this, microbes);

                if (anim.getFrame() % 10 == 0) {
                    fps = anim.getFps();
                }

                // clear
                this.clear();

                // draw
                drawMicrobes(this, microbes);
                drawFps(this, fps);
            });
  1. 开始动画:
            anim.start();
        };
    </script>
</head>
  1. 将画布嵌入到 HTML 文档的主体中:
<body>
    <canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
    </canvas>
</body>

它是如何工作的...

为了绘制动画的 FPS,我们可以创建drawFps()函数,该函数以 FPS 值作为输入,绘制画布右上角的黑色框,然后写出 FPS 值。为了避免过于频繁地更新 FPS,我们可以将 FPS 值的副本存储在变量FPS中,并在每 10 帧更新一次。这样,FPS 最多每秒更新 6 次。

为了强调画布,我们可以简单地初始化更多的微生物。在这个示例中,我们初始化了 1,500 个微生物。如果你自己尝试这段代码,你可以尝试不同的数字,看看 FPS 如何受到影响。

还有更多...

如前所述,典型的动画应该以大约 40 到 60 FPS 运行。如果 FPS 低于 30,你会开始注意到动画有轻微的延迟。在 32 位 Windows 7 机器上使用 Google Chrome 进行测试,配备 2.2 GHz AMD 处理器和 2 GB RAM(是的,我知道,我需要升级),当我在动画 1,500 个微生物时,我看到大约 5 FPS。看起来不错,但也不是很好。当动画 2,000 个或更多的微生物时,动画开始看起来不可接受地卡顿。

我们使用 2D 上下文创建的几乎所有动画在台式机和笔记本电脑上表现良好。然而,如果您发现自己处于一个情况,您的动画在 2D 上下文中的计算开销足够大,以至于表现不佳,您可能会考虑改用 WebGL(我们将在第九章中介绍 WebGL,WebGL 简介)。与 2D 上下文不同,WebGL 利用硬件加速。在撰写本文时,所有主要浏览器中的 2D 上下文都不利用硬件加速。然而,使用 WebGL 确实会带来成本,因为开发和维护 WebGL 动画要比创建 2D 上下文动画困难得多。

另请参阅...

  • 在第一章中处理文本

  • 在第一章中绘制螺旋线

  • 在第六章中创建绘图应用程序

第六章:与画布交互:将事件监听器附加到形状和区域

在本章中,我们将涵盖:

  • 创建一个 Events 类

  • 使用画布鼠标坐标

  • 将鼠标事件监听器附加到区域

  • 将触摸事件监听器附加到移动设备上的区域

  • 将事件监听器附加到图像

  • 拖放形状

  • 拖放图像

  • 创建一个图像放大器

  • 创建一个绘图应用程序

介绍

到目前为止,我们已经学会了如何在画布上绘制,处理图像和视频,并创建流畅的动画。本章重点是画布的交互性。到目前为止,我们所有的画布项目都非常不响应和与用户脱节。尽管 HTML5 画布 API 没有提供一种方法来将事件监听器附加到形状和区域,但我们可以通过扩展 API 来实现这种功能。根据 HTML5 规范,一旦形状被绘制,我们就无法像在 HTML 文档中对待 DOM 元素那样访问它。直到 HTML5 画布规范包括将事件监听器附加到形状和区域的方法(希望有一天会有),我们需要构建自己的 Events 类来实现这一点。我们的类将使我们能够将事件监听器附加到包装一个或多个形状的区域,类似于将事件监听器附加到 DOM 元素。

这是一个非常强大的概念,因为它使我们能够在画布上绘制用户可以交互的形状。我们的 Events 类将支持 mousedown, mouseup, mouseover, mouseout, mousemove, touchstart, touchend, 和 touchmove 事件。

提示

尽管本章中的大多数示例都使用鼠标事件,但也可以通过用touchstart替换mousedown,用touchend替换mouseup,用touchmove替换mousemove来修改以支持移动触摸事件。

让我们开始吧!

创建一个 Events 类

类似于第五章,“通过动画使画布生动起来”,在那一章中,我们创建了一个自定义类来处理动画,本章中我们将创建一个自定义类来处理画布事件。

由于画布形状不可作为对象访问(遗憾!),我们无法像对待 div 元素那样附加事件监听器:

document.getElementById("foo").addEventListener("mouseup", function() {
  // do stuff
}, false);

那么我们能做什么呢?如果我们遵循画布 API 的模式,在其中形状的开始由beginPath()定义,形状的结束由closePath()定义,我们可以通过引入封装多个形状的区域的概念进一步扩展这个想法。此外,如果我们能够以类似的方式向区域添加事件监听器,就像我们向 DOM 元素添加事件监听器一样,那将非常好:

this.addRegionEventListener("mouseup", function() {
 // do stuff
});

Events 类的目标就是通过扩展画布 API 来支持画布事件,引入可以附加桌面事件监听器(如mousedownmouseupmouseovermouseoutmousemove)以及移动事件监听器(如touchstarttouchendtouchmove)的区域。

提示

与其手动输入 Events 类,不如考虑从本书的在线资源中下载该类www.html5canvastutorials.com/cookbook

如何做...

按照以下步骤创建一个 Events 类,它将使我们能够将事件监听器附加到画布上的形状和区域:

  1. 定义Events构造函数:
var Events = function(canvasId){
    this.canvas = document.getElementById(canvasId);
    this.context = this.canvas.getContext("2d");
    this.stage = undefined;
    this.listening = false;

    // desktop flags
    this.mousePos = null;
    this.mouseDown = false;
    this.mouseUp = false;
    this.mouseOver = false;
    this.mouseMove = false;

    // mobile flags
    this.touchPos = null;
    this.touchStart = false;
    this.touchMove = false;
    this.touchEnd = false;

    // Region Events
    this.currentRegion = null;
    this.regionIndex = 0;
    this.lastRegionIndex = -1;
    this.mouseOverRegionIndex = -1;
};
  1. 定义getContext()方法,返回画布上下文:
Events.prototype.getContext = function(){
    return this.context;
};
  1. 定义getCanvas()方法,返回画布 DOM 元素:
Events.prototype.getCanvas = function(){
    return this.canvas;
};
  1. 定义clear()方法,清除画布:
Events.prototype.clear = function(){
    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
};
  1. 定义getCanvasPos()方法,返回画布位置:
Events.prototype.getCanvasPos = function(){
    var obj = this.getCanvas();
    var top = 0;
    var left = 0;
    while (obj.tagName != "BODY") {
        top += obj.offsetTop;
        left += obj.offsetLeft;
        obj = obj.offsetParent;
    }
    return {
        top: top,
        left: left
    };
};
  1. 定义setStage()方法,设置stage()函数:
Events.prototype.setStage = function(func){
    this.stage = func;
    this.listen();
};
  1. 定义reset()方法,用于设置鼠标位置和触摸位置,重置区域索引,调用stage()函数,然后重置事件标志:
Events.prototype.reset = function(evt){
    if (!evt) {
        evt = window.event;
    }

    this.setMousePosition(evt);
    this.setTouchPosition(evt);
    this.regionIndex = 0;

    if (this.stage !== undefined) {
        this.stage();
    }

    // desktop flags
    this.mouseOver = false;
    this.mouseMove = false;
    this.mouseDown = false;
    this.mouseUp = false;

    // mobile touch flags
    this.touchStart = false;
    this.touchMove = false;
    this.touchEnd = false;
};
  1. 定义listen()方法,向画布元素添加事件监听器:
Events.prototype.listen = function(){
    var that = this;

    if (this.stage !== undefined) {
        this.stage();
    }

    // desktop events
    this.canvas.addEventListener("mousedown", function(evt){
        that.mouseDown = true;
        that.reset(evt);
    }, false);

    this.canvas.addEventListener("mousemove", function(evt){
        that.reset(evt);
    }, false);

    this.canvas.addEventListener("mouseup", function(evt){
        that.mouseUp = true;
        that.reset(evt);
    }, false);

    this.canvas.addEventListener("mouseover", function(evt){
        that.reset(evt);
    }, false);

    this.canvas.addEventListener("mouseout", function(evt){
        that.mousePos = null;
    }, false);

    // mobile events
    this.canvas.addEventListener("touchstart", function(evt){
        evt.preventDefault();
        that.touchStart = true;
        that.reset(evt);
    }, false);

    this.canvas.addEventListener("touchmove", function(evt){
        evt.preventDefault();
        that.reset(evt);
    }, false);

    this.canvas.addEventListener("touchend", function(evt){
        evt.preventDefault();
        that.touchEnd = true;
        that.reset(evt);
    }, false);
};
  1. 定义getMousePos()方法,用于桌面应用程序返回鼠标位置:
Events.prototype.getMousePos = function(evt){
    return this.mousePos;
};
  1. 定义getTouchPos()方法,用于移动应用程序返回触摸位置:
Events.prototype.getTouchPos = function(evt){
    return this.touchPos;
};
  1. 定义setMousePos()方法,用于设置鼠标位置:
Events.prototype.setMousePosition = function(evt){
    var mouseX = evt.clientX - this.getCanvasPos().left + window.pageXOffset;
    var mouseY = evt.clientY - this.getCanvasPos().top + window.pageYOffset;
    this.mousePos = {
        x: mouseX,
        y: mouseY
    };
};
  1. 定义setTouchPos()方法,用于设置触摸位置:
Events.prototype.setTouchPosition = function(evt){
    if (evt.touches !== undefined && evt.touches.length == 1) { // Only deal with one finger
        var touch = evt.touches[0]; // Get the information for finger #1
        var touchX = touch.pageX - this.getCanvasPos().left + window.pageXOffset;
        var touchY = touch.pageY - this.getCanvasPos().top + window.pageYOffset;

        this.touchPos = {
            x: touchX,
            y: touchY
        };
    }
};
  1. 定义beginRegion()方法,用于定义一个新区域:
Events.prototype.beginRegion = function(){
    this.currentRegion = {};
    this.regionIndex++;
};
  1. 定义addRegionEventListener()方法,用于向区域添加事件监听器:
Events.prototype.addRegionEventListener = function(type, func){
    var event = (type.indexOf('touch') == -1) ? 'on' + type : type;
    this.currentRegion[event] = func;
};
  1. 定义closeRegion()方法,用于关闭一个区域并确定是否发生了与当前区域相关的事件:
Events.prototype.closeRegion = function(){
    var pos = this.touchPos || this.mousePos;

    if (pos !== null && this.context.isPointInPath(pos.x, pos.y)) {
        if (this.lastRegionIndex != this.regionIndex) {
            this.lastRegionIndex = this.regionIndex;
        }

        // handle onmousedown
        if (this.mouseDown && this.currentRegion.onmousedown !== undefined) {
            this.currentRegion.onmousedown();
            this.mouseDown = false;
        }

        // handle onmouseup
        else if (this.mouseUp && this.currentRegion.onmouseup !== undefined) {
            this.currentRegion.onmouseup();
            this.mouseUp = false;
        }

        // handle onmouseover
        else if (!this.mouseOver && this.regionIndex != this.mouseOverRegionIndex && this.currentRegion.onmouseover !== undefined) {
            this.currentRegion.onmouseover();
            this.mouseOver = true;
            this.mouseOverRegionIndex = this.regionIndex;
        }

        // handle onmousemove
        else if (!this.mouseMove && this.currentRegion.onmousemove !== undefined) {
            this.currentRegion.onmousemove();
            this.mouseMove = true;
        }

        // handle touchstart
        if (this.touchStart && this.currentRegion.touchstart !== undefined) {
            this.currentRegion.touchstart();
            this.touchStart = false;
        }

        // handle touchend
        if (this.touchEnd && this.currentRegion.touchend !== undefined) {
            this.currentRegion.touchend();
            this.touchEnd = false;
        }

        // handle touchmove
        if (!this.touchMove && this.currentRegion.touchmove !== undefined) {
            this.currentRegion.touchmove();
            this.touchMove = true;
        }

    }
    else if (this.regionIndex == this.lastRegionIndex) {
        this.lastRegionIndex = -1;
        this.mouseOverRegionIndex = -1;

        // handle mouseout condition
        if (this.currentRegion.onmouseout !== undefined) {
            this.currentRegion.onmouseout();
        }
    }
};

工作原理...

尽管 HTML5 画布 API 没有提供一种方便处理事件监听器的方法,但它提供了一个关键方法,使这成为可能:

context.isPointInPath(x,y);

isPointInPath()方法如果给定的坐标在画布上绘制的任何路径内,则返回 true。由于画布是位图,这里没有图层和形状的概念,因此我们必须想办法利用isPointInPath()方法来确定特定区域的坐标,特别是鼠标坐标,是否在画布上。一旦我们能够检测鼠标光标是否在特定区域上方,我们可以添加额外的逻辑来处理mouseovermousemovemouseoutmousedownmouseuptouchstarttouchendtouchmove事件。

在深入讨论之前,让我们举个例子,制定一个模拟区域事件的流程,然后利用所学知识来制定我们需要创建Events类的方法。假设我们想在画布上绘制一个三角形、一个矩形和一个圆,然后当用户将鼠标放在圆上时,我们想要弹出一些文本。我们可以先绘制三角形,然后使用isPointInPath()来检查鼠标坐标是否在当前路径内。如果该方法返回 false,我们就知道鼠标光标在三角形外面。接下来,我们可以绘制矩形,再次检查鼠标坐标是否在任何路径内,这时包括了三角形和矩形。如果isPointInPath()仍然返回 false,我们现在知道鼠标光标在三角形和矩形外面。最后,我们可以绘制圆,再次检查鼠标坐标是否在画布上的任何路径内,这时包括了三角形、矩形和圆。如果该方法返回 true,则鼠标确实在圆上。如果返回 false,则鼠标光标在三角形、矩形和圆外面。

当然,这仅在我们假设在元素实际绘制之前,光标已经位于画布的某个位置时才有效。在光标移动后,我们能够检测鼠标光标是否在元素上方的唯一方法是在每次触发事件时重新绘制我们的元素,然后检查鼠标坐标是否在绘制每个元素后存在的形状内。我们可以通过使用Events类的setStage()方法来定义stage()函数来实现这一点。

接下来,我们需要一种方法来定义区域的开始和结束。我们可以创建一个beginRegion()方法来定义一个新的Region对象。Region对象可以有八个属性:mouseovermouseoutmousemovemousedownmouseuptouchstarttouchendtouchmove,所有这些都是用户定义的函数。接下来,我们可以创建一个名为addRegionEventListener()的方法,用于附加需要事件类型和事件发生时要调用的函数的区域事件。由于我们有一个开始新区域的方法,我们还需要创建一个closeRegion()方法。该方法包含了大部分逻辑,用于确定是否发生了八个事件中的一个。最后,我们可以创建一个listen()方法,该方法向画布元素添加事件监听器,以便适当处理区域事件。

本文介绍的 Events 类通过使用beginRegion()closeRegion()方法定义区域,然后在每次触发事件时重新绘制区域,以便检测事件属于哪个区域。这种方法的优点是易于实现,而且我们只需要一个画布元素。

尽管这种方法对于具有合理数量的区域和附加事件监听器的画布应用程序非常有效,但对于使用大量区域的应用程序来说可能不是最佳方法。需要成千上万个区域,每个区域都有自己的事件监听器的应用程序可能会因为每次鼠标移动时重绘的形状数量而遇到性能问题。

对于这样的应用程序,可以使用更复杂的方法,为每个区域分配自己的画布,然后将画布堆叠在一起,这样每次触发事件时就不必重新绘制区域。这种方法的一个很好的例子是 KineticJS 库(www.kineticjs.com)。

使用画布鼠标坐标

为了熟悉 Events 类,我们将简单地使用 Events 类的getMousePos()方法获取鼠标光标的坐标,然后在画布的左上角显示它。getMousePos()方法返回相对于画布的鼠标坐标,考虑了画布相对于页面的偏移位置,以及页面的滚动位置。

使用画布鼠标坐标

如何做到这一点...

按照以下步骤获取画布鼠标坐标,并在鼠标光标移动时将其显示在画布的左上角:

  1. 链接到Events类:
<script src="img/events.js">
</script>
  1. 定义writeMessage()函数,用于写出消息:
<script>
    function writeMessage(context, message){
        context.font = "18pt Calibri";
        context.fillStyle = "black";
        context.fillText(message, 10, 25);
    }
  1. 实例化一个新的 Events 对象并获取画布和上下文:
    window.onload = function(){
        var events = new Events("myCanvas");
        var canvas = events.getCanvas();
        var context = events.getContext();
  1. 当用户鼠标移出画布时,清除画布,然后写出消息“Mouseover me!”:
        canvas.addEventListener("mouseout", function(){
            events.clear();
            writeMessage(context, "Mouseover me!");
        }, false);
  1. 当用户在画布中移动鼠标时,清除画布,然后写出鼠标位置:
        canvas.addEventListener("mousemove", function(){
            var mousePos = events.getMousePos();
            events.clear();

            if (mousePos !== null) {
                message = "Mouse position: " + mousePos.x + "," + mousePos.y;
                writeMessage(context, message);
            }
        }, false);
  1. 开始监听事件:
    // if we don't set the stage function,
    // we'll have to manually start listening for events
        events.listen();
  1. 在用户开始之前写出初始消息:
        writeMessage(context, "Mouseover me!");
    };
</script>
  1. 将画布嵌入到 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

页面加载后,我们可以实例化一个 Events 对象,以便我们可以访问getMousePos()方法。接下来,我们可以为 canvas 对象附加一个mouseout事件监听器,该监听器将事件显示设置为“Mouseover me!”,并且还可以为 canvas 对象附加一个mousemove事件监听器,该监听器使用getMousePos()方法获取鼠标位置,然后写出坐标。最后,我们可以使用listen()方法开始监听事件。

将鼠标事件监听器附加到区域

在这个示例中,我们将通过定义区域并向其添加事件侦听器来深入了解Events类。我们将绘制一个三角形,为其附加mouseoutmousemove事件侦听器,然后绘制一个没有事件侦听器的矩形,最后绘制一个圆形,并为其附加mouseovermouseoutmousedownmouseup事件侦听器,以尝试Events类支持的不同桌面事件侦听器。

将鼠标事件侦听器附加到区域

如何做...

按照以下步骤绘制一个三角形、一个矩形和一个圆形,然后为每个形状附加鼠标事件侦听器:

  1. 链接到Events类:
<script src="img/events.js">
</script>
  1. 定义writeMessage()函数,用于写出一条消息:
<script>
    function writeMessage(context, message){
        context.font = "18pt Calibri";
        context.fillStyle = "black";
        context.fillText(message, 10, 25);
    }
  1. 实例化一个新的Events对象并获取画布和上下文:
    window.onload = function(){
        var events = new Events("myCanvas");
        var canvas = events.getCanvas();
        var context = events.getContext();
        var message = "";
  1. 开始定义stage()函数,首先清除画布:
        events.setStage(function(){
          this.clear();
  1. 使用beginRegion()开始一个新区域,然后绘制一个蓝色三角形:
            // draw blue triangle
            this.beginRegion();
            context.beginPath();
            context.lineWidth = 4;
            context.strokeStyle = "black";
            context.fillStyle = "#00D2FF";
            context.moveTo(50, 50);
            context.lineTo(180, 80);
            context.lineTo(80, 170);
            context.closePath();
            context.fill();
            context.stroke();
  1. 向三角形添加mousemovemouseout事件侦听器,并使用closeRegion()关闭该区域:
            this.addRegionEventListener("mousemove", function(){
                var mousePos = events.getMousePos();
                var mouseX = mousePos.x - 50;
                var mouseY = mousePos.y - 50;
                message = "Triangle mouse Position: " + mouseX + "," + mouseY;
            });

            this.addRegionEventListener("mouseout", function(){
                message = "Mouseout blue triangle!";
            });

            this.closeRegion();
  1. 绘制一个没有事件侦听器的黄色矩形:
            // draw yellow rectangle
            // this is an example of a shape
            // with no event listeners
            context.beginPath();
            context.lineWidth = 4;
            context.strokeStyle = "black";
            context.fillStyle = "yellow";
            context.rect(200, 65, 150, 75);
            context.fill();
            context.stroke();
  1. 开始一个新区域并绘制一个红色圆形:
            // draw red circle
            this.beginRegion();
            context.beginPath();
            context.arc(450, canvas.height / 2, 70, 0, Math.PI * 2, true);
            context.fillStyle = "red";
            context.fill();
            context.stroke();
  1. 向圆形附加mousedownmouseupmouseovermouseout事件侦听器,并关闭该区域:
            this.addRegionEventListener("mousedown", function(){
                message = "Mousedown red circle!";
            });
            this.addRegionEventListener("mouseup", function(){
                message = "Mouseup red circle!";
            });
            this.addRegionEventListener("mouseover", function(){
                message = "Mouseover red circle!";
            });
            this.addRegionEventListener("mouseout", function(){
                message = "Mouseout red circle!";
            });

            this.closeRegion();
  1. 写出一条消息:
      writeMessage(context, message);
        });

    // since we set the draw stage function, the listen()
    // method is automatically called for us
    };
</script>
  1. 将画布嵌入到 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

要将事件附加到本示例中的三个形状,我们首先需要初始化一个Events对象,然后设置stage()函数。在stage()函数内部,我们可以使用beginRegion()定义一个新区域,绘制蓝色三角形,使用addRegionEventListener()附加事件,然后使用closeRegion()关闭该区域。接下来,我们可以绘制黄色矩形,而不需要定义区域,因为我们不会为其附加任何事件。最后,我们可以定义第二个区域,绘制红色圆形,附加事件侦听器,然后关闭该区域,完成stage()函数的定义。

另请参阅...

  • 在第二章中绘制一个矩形

  • 在第二章中绘制一个圆形

  • 在第二章中使用自定义形状和填充样式

  • 在移动设备上将触摸事件侦听器附加到区域

在移动设备上附加触摸事件侦听器到区域

对于那些喊着“移动设备怎么办?台式机和笔记本电脑已经过时了!”的人来说,这个示例就是为你准备的。随着互联网用户从巨大的桌面设备转向移动设备,并开始从移动设备消费互联网内容,每天都越来越明显,包括 canvas 在内的 Web 的未来主要在移动空间中。

与在台式机和笔记本电脑上运行的 Web 应用程序不同,移动设备上运行的 Web 应用程序是通过touchstarttouchendtouchmove事件的触摸事件来检测用户交互的。

在这个示例中,我们将通过向三角形和圆形添加触摸事件侦听器来创建前一个示例的移动版本。

如前所述,本章中的任何示例都可以通过添加触摸事件侦听器来修改以支持移动设备。

在移动设备上将触摸事件侦听器附加到区域

如何做...

按照以下步骤绘制一个三角形、一个矩形和一个圆形,然后为每个形状附加移动触摸事件:

  1. 在头标签内添加一个 viewport meta 标签,以设置移动设备的宽度,设置初始比例,并禁用用户缩放:
<meta name="viewport" content="width=device-width, initial-scale=0.552, user-scalable=no"/>
  1. 链接到Events类:
<script src="img/events.js">
</script>
  1. 定义writeMessage()函数,用于写出一条消息:
<script>
    function writeMessage(context, message){
        context.font = "18pt Calibri";
        context.fillStyle = "black";
        context.fillText(message, 10, 25);
    }
  1. 实例化一个新的Events对象并获取画布和上下文:
    window.onload = function(){
        var events = new Events("myCanvas");
        var canvas = events.getCanvas();
        var context = events.getContext();
        var message = "";
  1. 开始定义sStage()函数,首先清除画布:
        events.setStage(function(){
      this.clear();
  1. 使用beginRegion()开始一个新区域,然后绘制一个蓝色三角形:
            // draw blue triangle
            this.beginRegion();
            context.beginPath();
            context.lineWidth = 4;
            context.strokeStyle = "black";
            context.fillStyle = "#00D2FF";
            context.moveTo(50, 50);
            context.lineTo(180, 80);
            context.lineTo(80, 170);
            context.closePath();
            context.fill();
            context.stroke();
  1. touchmove事件监听器添加到三角形上,并使用closeRegion()关闭区域:
            this.addRegionEventListener("touchmove", function(){
                var touchPos = events.getTouchPos();

                if (touchPos !== null) {
                    var touchX = touchPos.x - 20;
                    var touchY = touchPos.y - 50;

                    message = "Triangle touch position: " + touchX + "," + touchY;
                }
            });

            this.closeRegion();
  1. 绘制一个没有事件监听器的黄色矩形:
            // draw yellow rectangle
            // this is an example of a shape
            // with no event listeners
            context.beginPath();
            context.lineWidth = 4;
            context.strokeStyle = "black";
            context.fillStyle = "yellow";
            context.rect(200, 65, 150, 75);
            context.fill();
            context.stroke();
  1. 开始一个新的区域并绘制一个红色圆:
            // draw red circle
            this.beginRegion();
            context.beginPath();
            context.arc(450, canvas.height / 2, 70, 0, Math.PI * 2, true);
            context.fillStyle = "red";
            context.fill();
            context.stroke();
  1. touchstarttouchend事件监听器附加到圆上并关闭区域:
            this.addRegionEventListener("touchstart", function(){
                message = "Touchstart red circle!";
            });

            this.addRegionEventListener("touchend", function(){
                message = "Touchend red circle!";
            });

            this.closeRegion();
  1. 写出一条消息:
      writeMessage(context, message);
        });

    // since we set the draw stage function, the listen()
    // method is automatically called for us
    };
</script>
  1. 将画布嵌入到 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

与上一个示例类似,在这个示例中,我们将事件监听器附加到三角形和圆上,只是这一次我们将附加触摸事件监听器,以便演示可以在移动设备上运行。

移动设备上的触摸事件实际上非常简单,并且工作方式与桌面事件基本相同。 mousedown 的移动设备等效是 touchstartmouseup 的等效是 touchendmousemove 的等效是 touchmove。由于移动设备无法检测到手指是否悬停在区域上,因此移动设备没有 mouseovermouseout 的等效,我不会感到惊讶,如果将来移动设备可以检测到手指是否接近屏幕但没有触碰到它。

为了显示蓝色三角形的触摸坐标,我们可以使用touchmove事件监听器,并且为了检测红色圆何时被触摸或释放,我们可以使用touchstarttouchend事件。

另请参阅...

  • 在第二章中绘制一个矩形

  • 在第二章中绘制一个圆

  • 在第二章中使用自定义形状和填充样式

  • 将鼠标事件监听器附加到区域

附加事件监听器到图像

在这个示例中,我们将事件监听器附加到图像上。由于我们只能使用Events类将事件监听器附加到路径上,并且在画布上绘制的图像不被归类为路径,因此我们可以创建一个覆盖图像的矩形区域,以便将事件监听器附加到矩形区域,并因此将事件监听器附加到图像。

将事件监听器附加到图像

如何做...

按照以下步骤绘制两个不同的图像,然后将mouseovermouseoutmousedownmouseup事件监听器附加到它们:

  1. 链接到Events类:
<script src="img/events.js">
</script>
  1. 定义writeMessage()函数,用于写出一条消息:
<script>
    function writeMessage(context, message){
        context.font = "18pt Calibri";
        context.fillStyle = "black";
        context.fillText(message, 10, 25);
    }
  1. 创建一个图像加载器,加载一组图像,然后在所有图像加载完成时调用callback函数:
    /*
     * loads the images and then calls the callback function
     * with a hash of image objects when the images have loaded
     */
    function loadImages(sources, callback){
        var loadedImages = 0;
        var numImages = 0;
        var images = {};
        // get num of sources
        for (var src in sources) {
            numImages++;
        }
       // load images
        for (var src in sources) {
            images[src] = new Image();
            images[src].onload = function(){
        // call callback function() when images
        // have loaded
                if (++loadedImages >= numImages) {
                    callback(images);
                }
            };
            images[src].src = sources[src];
        }
    }
  1. 定义drawImages()函数,该函数实例化一个新的Events对象并开始定义stage()函数:
    function drawImages(images){
        var events = new Events("myCanvas");
        var canvas = events.getCanvas();
        var context = events.getContext();
        var message = "";

        events.setStage(function(){
      this.clear();
  1. 开始一个新的区域,绘制左侧图像,定义代表图像路径的矩形区域,将事件监听器附加到矩形区域,然后关闭区域。然后重复这些步骤来创建右侧图像,并写出一条消息:
            this.beginRegion();

            context.drawImage(images.challengerImg, 50, 70, 240, 143);
            // draw rectangular region for image
            context.beginPath();
            context.rect(50, 70, 240, 143);
            context.closePath();

            this.addRegionEventListener("mouseover", function(){
                message = "Dodge Challenger mouseover!";
            });
            this.addRegionEventListener("mouseout", function(){
                message = "Dodge Challenger mouseout!";
            });
            this.addRegionEventListener("mousedown", function(){
                message = "Dodge Challenger mousedown!";
            });
            this.addRegionEventListener("mouseup", function(){
                message = "Dodge Challenger mouseup!";
            });
            this.closeRegion();

            this.beginRegion();
            context.drawImage(images.cobraImg, 350, 50, 200, 150);
            // draw rectangular region for image
            context.beginPath();
            context.rect(350, 50, 200, 150);
            context.closePath();
            this.addRegionEventListener("mouseover", function(){
                message = "AC Cobra mouseover!";
            });
            this.addRegionEventListener("mouseout", function(){
                message = "AC Cobra mouseout!";
            });
            this.addRegionEventListener("mousedown", function(){
                message = "AC Cobra mousedown!";
            });
            this.addRegionEventListener("mouseup", function(){
                message = "AC Cobra mouseup!";
            });
            this.closeRegion();

            writeMessage(context, message);
        });
    }
  1. 页面加载时创建图像源的哈希,然后将其传递给loadImages()函数:
    window.onload = function(){
        var sources = {
            challengerImg: "challenger.jpg",
            cobraImg: "cobra.jpg"
        };

        loadImages(sources, drawImages);
    };
</script>
  1. 将画布嵌入到 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

页面加载后,我们可以使用图像加载器函数加载两个图像。当两个图像都加载完成时,将调用drawImages()函数并实例化一个Events对象。在stage()函数内部,我们可以使用beginRegion()开始一个新的区域,绘制第一张图像,绘制一个矩形路径来定义图像路径,使用addRegionEventListener()附加事件,然后关闭区域。接下来,我们可以重复此过程,创建第二个图像及其自己的一组事件监听器。

另请参阅...

  • 在第三章中绘制一个图像

拖放形状

在这个食谱中,我们将解决事件侦听器的圣杯——拖放。如果没有Events类或其他轻量级的 JavaScript 库,拖放操作可能会相当繁琐。我们可以使用Events类将mouseovermousedownmousemovemouseupmouseout事件侦听器附加到矩形上,以处理拖放操作的不同阶段。

拖放形状

如何做...

按照以下步骤拖放矩形:

  1. 链接到Events类:
<script src="img/events.js">
</script>
  1. 定义writeMessage()函数,用于写出消息:
<script>
    function writeMessage(context, message){
        context.font = "18pt Calibri";
        context.fillStyle = "black";
        context.fillText(message, 10, 25);
    }
  1. 页面加载时,实例化一个新的Events对象,定义将要拖放的矩形的起始位置,并为拖放操作定义draggingRectdraggingRectOffsetXdraggingRectOffsetY
    window.onload = function(){
        events = new Events("myCanvas");
        var canvas = events.getCanvas();
        var context = events.getContext();

        var rectX = canvas.width / 2 - 50;
        var rectY = canvas.height / 2 - 25;
        var draggingRect = false;
        var draggingRectOffsetX = 0;
        var draggingRectOffsetY = 0;
  1. 对于stage()函数,首先根据鼠标的坐标设置矩形的坐标,如果draggingRect布尔值为 true:
        events.setStage(function(){                    
            // get the mouse position
            var mousePos = this.getMousePos();

            if (draggingRect) {
                rectX = mousePos.x - draggingRectOffsetX;
                rectY = mousePos.y - draggingRectOffsetY;
            }
  1. 清除画布,写出消息,开始一个新的区域,绘制矩形,附加事件,然后关闭区域:
            // clear the canvas
            this.clear();

            writeMessage(context, "Drag and drop the box...");

            this.beginRegion();

            // draw the box
            context.beginPath();
            context.rect(rectX, rectY, 100, 50);
            context.lineWidth = 4;
            context.strokeStyle = "black";
            context.fillStyle = "#00D2FF";
            context.fill();
            context.stroke();
            context.closePath();

            // attach event listeners
            this.addRegionEventListener("mousedown", function(){
                draggingRect = true;
                var mousePos = events.getMousePos();

                draggingRectOffsetX = mousePos.x - rectX;
                draggingRectOffsetY = mousePos.y - rectY;
            });
            this.addRegionEventListener("mouseup", function(){
                draggingRect = false;
            });
            this.addRegionEventListener("mouseover", function(){
                document.body.style.cursor = "pointer";
            });
            this.addRegionEventListener("mouseout", function(){
                document.body.style.cursor = "default";
            });

            this.closeRegion();
        });
    };
</script>
  1. 将画布嵌入到 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

拖放由三个阶段处理:

  1. 检测形状上的mousedown事件,开始操作

  2. 使用mousemove事件侦听器根据鼠标坐标定位形状

  3. 当鼠标按钮释放时放下形状(mouseup

stage()函数内,如果draggingRect布尔值为 true,我们可以设置矩形相对于鼠标位置的位置。然后我们可以使用beginRegion()开始一个新的区域,绘制矩形,然后使用addRegionEventListener()方法附加事件侦听器。我们可以添加一个mousedown事件侦听器,将draggingRect布尔值设置为 true,然后计算draggingRectOffsetXdraggingRectOffsetY变量,这些变量考虑了鼠标和矩形左上角之间的位置偏移。接下来,我们可以添加一个mouseup事件侦听器,将draggingRect布尔值设置为 false,完成拖放操作。我们还可以附加一个mouseover事件侦听器,将光标变成手形,以显示可以与元素交互,还可以附加一个mouseout事件侦听器,将光标图像恢复为默认指针,以指示鼠标光标不再位于元素上。

另请参见...

  • 拖放图像

拖放图像

这个食谱基本上结合了前两个食谱的概念,演示了如何拖放图像。

如何做...

按照以下步骤拖放图像:

  1. 链接到Events类:
<script src="img/events.js">
</script>
  1. 定义writeMessage()函数,用于写出消息:
<script>
    function writeMessage(context, message){
        context.font = "18pt Calibri";
        context.fillStyle = "black";
        context.fillText(message, 10, 25);
    }
  1. 定义drawImage()函数,该函数首先实例化一个新的Events对象,并设置覆盖图像的矩形区域的初始位置:
    function drawImage(challengerImg){
        var events = new Events("myCanvas");
        var canvas = events.getCanvas();
        var context = events.getContext();

        var rectX = canvas.width / 2 - challengerImg.width / 2;
        var rectY = canvas.height / 2 - challengerImg.height / 2;
        var draggingRect = false;
        var draggingRectOffsetX = 0;
        var draggingRectOffsetY = 0;
  1. 定义stage()函数,该函数首先根据鼠标的坐标设置图像的位置,如果draggingRect布尔值为 true:
        events.setStage(function(){
            var mousePos = this.getMousePos();

            if (draggingRect) {
                rectX = mousePos.x - draggingRectOffsetX;
                rectY = mousePos.y - draggingRectOffsetY;
            }
  1. 清除画布并写出消息:
            // clear the canvas
            this.clear();
            writeMessage(context, "Drag and drop the car...");
  1. 开始一个新的区域,绘制图像,绘制一个矩形区域来定义图像路径,附加事件侦听器,然后关闭区域:
            this.beginRegion();
            context.drawImage(challengerImg, rectX, rectY, challengerImg.width, challengerImg.height);
            // draw rectangular region for image
            context.beginPath();
            context.rect(rectX, rectY, challengerImg.width, challengerImg.height);
            context.closePath();

            this.addRegionEventListener("mousedown", function(){
                draggingRect = true;
                var mousePos = events.getMousePos();

                draggingRectOffsetX = mousePos.x - rectX;
                draggingRectOffsetY = mousePos.y - rectY;
            });
            this.addRegionEventListener("mouseup", function(){
                draggingRect = false;
            });
            this.addRegionEventListener("mouseover", function(){
                document.body.style.cursor = "pointer";
            });
            this.addRegionEventListener("mouseout", function(){
                document.body.style.cursor = "default";
            });

            this.closeRegion();
        });
    }
  1. 页面加载时,加载图像,然后调用drawImage()函数:
    window.onload = function(){
        // load image
        challengerImg = new Image();
        challengerImg.onload = function(){
            drawImage(this);
        };
        challengerImg.src = "challenger.jpg";
    };
</script>
  1. 将画布嵌入到 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

它是如何工作的...

要拖放图像,我们可以在图像上方绘制一个不可见的矩形路径,为图像提供路径,并且可以像处理前一个食谱一样附加mousedownmouseupmousemove事件来处理拖放的三个阶段。

当用户拖放图像时,实质上是在拖放图像及其对应的矩形路径。

另请参见...

  • *在第三章中绘制图像

  • 拖放形状

创建图像放大器

在本教程中,我们将通过根据小图像的鼠标坐标裁剪大图像的一部分来创建一个非常漂亮的图像放大器,然后在小图像上方显示结果。

创建图像放大器

操作步骤...

按照以下步骤创建一个图像放大器,当用户将鼠标悬停在图像上方时,它会呈现图像的放大部分:

  1. 链接到Events类:
<script src="img/events.js">
</script>
  1. 创建一个图像加载器,加载小图像和大图像,然后在图像加载完成时调用回调函数:
<script>
    /*
     * loads the images and then calls the callback function
     * with a hash of image objects  when the images have loaded
     */
    function loadImages(sources, callback){
        var loadedImages = 0;
        var numImages = 0;
        var images = {};
        // get num of sources
        for (var src in sources) {
            numImages++;
        }
        // load images
        for (var src in sources) {
            images[src] = new Image();
            images[src].onload = function(){
                // call callback function when images
                // have loaded
                if (++loadedImages >= numImages) {
                    callback(images);
                }
            };
            images[src].src = sources[src];
        }
    }
  1. 定义drawMagnifier()函数,绘制放大的图像:
    function drawMagnifier(config){
        var context = config.context;
    var images = config.images;
        var mousePos = config.mousePos;
        var imageX = config.imageX;
        var imageY = config.imageY;
        var magWidth = config.magWidth;
        var magHeight = config.magHeight;
        var smallWidth = config.smallWidth;
        var smallHeight = config.smallHeight;
        var largeWidth = config.largeWidth;
        var largeHeight = config.largeHeight;

        /*
         * sourceX and sourceY assume that the rectangle we are
         * cropping out of the large image exists within the large
         * image. We'll have to make some adjustments for the
         * cases where the magnifier goes past the edges of the * large image
         */
        var sourceX = ((mousePos.x - imageX) * largeWidth / smallWidth) - magWidth / 2;
        var sourceY = ((mousePos.y - imageY) * largeHeight / smallHeight) - magHeight / 2;
        var destX = mousePos.x - magWidth / 2;
        var destY = mousePos.y - magHeight / 2;
        var viewWidth = magWidth;
        var viewHeight = magHeight;
        var viewX = destX;
        var viewY = destY;
        var drawMagImage = true;
        // boundary checks and adjustments for cases
        // where the magnifyer goes past the edges of the large image
        if (sourceX < 0) {
            if (sourceX > -1 * magWidth) {
                var diffX = -1 * sourceX;
                viewX += diffX;
                viewWidth -= diffX;
                sourceX = 0;
            }
            else {
                drawMagImage = false;
            }
        }

        if (sourceX > largeWidth - magWidth) {
            if (sourceX < largeWidth) {
                viewWidth = largeWidth - sourceX;
            }
            else {
                drawMagImage = false;
            }
        }

        if (sourceY < 0) {
            if (sourceY > -1 * magHeight) {
                var diffY = -1 * sourceY;
                viewY += diffY;
                viewHeight -= diffY;
                sourceY = 0;
            }
            else {
                drawMagImage = false;
            }
        }

        if (sourceY > largeHeight - magHeight) {
            if (sourceY < largeHeight) {
                viewHeight = largeHeight - sourceY;
            }
            else {
                drawMagImage = false;
            }
        }
        // draw white magnifier background
        context.beginPath();
        context.fillStyle = "white";
        context.fillRect(destX, destY, magWidth, magHeight);

        // draw image
        if (drawMagImage) {
            context.beginPath();
            context.drawImage(images.cobraLargeImg, sourceX, sourceY, viewWidth, viewHeight, viewX, viewY, viewWidth, viewHeight);
        }

        // draw magnifier border
        context.beginPath();
        context.lineWidth = 2;
        context.strokeStyle = "black";
        context.strokeRect(destX, destY, magWidth, magHeight);
    }
  1. 定义drawImages()函数,该函数首先实例化一个新的Events对象,并定义放大镜的属性:
    function drawImages(images){
        var events = new Events("myCanvas");
        var canvas = events.getCanvas();
        var context = events.getContext();

        // define magnifier dependencies
        var imageX = canvas.width / 2 - images.cobraSmallImg.width / 2;
        var imageY = canvas.height / 2 - images.cobraSmallImg.height / 2;
        var magWidth = 200;
        var magHeight = 150;
        var smallWidth = images.cobraSmallImg.width;
        var smallHeight = images.cobraSmallImg.height;
        var largeWidth = images.cobraLargeImg.width;
        var largeHeight = images.cobraLargeImg.height;
  1. 设置stage()函数,绘制小图像,然后调用drawMagnifier()绘制放大的图像:
        events.setStage(function(){
            var mousePos = events.getMousePos();
            this.clear();
            context.drawImage(images.cobraSmallImg, imageX, imageY, smallWidth, smallHeight);
            // draw border around image
            context.beginPath();
            context.lineWidth = 2;
            context.strokeStyle = "black";
            context.strokeRect(imageX, imageY, smallWidth, smallHeight);
            context.closePath();

            if (mousePos !== null) {
                drawMagnifier({
                    context: context,
          images: images,
                    mousePos: mousePos,
                    imageX: imageX,
                    imageY: imageY,
                    magWidth: magWidth,
                    magHeight: magHeight,
                    smallWidth: smallWidth,
                    smallHeight: smallHeight,
                    largeWidth: largeWidth,
                    largeHeight: largeHeight
                });
            }
        });
  1. 向画布元素添加事件侦听器,如果用户将鼠标移出画布,则重新绘制舞台以移除放大的图像:
        canvas.addEventListener("mouseout", function(){
            events.stage();
        }, false);
    }
  1. 页面加载时,构建图像源的哈希,并将其传递给图像加载器函数:
    window.onload = function(){
        var sources = {
            cobraSmallImg: "cobra_280x210.jpg",
            cobraLargeImg: "cobra_800x600.jpg"
        };

        loadImages(sources, drawImages);
    };
</script>
  1. 将画布嵌入到 HTML 文档的主体中。
<canvas id="myCanvas" width="600" height="250" style="border:1px solid black;">
</canvas>

工作原理...

要创建图像放大器,我们需要两个图像,一个小图像和一个大图像。小图像将始终显示在画布上,而大图像将用作缓冲图像以绘制放大镜。页面加载后,两个图像都加载完成后,我们可以实例化一个Events对象并开始定义stage()函数。

在画布上绘制小图像后,我们可以通过计算drawImage()方法的sourceXsourceYdestXdestY参数来绘制放大的图像,该方法将裁剪出大图像的放大部分,然后在小图像上方显示结果。

要获取sourceXsourceY,我们可以通过鼠标位置与小图像左上角位置的差值来获取相对于小图像的鼠标坐标,然后我们可以通过放大倍数(即大图像宽度除以小宽度)乘以结果,并减去放大窗口大小的一半,来获取大图像的相应坐标:

        var sourceX = ((mousePos.x - imageX) * largeWidth / smallWidth) - magWidth / 2;
        var sourceY = ((mousePos.y - imageY) * largeHeight / smallHeight) - magHeight / 2;

为了使放大的图像居中在鼠标光标上,我们可以将destX设置为鼠标位置的 x 偏移量减去放大镜宽度的一半,并且我们可以将destY设置为鼠标位置的 y 偏移量减去放大镜高度的一半:

var destX = mousePos.x - magWidth / 2;
var destY = mousePos.y - magHeight / 2;

另请参阅...

  • 在第三章中绘制图像

  • 在第三章中裁剪图像

创建绘图应用程序

在本教程中,我们将创建一个漂亮的绘图应用程序,以便用户可以在浏览器中绘制图片。

创建绘图应用程序

操作步骤...

按照以下步骤创建一个简单的绘图应用程序:

  1. 样式化工具栏、输入和按钮:
        <style>
            canvas {
                border: 1px solid black;
                font-family: “Helvetica Neue”, “Arial”, “Lucida Grande”, “Lucida Sans Unicode”, “Microsoft YaHei”, sans-serif;
                font-size: 13px;
                line-height: 1.5;
                color: #474747;
            }

            #toolbar {
                width: 590px;
                border: 1px solid black;
                border-bottom: 0px;
                padding: 5px;
                background-color: #f8f8f8;
            }

            input[type = ‘text’] {
                width: 30px;
				margin: 0px 5px 0px 5px;
            }
            label {
                margin-left: 40px;
            }

            label:first-of-type {
                margin-left: 0px;
            }

            input[type = ‘button’] {
                float: right;
            }

            #colorSquare {
                position: relative;
                display: inline-block;
                width: 20px;
                height: 20px;
                background-color: blue;
                top: 4px;
            }
        </style>
  1. 链接到Events类:
<script src=”events.js”>
</script>
  1. 定义addPoint()函数,向点数组添加一个点:
        <script>
            function addPoint(events, points){
                var context = events.getContext();
                var drawingPos = events.getMousePos();

                if (drawingPos !== null) {
                    points.push(drawingPos);
                }
            }
  1. 定义drawPath()函数,清除画布,在路径开始之前重新绘制画布绘图,然后使用点数组中的点绘制绘图路径:
            function drawPath(canvas, points, canvasImg){
                var context = canvas.getContext(“2d”);

                // clear canvas
                context.clearRect(0, 0, canvas.width, canvas.height);

                // redraw canvas before path
                context.drawImage(canvasImg, 0, 0, canvas.width, canvas.height);

                // draw patch
                context.beginPath();
                context.lineTo(points[0].x, points[0].y);
                for (var n = 1; n < points.length; n++) {
                    var point = points[n];
                    context.lineTo(point.x, point.y);
                }
                context.stroke();
            }
  1. 定义updateColorSquare()函数,更新工具栏颜色方块的颜色:
            function updateColorSquare(){
                var red = document.getElementById(“red”).value;
                var green = document.getElementById(“green”).value;
                var blue = document.getElementById(“blue”).value;

                var colorSquare = document.getElementById(“colorSquare”);
                colorSquare.style.backgroundColor = “rgb(“ + red + “,” + green + “,” + blue + “)”;
            }
  1. 定义getCanvasImg()方法,返回画布绘图的图像对象:
            function getCanvasImg(canvas){
                var img = new Image();
                img.src = canvas.toDataURL();
                return img;
            }
  1. 页面加载时,实例化一个新的Events对象,定义isMouseDown标志,获取画布图像,并初始化绘图颜色和大小:
            window.onload = function(){
                var events = new Events(“myCanvas”);
                var canvas = events.getCanvas();
                var context = events.getContext();
                var isMouseDown = false;
                var canvasImg = getCanvasImg(canvas);
                var points = [];

                // initialize drawing params
                var red = document.getElementById(“red”).value;
                var green = document.getElementById(“green”).value;
                var blue = document.getElementById(“blue”).value;
                var size = document.getElementById(“size”).value;
  1. 输入新颜色时更新颜色方块:
                // attach listeners
                document.getElementById(“red”).addEventListener(“keyup”, function(evt){
                    updateColorSquare();
                }, false);

                document.getElementById(“green”).addEventListener(“keyup”, function(evt){
                    updateColorSquare();
                }, false);

                document.getElementById(“blue”).addEventListener(“keyup”, function(evt){
                    updateColorSquare();
                }, false);
  1. 按下清除按钮时清除画布:
                document.getElementById(“clearButton”).addEventListener(“click”, function(evt){
                    events.clear();
                    points = [];
                    canvasImg = getCanvasImg(canvas);
                }, false);
  1. 按下保存按钮时,将画布绘图转换为数据 URL,并在新窗口中打开绘图作为图像:
                document.getElementById(“saveButton”).addEventListener(“click”, function(evt){
                    // open new window with saved image so user
                    // can right click and save to their computer
                    window.open(canvas.toDataURL());
                }, false);
  1. 当用户在画布上mousedown时,获取绘图位置、颜色和大小,设置路径样式,将第一个点添加到点数组中,然后将isMouseDown标志设置为 true:
                canvas.addEventListener(“mousedown”, function(){
                    var drawingPos = events.getMousePos();

                    // update drawing params
                    red = document.getElementById(“red”).value;
                    green = document.getElementById(“green”).value;
                    blue = document.getElementById(“blue”).value;
                    size = document.getElementById(“size”).value;

                    // start drawing path
                    context.strokeStyle = “rgb(“ + red + “,” + green + “,” + blue + “)”;
                    context.lineWidth = size;
                    context.lineJoin = “round”;
                    context.lineCap = “round”;
                    addPoint(events, points);
                    isMouseDown = true;
                }, false);
  1. 当用户从画布上mouseup时,将isMouseDown标志设置为 false,绘制路径,然后保存当前图像绘制:
                canvas.addEventListener(“mouseup”, function(){
                    isMouseDown = false;
                    if (points.length > 0) {
                        drawPath(this, points, canvasImg);
                        // reset points
                        points = [];
                    }
                    canvasImg = getCanvasImg(this);
                }, false);
  1. 当用户的鼠标离开画布时,模拟一个mouseup事件:
                canvas.addEventListener(“mouseout”, function(){
                    if (document.createEvent) {
                        var evt = document.createEvent(‘MouseEvents’);
                        evt.initEvent(“mouseup”, true, false);
                        this.dispatchEvent(evt);
                    }
                    else {
                        this.fireEvent(“onmouseup”);
                    }
                }, false);
  1. 设置stage()函数,如果鼠标按下并移动,则不断向当前绘图路径添加新点:
                events.setStage(function(){
                    if (isMouseDown) {
                        addPoint(this, points);
                        drawPath(canvas, points, canvasImg);
                    }
                });
            };
        </script>
  1. 构建工具栏并添加画布元素:
    <body>
        <div id=”toolbar”>
            <label>
                Color
            </label>
            R: <input type=”text” id=”red” maxlength=”3” class=”short” value=”0”>G: <input type=”text” id=”green” maxlength=”3” class=”short” value=”0”>B: <input type=”text” id=”blue” maxlength=”3” class=”short” value=”255”>
            <div id=”colorSquare”>
            </div>
            <label>
                Size:
            </label>
            <input type=”text” id=”size” maxlength=”3” class=”short” value=”20”>px<input type=”button” id=”clearButton” value=”Clear”><input type=”button” id=”saveButton” value=”Save”>
        </div>
        <canvas id=”myCanvas” width=”600” height=”250”>
        </canvas>
    </body>

它是如何工作的...

绘图应用程序通常具有以下核心功能:

  • mousedown事件开始绘图路径,mouseup事件结束绘图路径

  • 可以设置线宽

  • 可以设置颜色

  • 可以清除绘图

  • 可以保存绘图

当然,如果你想在网页上创建类似于 Photoshop 或 Gimp 的绘图应用程序,你可以添加数百种其他功能,但在这里,我们只是确定了一些基本功能来开始。

前面列表中的第一条显然是最重要的-我们需要找出一种用户可以在屏幕上绘制线条的方法。最直接的方法是按照以下步骤进行:

  1. 当用户在画布上的某个地方mousedown时,设置路径样式并将鼠标位置坐标添加到点数组中,以定义绘图路径的起点。

  2. 当用户移动鼠标时,获取鼠标位置并向点数组添加另一个点,然后用新点重新绘制路径。

  3. 当用户从画布上mouseup时,设置一个标志,表示路径结束,并保存当前绘图图像以便在下一个绘图路径中使用。

为了保持简单,我们可以让用户使用文本输入设置线宽,并让用户使用三个文本输入设置颜色(颜色的红色、绿色和蓝色分量)。

最后,我们可以创建一个清除按钮,使用Events对象的clear()方法清除画布,并创建一个保存按钮,使用画布上下文的toDataURL()方法将画布绘图转换为数据 URL,然后打开一个新窗口显示数据 URL。然后,用户可以右键单击图像将其保存到计算机上。

还有更多...

如果你正在创建一个更复杂的绘图应用程序,以下是一些更多的想法:

  • 在所有主要浏览器都支持颜色选择器输入之前,你可以创建一个自定义颜色选择器小部件,让用户以图形方式选择颜色,而不是输入他们想要的颜色的红色、绿色和蓝色分量

  • 你可以使用 HTML5 范围输入创建滑块条来设置画笔大小

  • 你可以通过动态创建每个图层的新画布元素来创建图层支持。类似于 Photoshop 和 Gimp,你可以提供删除图层和合并图层的功能

  • 如果你的应用程序支持分层,你还可以为每个图层添加不透明度控制

  • 你可以通过将绘图保存在本地存储或离线数据库中来增强保存功能(参见第三章中的将画布绘图转换为数据 URL

  • 提供预先构建的绘图形状,如直线、矩形和圆形

  • 允许形状进行缩放和旋转

  • 允许用户将图像导入其绘图中

  • 列表还在继续...

希望这个教程能进一步激发你对画布的兴趣,并让你思考其他可能性。我认为可以肯定地说,最终会有人创建一个完全由画布驱动的全功能图像编辑网页应用程序,并让 Adobe 感到压力。也许这个人就是你!

另请参阅...

  • 在第一章中绘制螺旋线

  • 在第三章中将画布绘制转换为数据 URL

  • 在第三章中将画布绘图保存为图像

  • 使用画布鼠标坐标

第七章:创建图表和图形

在本章中,我们将涵盖:

  • 创建一个饼图

  • 创建一个条形图

  • 绘制方程

  • 用线图绘制数据点

介绍

到目前为止,您可能已经注意到第一章到第四章涵盖了 HTML5 画布基础知识,第五章和第六章涵盖了高级主题,而第七章和第八章涵盖了真实的应用。毕竟,如果我们不能产生有用的东西,学习画布有什么好处呢?本章重点是通过创建一些真实的画布应用程序,如创建饼图、条形图、图表和线图。与前几章不同,本章只包含四个示例,因为每个示例都提供了一个完整、易于配置和可投入生产的产品。让我们开始吧!

创建一个饼图

饼图可能是最常见的数据可视化之一,因为它们可以快速让用户了解数据元素的相对权重。在这个示例中,我们将创建一个可配置的饼图类,它接受一个数据元素数组并生成一个饼图。此外,我们将以这样的方式构造饼图绘制方法,使得饼图和标签自动填满尽可能多的画布。

创建饼图

如何做...

按照以下步骤创建一个可以自动定位和调整饼图和图例的饼图类:

  1. 为'PieChart'类定义构造函数,用于绘制饼图:
/*
 * PieChart constructor
 */
function PieChart(canvasId, data){
  // user defined properties
  this.canvas = document.getElementById(canvasId);
  this.data = data;

  // constants
  this.padding = 10;
  this.legendBorder = 2;
  this.pieBorder = 5;
  this.colorLabelSize = 20;
  this.borderColor = "#555";
  this.shadowColor = "#777";
  this.shadowBlur = 10;
  this.shadowX = 2;
  this.shadowY = 2;
  this.font = "16pt Calibri";

    // relationships
    this.context = this.canvas.getContext("2d");
    this.legendWidth = this.getLegendWidth();
    this.legendX = this.canvas.width - this.legendWidth;
    this.legendY = this.padding;
    this.pieAreaWidth = (this.canvas.width - this.legendWidth);
    this.pieAreaHeight = this.canvas.height;
    this.pieX = this.pieAreaWidth / 2;
    this.pieY = this.pieAreaHeight / 2;
    this.pieRadius = (Math.min(this.pieAreaWidth, this.pieAreaHeight) / 2) - (this.padding);

    // draw pie chart
    this.drawPieBorder();
    this.drawSlices();
    this.drawLegend();
}
  1. 定义'getLegendWidth()'方法,通过考虑最长标签的文本长度来返回图例的宽度:
/*
 * gets the legend width based on the size
 * of the label text
 */
PieChart.prototype.getLegendWidth = function(){
    /*
     * loop through all labels and determine which
     * label is the longest.  Use this information
     * to determine the label width
     */
    this.context.font = this.font;
    var labelWidth = 0;

    for (var n = 0; n < this.data.length; n++) {
        var label = this.data[n].label;
        labelWidth = Math.max(labelWidth, this.context.measureText(label).width);
    }

    return labelWidth + (this.padding * 2) + this.legendBorder + this.colorLabelSize;
};
  1. 定义'drawPieBorder()'方法,绘制饼图周围的边框:
PieChart.prototype.drawPieBorder = function(){
    var context = this.context;
    context.save();
    context.fillStyle = "white";
    context.shadowColor = this.shadowColor;
    context.shadowBlur = this.shadowBlur;
    context.shadowOffsetX = this.shadowX;
    context.shadowOffsetY = this.shadowY;
    context.beginPath();
    context.arc(this.pieX, this.pieY, this.pieRadius + this.pieBorder, 0, Math.PI * 2, false);
    context.fill();
    context.closePath();
    context.restore();
};
  1. 定义'drawSlices()'方法,循环遍历数据并为每个数据元素绘制一个饼图切片:
/*
 * draws the slices for the pie chart
 */
PieChart.prototype.drawSlices = function(){
    var context = this.context;
    context.save();
    var total = this.getTotalValue();
    var startAngle = 0;
    for (var n = 0; n < this.data.length; n++) {
        var slice = this.data[n];

        // draw slice
        var sliceAngle = 2 * Math.PI * slice.value / total;
        var endAngle = startAngle + sliceAngle;

        context.beginPath();
        context.moveTo(this.pieX, this.pieY);
        context.arc(this.pieX, this.pieY, this.pieRadius, startAngle, endAngle, false);
        context.fillStyle = slice.color;
        context.fill();
        context.closePath();
        startAngle = endAngle;
    }
  context.restore();
};
  1. 定义'getTotalValue()'方法,用于获取数据值的总和:
/*
 * gets the total value of the labels by looping through
 * the data and adding up each value
 */
PieChart.prototype.getTotalValue = function(){
    var data = this.data;
    var total = 0;

    for (var n = 0; n < data.length; n++) {
        total += data[n].value;
    }

    return total;
};
  1. 定义'drawLegend()'方法,绘制图例:
/*
 * draws the legend
 */
PieChart.prototype.drawLegend = function(){
    var context = this.context;
    context.save();
    var labelX = this.legendX;
    var labelY = this.legendY;

    context.strokeStyle = "black";
    context.lineWidth = this.legendBorder;
    context.font = this.font;
    context.textBaseline = "middle";

    for (var n = 0; n < this.data.length; n++) {
        var slice = this.data[n];

        // draw legend label
        context.beginPath();
        context.rect(labelX, labelY, this.colorLabelSize, this.colorLabelSize);
        context.closePath();
        context.fillStyle = slice.color;
        context.fill();
        context.stroke();
        context.fillStyle = "black";
        context.fillText(slice.label, labelX + this.colorLabelSize + this.padding, labelY + this.colorLabelSize / 2);

        labelY += this.colorLabelSize + this.padding;
    }
  context.restore();
};
  1. 页面加载时,构建数据并实例化一个'PieChart'对象:
window.onload = function(){
    var data = [{
        label: "Eating",
        value: 2,
        color: "red"
    }, {
        label: "Working",
        value: 8,
        color: "blue"
    }, {
        label: "Sleeping",
        value: 8,
        color: "green"
    }, {
        label: "Errands",
        value: 2,
        color: "yellow"
    }, {
        label: "Entertainment",
        value: 4,
        color: "violet"
    }];

    new PieChart("myCanvas", data);
};
  1. 在 HTML 文档的 body 中嵌入 canvas 标签:
<canvas id="myCanvas" width="600" height="300" style="border:1px solid black;">
</canvas>

另请参阅...

  • 在第一章中绘制一个弧线

  • 在第一章中使用文本

  • 在第二章中绘制一个矩形

它是如何工作的...

在深入了解代码如何工作之前,让我们先退一步,思考一下'PieChart'对象应该做什么。作为开发人员,我们需要传入画布 ID,以便对象知道在哪里绘制,还需要一个数据元素数组,以便知道要绘制什么。

使用'drawSlices()'和'drawPieBorder()'方法呈现'PieChart'元素。'drawSlices()'方法执行以下步骤:

  1. 循环遍历数据元素。

  2. 通过将 2π乘以总值的值分数来计算每个数据值的角度。

  3. 使用'arc()'方法为每个切片绘制弧线。

  4. 用数据元素颜色填充每个切片。

饼图呈现后,我们可以使用'drawLegend()'方法绘制图例。此方法执行以下步骤:

  1. 循环遍历数据元素。

  2. 为每个元素使用'rect()'绘制一个框。

  3. 使用'stroke()'和'fill()'为每个框描边和填充数据元素颜色。

  4. 使用'fillText()'为每个元素写入相应的标签。

页面加载后,我们可以创建一个数据元素数组,用于标识我们的日常活动以及每个活动对应的小时数,然后通过传入数据数组实例化一个新的 PieChart 对象。

提示

在这个示例中,我们通过硬编码数据元素数组来创建了人工数据。然而,在现实生活中,我们更可能通过 JSON 或 XML 等方式提供数据。

创建条形图

在饼图之后,条形图是另一个用于可视化数据的流行工具。在这个示例中,我们将创建一个可配置的 Bar Chart 类,该类接受一个数据元素数组并创建一个简单的条形图。我们将重用上一个示例中的数据结构来进行比较。与饼图类不同,条形图绘制方法还会自动缩放图表以填充画布。

创建条形图

如何做...

按照以下步骤创建一个 Bar Chart 类,该类可以自动定位和调整一个数据数组的条形图的大小:

  1. 定义BarChart构造函数,绘制图表:
/*
 * BarChart constructor
 */
function BarChart(config){
    // user defined properties
    this.canvas = document.getElementById(config.canvasId);
    this.data = config.data;
    this.color = config.color;
    this.barWidth = config.barWidth;
    this.gridLineIncrement = config.gridLineIncrement;
    /*
     * adjust max value to highest possible value divisible
     * by the grid line increment value and less than
     * the requested max value
     */
    this.maxValue = config.maxValue - Math.floor(config.maxValue % this.gridLineIncrement);
    this.minValue = config.minValue;

    // constants
    this.font = "12pt Calibri";
    this.axisColor = "#555";
    this.gridColor = "#aaa";
    this.padding = 10;

    // relationships
    this.context = this.canvas.getContext("2d");
    this.range = this.maxValue - this.minValue;
    this.numGridLines = this.numGridLines = Math.round(this.range / this.gridLineIncrement);
    this.longestValueWidth = this.getLongestValueWidth();
    this.x = this.padding + this.longestValueWidth;
    this.y = this.padding * 2;
    this.width = this.canvas.width - (this.longestValueWidth + this.padding * 2);
    this.height = this.canvas.height - (this.getLabelAreaHeight() + this.padding * 4);

    // draw bar chart
    this.drawGridlines();
    this.drawYAxis();
    this.drawXAxis();
    this.drawBars();
    this.drawYVAlues();
    this.drawXLabels();
}
  1. 定义getLabelAreaHeight()方法,确定标签区域的高度(x 轴下方的标签):
/*
 * gets the label height by finding the max label width and
 * using trig to figure out the projected height since
 * the text will be rotated by 45 degrees
 */
BarChart.prototype.getLabelAreaHeight = function(){
    this.context.font = this.font;
    var maxLabelWidth = 0;

    /*
     * loop through all labels and determine which
     * label is the longest.  Use this information
     * to determine the label width
     */
    for (var n = 0; n < this.data.length; n++) {
        var label = this.data[n].label;
        maxLabelWidth = Math.max(maxLabelWidth, this.context.measureText(label).width);
    }

    /*
     * return y component of the labelWidth which
     * is at a 45 degree angle:
     *
     * a² + b² = c²
     * a = b
     * c = labelWidth
     * a = height component of right triangle
     * solve for a
     */
    return Math.round(maxLabelWidth / Math.sqrt(2));
};
  1. 定义getLongestValueWidth()方法,返回最长值文本宽度:
BarChart.prototype.getLongestValueWidth = function(){
    this.context.font = this.font;
    var longestValueWidth = 0;
    for (var n = 0; n <= this.numGridLines; n++) {
        var value = this.maxValue - (n * this.gridLineIncrement);
        longestValueWidth = Math.max(longestValueWidth, this.context.measureText(value).width);

    }
    return longestValueWidth;
};
  1. 定义drawXLabels()方法,绘制 x 轴标签:
BarChart.prototype.drawXLabels = function(){
    var context = this.context;
    context.save();
    var data = this.data;
    var barSpacing = this.width / data.length;

    for (var n = 0; n < data.length; n++) {
        var label = data[n].label;
        context.save();
        context.translate(this.x + ((n + 1 / 2) * barSpacing), this.y + this.height + 10);
        context.rotate(-1 * Math.PI / 4); // rotate 45 degrees
        context.font = this.font;
        context.fillStyle = "black";
        context.textAlign = "right";
        context.textBaseline = "middle";
        context.fillText(label, 0, 0);
        context.restore();
    }
    context.restore();
};
  1. 定义drawYValues()方法,绘制 y 轴值:
BarChart.prototype.drawYVAlues = function(){
    var context = this.context;
    context.save();
    context.font = this.font;
    context.fillStyle = "black";
    context.textAlign = "right";
    context.textBaseline = "middle";

    for (var n = 0; n <= this.numGridLines; n++) {
        var value = this.maxValue - (n * this.gridLineIncrement);
        var thisY = (n * this.height / this.numGridLines) + this.y;
        context.fillText(value, this.x - 5, thisY);
    }

    context.restore();
};
  1. 定义drawBars()方法,该方法循环遍历所有数据元素,并为每个数据元素绘制一个条形图:
BarChart.prototype.drawBars = function(){
    var context = this.context;
    context.save();
    var data = this.data;
    var barSpacing = this.width / data.length;
    var unitHeight = this.height / this.range;

    for (var n = 0; n < data.length; n++) {
        var bar = data[n];
        var barHeight = (data[n].value - this.minValue) * unitHeight;

        /*
         * if bar height is less than zero, this means that its
         * value is less than the min value.  Since we don't want to draw
         * bars below the x-axis, only draw bars whose height is greater
         * than zero
         */
        if (barHeight > 0) {
            context.save();
            context.translate(Math.round(this.x + ((n + 1 / 2) * barSpacing)), Math.round(this.y + this.height));
            /*
             * for convenience, we can draw the bars upside down
             * starting at the x-axis and then flip
             * them back into the correct orientation using
             * scale(1, -1).  This is a great example of how
             * transformations can help reduce computations
             */
            context.scale(1, -1);

            context.beginPath();
            context.rect(-this.barWidth / 2, 0, this.barWidth, barHeight);
            context.fillStyle = this.color;
            context.fill();
            context.restore();
        }
    }
    context.restore();
};
  1. 定义drawGridlines()方法,该方法在条形图上绘制水平网格线:
BarChart.prototype.drawGridlines = function(){
    var context = this.context;
    context.save();
    context.strokeStyle = this.gridColor;
    context.lineWidth = 2;

    // draw y axis grid lines
    for (var n = 0; n < this.numGridLines; n++) {
        var y = (n * this.height / this.numGridLines) + this.y;
        context.beginPath();
        context.moveTo(this.x, y);
        context.lineTo(this.x + this.width, y);
        context.stroke();
    }
    context.restore();
};
  1. 定义drawXAxis()方法,绘制 x 轴:
BarChart.prototype.drawXAxis = function(){
    var context = this.context;
    context.save();
    context.beginPath();
    context.moveTo(this.x, this.y + this.height);
    context.lineTo(this.x + this.width, this.y + this.height);
    context.strokeStyle = this.axisColor;
    context.lineWidth = 2;
    context.stroke();
    context.restore();
};
  1. 定义drawYAxis()方法,绘制 y 轴:
BarChart.prototype.drawYAxis = function(){
    var context = this.context;
    context.save();
    context.beginPath();
    context.moveTo(this.x, this.y);
    context.lineTo(this.x, this.height + this.y);
    context.strokeStyle = this.axisColor;
    context.lineWidth = 2;
    context.stroke();
    context.restore();
};
  1. 页面加载时,构建数据并实例化一个新的BarChart对象:
window.onload = function(){
    var data = [{
        label: "Eating",
        value: 2
    }, {
        label: "Working",
        value: 8
    }, {
        label: "Sleeping",
        value: 8
    }, {
        label: "Errands",
        value: 2
    }, {
        label: "Entertainment",
        value: 4
    }];

    new BarChart({
        canvasId: "myCanvas",
        data: data,
        color: "blue",
        barWidth: 50,
        minValue: 0,
        maxValue: 10,
        gridLineIncrement: 2
    });
};
  1. 将画布嵌入到 HTML 文档的 body 中:
<canvas id="myCanvas" width="600" height="300" style="border:1px solid black;">
</canvas>

它是如何工作的...

与饼图相比,条形图需要更多的配置才能真正通用。对于我们的BarChart类的实现,我们需要传入画布 id、数据元素数组、条形颜色、条形宽度、网格线增量(网格线之间的单位数)、最大值和最小值。BarChart构造函数使用六种方法来渲染条形图——drawGridlines()drawYAxis()drawXAxis()drawBars()drawYValues()drawXLabels()

BarChart类的关键是drawBars()方法,该方法遍历所有数据元素,然后为每个数据元素绘制一个矩形。绘制每个条形图的最简单方法是首先垂直反转上下文(使得 y 的正值向上而不是向下),将光标定位在 x 轴上,然后向下绘制一个高度等于数据元素值的矩形。由于上下文在垂直方向上被反转,条形图实际上会向上升起。

参见...

  • 在第一章中使用文本

  • 在第二章中绘制矩形

  • 在第四章中翻译画布上下文

  • 在第四章中旋转画布上下文

  • 在第四章中创建镜像变换

绘制方程

在这个示例中,我们将创建一个可配置的 Graph 类,该类绘制带有刻度线和值的 x 和 y 轴,然后我们将构建一个名为drawEquation()的方法,该方法允许我们绘制 f(x)函数。我们将实例化一个 Graph 对象,然后绘制正弦波、抛物线方程和线性方程。

绘制方程

如何做...

按照以下步骤创建一个可以绘制带有值的 x 和 y 轴,并且还可以绘制多个 f(x)方程的 Graph 类:

  1. 定义Graph类的构造函数,绘制 x 和 y 轴:
function Graph(config){
    // user defined properties
    this.canvas = document.getElementById(config.canvasId);
    this.minX = config.minX;
    this.minY = config.minY;
    this.maxX = config.maxX;
    this.maxY = config.maxY;
    this.unitsPerTick = config.unitsPerTick;
    // constants
    this.axisColor = "#aaa";
    this.font = "8pt Calibri";
    this.tickSize = 20;

    // relationships
    this.context = this.canvas.getContext("2d");
    this.rangeX = this.maxX - this.minX;
    this.rangeY = this.maxY - this.minY;
    this.unitX = this.canvas.width / this.rangeX;
    this.unitY = this.canvas.height / this.rangeY;
    this.centerY = Math.round(Math.abs(this.minY / this.rangeY) * this.canvas.height);
    this.centerX = Math.round(Math.abs(this.minX / this.rangeX) * this.canvas.width);
    this.iteration = (this.maxX - this.minX) / 1000;
    this.scaleX = this.canvas.width / this.rangeX;
    this.scaleY = this.canvas.height / this.rangeY;

    // draw x and y axis 
    this.drawXAxis();
    this.drawYAxis();
}
  1. 定义drawXAxis()方法,绘制 x 轴:
Graph.prototype.drawXAxis = function(){
    var context = this.context;
    context.save();
    context.beginPath();
    context.moveTo(0, this.centerY);
    context.lineTo(this.canvas.width, this.centerY);
    context.strokeStyle = this.axisColor;
    context.lineWidth = 2;
    context.stroke();

    // draw tick marks
    var xPosIncrement = this.unitsPerTick * this.unitX;
    var xPos, unit;
    context.font = this.font;
    context.textAlign = "center";
    context.textBaseline = "top";

    // draw left tick marks
    xPos = this.centerX - xPosIncrement;
    unit = -1 * this.unitsPerTick;
    while (xPos > 0) {
        context.moveTo(xPos, this.centerY - this.tickSize / 2);
        context.lineTo(xPos, this.centerY + this.tickSize / 2);
        context.stroke();
        context.fillText(unit, xPos, this.centerY + this.tickSize / 2 + 3);
        unit -= this.unitsPerTick;
        xPos = Math.round(xPos - xPosIncrement);
    }
    // draw right tick marks
    xPos = this.centerX + xPosIncrement;
    unit = this.unitsPerTick;
    while (xPos < this.canvas.width) {
        context.moveTo(xPos, this.centerY - this.tickSize / 2);
        context.lineTo(xPos, this.centerY + this.tickSize / 2);
        context.stroke();
        context.fillText(unit, xPos, this.centerY + this.tickSize / 2 + 3);
        unit += this.unitsPerTick;
        xPos = Math.round(xPos + xPosIncrement);
    }
    context.restore();
};
  1. 定义drawYAxis()方法,绘制 y 轴:
Graph.prototype.drawYAxis = function(){
    var context = this.context;
    context.save();
    context.beginPath();
    context.moveTo(this.centerX, 0);
    context.lineTo(this.centerX, this.canvas.height);
    context.strokeStyle = this.axisColor;
    context.lineWidth = 2;
    context.stroke();

    // draw tick marks  
    var yPosIncrement = this.unitsPerTick * this.unitY;
    var yPos, unit;
    context.font = this.font;
    context.textAlign = "right";
    context.textBaseline = "middle";

    // draw top tick marks
    yPos = this.centerY - yPosIncrement;
    unit = this.unitsPerTick;
    while (yPos > 0) {
        context.moveTo(this.centerX - this.tickSize / 2, yPos);
        context.lineTo(this.centerX + this.tickSize / 2, yPos);
        context.stroke();
        context.fillText(unit, this.centerX - this.tickSize / 2 - 3, yPos);
        unit += this.unitsPerTick;
        yPos = Math.round(yPos - yPosIncrement);
    }

    // draw bottom tick marks
    yPos = this.centerY + yPosIncrement;
    unit = -1 * this.unitsPerTick;
    while (yPos < this.canvas.height) {
        context.moveTo(this.centerX - this.tickSize / 2, yPos);
        context.lineTo(this.centerX + this.tickSize / 2, yPos);
        context.stroke();
        context.fillText(unit, this.centerX - this.tickSize / 2 - 3, yPos);
        unit -= this.unitsPerTick;
        yPos = Math.round(yPos + yPosIncrement);
    }
    context.restore();
};
  1. 定义drawEquation()方法,该方法接受一个函数 f(x),然后通过循环从minXmaxX的增量 x 值来绘制方程:
Graph.prototype.drawEquation = function(equation, color, thickness){
    var context = this.context;
    context.save();
    context.save();
    this.transformContext();

    context.beginPath();
    context.moveTo(this.minX, equation(this.minX));

    for (var x = this.minX + this.iteration; x <= this.maxX; x += this.iteration) {
        context.lineTo(x, equation(x));
    }

    context.restore();
    context.lineJoin = "round";
    context.lineWidth = thickness;
    context.strokeStyle = color;
    context.stroke();
    context.restore();
};
  1. 定义transformContext()方法,将上下文平移到图形中心,拉伸图形以适应画布,然后反转 y 轴:
Graph.prototype.transformContext = function(){
    var context = this.context;

    // move context to center of canvas
    this.context.translate(this.centerX, this.centerY);

    /*
     * stretch grid to fit the canvas window, and
     * invert the y scale so that increments
     * as you move upwards
     */
    context.scale(this.scaleX, -this.scaleY);
};
  1. 当页面加载时,实例化一个新的Graph对象,然后使用drawEquation()方法绘制三个方程:
window.onload = function(){
    var myGraph = new Graph({
        canvasId: "myCanvas",
        minX: -10,
        minY: -10,
        maxX: 10,
        maxY: 10,
        unitsPerTick: 1
    });

    myGraph.drawEquation(function(x){
        return 5 * Math.sin(x);
    }, "green", 3);

    myGraph.drawEquation(function(x){
        return x * x;
    }, "blue", 3);

    myGraph.drawEquation(function(x){
        return 1 * x;
    }, "red", 3);
};
  1. 将画布嵌入 HTML 文档的 body 中:
<canvas id="myCanvas" width="600" height="300" style="border:1px solid black;">
</canvas>

工作原理...

我们的Graph类只需要六个参数,canvasIdminXminYmaxXmaxYunitsPerTick。实例化后,它使用drawXAxis()方法绘制 x 轴,使用drawYAxis()方法绘制 y 轴。

Graph对象的真正亮点是drawEquation()方法,它接受一个方程 f(x)、一个线颜色和一个线粗细。虽然该方法相对较短(大约 20 行代码),但实际上非常强大。它的工作原理如下:

  1. 首先调用transformContext()方法,将画布上下文定位,缩放上下文以适应画布,并使用scale()方法通过将 y 分量乘以-1 来反转 y 轴。这样做是为了使绘图过程更简单,因为增加的 y 值将向上而不是向下(请记住,默认情况下,向下移动时 y 值增加)。

  2. 一旦画布上下文准备好,使用equation函数确定 x 等于minX时的 y 值,即 f(minX)。

  3. 使用moveTo()移动绘图光标。

  4. 通过for循环,轻微增加 x 值,并使用方程 f(x)确定每次迭代的相应 y 值。

  5. lineTo()从最后一个点画线到当前点。

  6. 继续循环,直到 x 等于maxX

由于每次迭代绘制的线条非常小,人眼看不见,从而产生平滑曲线的错觉。

当页面加载时,我们可以实例化一个新的Graph对象,然后通过调用drawEquation()方法绘制绿色正弦波、蓝色抛物线方程和红色线性方程。

另请参阅...

  • 在第一章中绘制一条线

  • 在第一章中处理文本

  • 在第四章中转换画布上下文

  • 在第四章中缩放画布上下文

  • 在第四章中创建镜像变换

用线图绘制数据点

如果你曾经上过科学课,你可能熟悉根据实验数据生成线图。线图可能是在传达数据趋势时最有用的数据可视化之一。在这个示例中,我们将创建一个可配置的 Line Chart 类,它接受一个数据元素数组,并绘制每个点,同时用线段连接这些点。

用线图绘制数据点

如何做...

按照以下步骤创建一个 Line Chart 类,可以自动定位和调整线图的大小,从一个数据数组中:

  1. 定义LineChart类的构造函数,绘制 x 轴和 y 轴:
function LineChart(config){
    // user defined properties
    this.canvas = document.getElementById(config.canvasId);
    this.minX = config.minX;
    this.minY = config.minY;
    this.maxX = config.maxX;
    this.maxY = config.maxY;
    this.unitsPerTickX = config.unitsPerTickX;
    this.unitsPerTickY = config.unitsPerTickY;

    // constants
    this.padding = 10;
    this.tickSize = 10;
    this.axisColor = "#555";
    this.pointRadius = 5;
    this.font = "12pt Calibri";
    /*
     * measureText does not provide a text height
     * metric, so we'll have to hardcode a text height
     * value
     */
    this.fontHeight = 12;
    // relationships  
    this.context = this.canvas.getContext("2d");
    this.rangeX = this.maxX - this.minY;
    this.rangeY = this.maxY - this.minY;
    this.numXTicks = Math.round(this.rangeX / this.unitsPerTickX);
    this.numYTicks = Math.round(this.rangeY / this.unitsPerTickY);
    this.x = this.getLongestValueWidth() + this.padding * 2;
    this.y = this.padding * 2;
    this.width = this.canvas.width - this.x - this.padding * 2;
    this.height = this.canvas.height - this.y - this.padding - this.fontHeight;
    this.scaleX = this.width / this.rangeX;
    this.scaleY = this.height / this.rangeY;

    // draw x y axis and tick marks
    this.drawXAxis();
    this.drawYAxis();
}
  1. 定义getLongestValueWidth()方法,返回最长值文本的像素长度:
LineChart.prototype.getLongestValueWidth = function(){
    this.context.font = this.font;
    var longestValueWidth = 0;
    for (var n = 0; n <= this.numYTicks; n++) {
        var value = this.maxY - (n * this.unitsPerTickY);
        longestValueWidth = Math.max(longestValueWidth, this.context.measureText(value).width);
    }
    return longestValueWidth;
};
  1. 定义drawXAxis()方法,绘制 x 轴和标签:
LineChart.prototype.drawXAxis = function(){
    var context = this.context;
    context.save();
    context.beginPath();
    context.moveTo(this.x, this.y + this.height);
    context.lineTo(this.x + this.width, this.y + this.height);
    context.strokeStyle = this.axisColor;
    context.lineWidth = 2;
    context.stroke();

    // draw tick marks
    for (var n = 0; n < this.numXTicks; n++) {
        context.beginPath();
        context.moveTo((n + 1) * this.width / this.numXTicks + this.x, this.y + this.height);
        context.lineTo((n + 1) * this.width / this.numXTicks + this.x, this.y + this.height - this.tickSize);
        context.stroke();
    }

    // draw labels
    context.font = this.font;
    context.fillStyle = "black";
    context.textAlign = "center";
    context.textBaseline = "middle";

    for (var n = 0; n < this.numXTicks; n++) {
        var label = Math.round((n + 1) * this.maxX / this.numXTicks);
        context.save();
        context.translate((n + 1) * this.width / this.numXTicks + this.x, this.y + this.height + this.padding);
        context.fillText(label, 0, 0);
        context.restore();
    }
    context.restore();
};
  1. 定义drawYAxis()方法,绘制 y 轴和值:
LineChart.prototype.drawYAxis = function(){
    var context = this.context;
    context.save();
    context.save();
    context.beginPath();
    context.moveTo(this.x, this.y);
    context.lineTo(this.x, this.y + this.height);
    context.strokeStyle = this.axisColor;
    context.lineWidth = 2;
    context.stroke();
    context.restore();
    // draw tick marks
    for (var n = 0; n < this.numYTicks; n++) {
        context.beginPath();
        context.moveTo(this.x, n * this.height / this.numYTicks + this.y);
        context.lineTo(this.x + this.tickSize, n * this.height / this.numYTicks + this.y);
        context.stroke();
    }

    // draw values
    context.font = this.font;
    context.fillStyle = "black";
    context.textAlign = "right";
    context.textBaseline = "middle";

    for (var n = 0; n < this.numYTicks; n++) {
        var value = Math.round(this.maxY - n * this.maxY / this.numYTicks);
        context.save();
        context.translate(this.x - this.padding, n * this.height / this.numYTicks + this.y);
        context.fillText(value, 0, 0);
        context.restore();
    }
    context.restore();
};
  1. 定义drawLine()方法,循环遍历数据点并绘制连接每个数据点的线段:
LineChart.prototype.drawLine = function(data, color, width){
    var context = this.context;
    context.save();
    this.transformContext();
    context.lineWidth = width;
    context.strokeStyle = color;
    context.fillStyle = color;
    context.beginPath();
    context.moveTo(data[0].x * this.scaleX, data[0].y * this.scaleY);

    for (var n = 0; n < data.length; n++) {
        var point = data[n];

        // draw segment
        context.lineTo(point.x * this.scaleX, point.y * this.scaleY);
        context.stroke();
        context.closePath();
        context.beginPath();
        context.arc(point.x * this.scaleX, point.y * this.scaleY, this.pointRadius, 0, 2 * Math.PI, false);
        context.fill();
        context.closePath();

        // position for next segment
        context.beginPath();
        context.moveTo(point.x * this.scaleX, point.y * this.scaleY);
    }
    context.restore();
};
  1. 定义transformContext()方法,平移上下文,然后垂直反转上下文:
LineChart.prototype.transformContext = function(){
    var context = this.context;

    // move context to center of canvas
    this.context.translate(this.x, this.y + this.height);

    // invert the y scale so that that increments
    // as you move upwards
    context.scale(1, -1);
};
  1. 页面加载时,实例化一个LineChart对象,为蓝线创建一个数据集,使用drawLine()绘制线条,为红线定义另一个数据集,然后绘制红线:
window.onload = function(){
    var myLineChart = new LineChart({
        canvasId: "myCanvas",
        minX: 0,
        minY: 0,
        maxX: 140,
        maxY: 100,
        unitsPerTickX: 10,
        unitsPerTickY: 10
    });
    var data = [{
        x: 0,
        y: 0
    }, {
        x: 20,
        y: 10
    }, {
        x: 40,
        y: 15
    }, {
        x: 60,
        y: 40
    }, {
        x: 80,
        y: 60
    }, {
        x: 100,
        y: 50
    }, {
        x: 120,
        y: 85
    }, {
        x: 140,
        y: 100
    }];

    myLineChart.drawLine(data, "blue", 3);

    var data = [{
        x: 20,
        y: 85
    }, {
        x: 40,
        y: 75
    }, {
        x: 60,
        y: 75
    }, {
        x: 80,
        y: 45
    }, {
        x: 100,
        y: 65
    }, {
        x: 120,
        y: 40
    }, {
        x: 140,
        y: 35
    }];

    myLineChart.drawLine(data, "red", 3);
};
  1. 将画布嵌入到 HTML 文档的主体中:
<canvas id="myCanvas" width="600" height="300" style="border:1px solid black;">
</canvas>

工作原理...

首先,我们需要使用七个属性配置LineChart对象,包括canvasIdminXminYmaxXmaxYunitsPerTickXunitsPerTickY。当LineChart对象被实例化时,我们将渲染 x 轴和 y 轴。

大部分有趣的事情发生在drawLine()方法中,它需要一个数据元素数组、线条颜色和线条粗细。它的工作原理如下:

  1. 使用transformContext()来平移、缩放和反转上下文。

  2. 使用moveTo()方法将绘图光标定位在数据数组中的第一个数据点处。

  3. 循环遍历所有数据元素,从上一个点到当前点画一条线,然后使用arc()方法在当前位置画一个小圆圈。

页面加载后,我们可以实例化LineChart对象,为蓝线创建一个数据点数组,使用drawLine()方法画线,为红线创建另一个数据点数组,然后画红线。

另请参阅...

  • 在第一章中画一条线

  • 在第一章中处理文本

  • 在第二章中画一个圆

  • 在第四章中翻译画布上下文

第八章:用游戏开发拯救世界

在本章中,我们将涵盖:

  • 为英雄和敌人创建精灵表

  • 创建关卡图像和边界地图

  • 为英雄和敌人创建一个 Actor 类

  • 创建一个关卡类

  • 创建一个生命条类

  • 创建一个控制器类

  • 创建一个模型类

  • 创建一个 View 类

  • 设置 HTML 文档并开始游戏

介绍

如果有人仅仅因为这一章而购买了这本书,我一点也不会感到惊讶——毕竟,掌握 HTML5 画布而不能创建自己的视频游戏有什么乐趣呢?在这本书中的所有章节中,这一章无疑是我最喜欢的(下一章是紧随其后的)。我们可能实际上无法通过游戏开发拯救世界,但创建我们自己的虚拟世界并拯救它们确实很有趣。在这一章中,我们将把我们新学到的知识整合起来,创建 Canvas Hero,一个以 Canvas Hero 为主角的横向卷轴动作游戏,他可以在一个充满邪恶坏人的未来世界中奔跑、跳跃、升空和出拳。以下是游戏的一些特点:

  • 英雄可以向左跑,向右跑,跳跃和出拳攻击

  • 关卡将看起来很未来

  • 关卡将充满四处奔跑寻找麻烦的敌人

  • 关卡将有一个前景图像,随着玩家的移动而向左和向右移动,并且还将有一个静止的背景图像以创建深度

  • 玩家可以跳得足够高,以跳过坏人并避免被出拳

  • 当玩家或敌人被击中时,它们会闪白色,以显示它们受到了伤害

  • 重力将始终作用于玩家

  • 玩家不能穿过地板,穿过墙壁,或者跳过天花板

  • 尽管英雄可以跳得很高,但关卡中会有策略性放置的升空舱,以给玩家垂直提升,使他能够到达高处的平台

  • 当玩家的健康值降至零或玩家掉入洞中时,游戏结束

  • 当所有坏人被打败时,玩家赢得游戏

这里有一些截图,让你了解游戏完成后会是什么样子:

介绍介绍介绍介绍

本章的前两个配方涵盖了为英雄和坏人创建精灵表以及关卡图像和边界地图图像的技术。接下来的三个配方涵盖了为英雄、坏人、关卡和生命条对象创建类的步骤。之后的配方涵盖了游戏的MVC模型视图控制器)架构,最后一个配方将涵盖 HTML 标记。让我们开始吧!

为英雄和敌人创建精灵表

精灵表是包含不同玩家和敌人不同动作的快照的图像文件。精灵表是与数十甚至数百个单独图像一起工作的替代方案,这些图像可能会影响初始加载时间,也会成为图形艺术家的噩梦。Canvas Hero 包含一个英雄的精灵表,一个坏人的精灵表,以及当英雄或坏人受到伤害时使用的一组白色精灵表。

准备好了...

在我们开始之前,值得注意的是,即使是最有才华的游戏艺术家也可能花费比编写游戏代码更多的时间来创建游戏图形,这是经常被忽视的事情。对于 Canvas Hero,我们可以通过从我最喜欢的精灵资源网站www.spriters-resource.com下载一些精灵来简化我们的生活,这是一个包含大量经典老式游戏的精灵表和关卡图像的免费网站。

如何做到这一点

一旦我们找到了适合英雄和坏家伙的精灵表,我们可以裁剪出所需的精灵,然后使用 Adobe Photoshop、Gimp 或其他一些图像编辑软件制作精灵表。这是完成的英雄精灵表:

操作方法

正如您所看到的,英雄的精灵表包含四种动作,站立、跳跃、奔跑和出拳(从上到下)。在创建精灵表时,重要的是所有精灵图像都适合于定义的精灵大小。对于 Canvas Hero,每个精灵图像都适合于 144 x 144 像素的正方形。我们还应确保每个精灵图像面向同一方向,因为我们可以在需要渲染面向另一个方向的精灵时,在程序上水平翻转这些图像。

同样,我们也可以使用相同的过程为坏家伙创建精灵表:

操作方法

您会注意到坏家伙的精灵表比英雄的精灵表简单得多,因为他们的动作局限于奔跑和战斗(他们从不站立或跳跃)。为了保持一致,我们也可以将坏家伙的精灵设为 144 x 144 像素。

创建级别图像和边界地图

现在我们已经为英雄和坏家伙准备好了精灵表,是时候为他们创建一个虚拟世界了。在 Canvas Hero 中,我们的虚拟世界将是一个单一的级别,随着玩家的移动而向左右移动,其中包括墙壁、天花板、地板、平台和洞。在这个配方中,我们将介绍制作级别图像以及边界地图图像的步骤,这些图像以图形方式包含有关级别边界的信息,并用不同颜色标识特殊区域。

操作方法...

要为 Canvas Hero 创建级别图像,我们可以使用从www.spriters-resource.com下载的一些预先构建的图形,并使用 Photoshop、Gimp 或您选择的其他图像编辑器添加新的平台、洞和悬浮器。为了保持级别的大小适中,我们可以创建一个 6944 x 600 像素的前景级别图像。900 x 600 像素的画布将作为级别的查看窗口。这是包含透明前景和几个悬浮器的级别部分的快照:

操作方法...

接下来,我们可以创建一个背景图像,以营造深度的错觉。这是 Canvas Hero 的完成背景图像:

操作方法...

这是前景和背景图像在一起的样子:

操作方法...

一旦我们完成了前景和背景图像,我们的下一步是创建边界地图。边界地图是一种图形方式,用于将玩家限制在某些区域内,并定义特殊区域。

要为 Canvas Hero 创建边界地图,我们可以从黑色背景开始,将其与级别图像叠加在一起,然后在演员可以自由奔跑的地方绘制品红色矩形,并添加青色矩形来表示悬浮区域。保持背景图像为纯色有助于减小边界地图图像的大小,并减少图像加载时间。以下图像是与前面图像对应的边界地图的一部分:

操作方法...

它是如何工作的...

为了更好地理解边界地图的工作原理,让我们走一遍玩家在从左到右穿过前面屏幕时的步骤。还要记住,玩家的 x,y 位置位于精灵图像的中心,大约与英雄的臀部齐平:

  • 从左边开始,注意品红色部分,RGB(255,0,255),边界地图非常薄(大约只有 10 像素左右)。这个区域对应于玩家可以在低悬的天花板上方的小空间内驻留。如果玩家在这个区域跳跃,他的垂直上升将被阻止。

  • 一旦英雄走过低悬的天花板,他就会来到一个升空舱。请注意,有足够的垂直品红色空间供他向上跳跃并进入青色的升空区域,RGB(0,255,255)。

  • 一旦玩家进入青色区域,他就会开始向上飘,直到他能到达屏幕中间的平台。

  • 当玩家站在平台上时,天花板就在他的头上,这阻止了他跳跃。

  • 玩家可以继续向右走,然后从平台上掉下来。

  • 一旦着陆,玩家可以跳入由青色矩形标识的第二个升空区域,这将使他跳到下一个平台上。

还有更多...

还有更多内容!

边界地图替代方案

如果您不想使用边界地图来定义级别边界,您可以考虑构建一个大的边界点数组,该数组定义了玩家可以驻留的空间区域。这种方法的缺点是,随着级别变得越来越大和复杂,数组的维护可能非常耗时。此外,这种方法可能会产生显着的性能开销,因为不断循环遍历数组并为每个动画帧执行边界计算。

级别图像替代方案

为了使本章尽可能简单,我们选择使用一个大图像创建级别。不幸的是,这个图像在加载游戏时是主要的瓶颈。尽管其他图像尺寸较小,包括边界地图,但级别图像约为 1.6 MB,可能需要几秒钟才能加载。如果您的级别很大,或者您只是想尽快加载游戏,您可能会考虑以下替代方案:

  • 懒加载器 - 懒加载器将根据玩家的位置请求级别的各个部分,因此只有可见的和周围的级别块被下载,而不是一次性下载整个级别图像。这种方法的好处是改进了初始加载时间,缺点是您必须以编程方式管理何时下载级别的哪些部分。

  • 平铺布局 - 平铺布局是由平铺图像构建的级别。换句话说,您可以创建小的平铺图像(例如 30 x 30 像素),用于构建地板、墙壁、天花板、升空舱等的纹理,然后使用这些图像来构建级别。这种方法的好处是几乎没有加载时间,因为图像非常小,缺点是级别可能开始看起来有点重复和乏味。

为英雄和敌人创建一个 Actor 类

现在我们已经设置好了所有的主要图像,准备好了,是时候进行有趣的部分了(至少在我看来),我们将使用 JavaScript 和 HTML5 画布为我们的虚拟世界注入生命。我们的首要任务是创建一个 Actor 类,其中包含英雄和坏人的属性和方法。换句话说,英雄和坏人都将是 Actor 类的实例。Actor 类将负责使用诸如moveRight()moveLeft()等方法指导演员,并负责通过使用精灵表对演员进行动画渲染。

如何做...

按照以下步骤创建一个 Actor 类,该类可用于实例化英雄或坏人:

  1. 定义Actor构造函数:
/*
 * Actor class should have no knowledge
 * of the Level or HealthBar classes to
 * keep it decoupled
 */
function Actor(config){
    this.controller = config.controller;
    this.normalSpriteSheet = config.normalSpriteSheet;
    this.hitSpriteSheet = config.hitSpriteSheet;
    this.x = config.x; // absolute x
    this.y = config.y; // absolute y
    this.playerSpeed = config.playerSpeed; // px / s
    this.motions = config.motions;
    this.startMotion = config.startMotion;
    this.facingRight = config.facingRight;
    this.moving = config.moving;
    this.spriteInterval = config.spriteInterval; // ms
    this.maxHealth = config.maxHealth;
    this.attackRange = config.attackRange;
    this.minAttackInterval = config.minAttackInterval;

    this.SPRITE_SIZE = 144;
    this.FADE_RATE = 1; // full fade in 1s
    this.spriteSheet = this.normalSpriteSheet;
    this.vx = 0;
    this.vy = 0;
    this.spriteSeq = 0;
    this.motion = this.startMotion;
    this.lastMotion = this.motion;
    this.airborne = false;
    this.attacking = false;
    this.canAttack = true;
    this.health = this.maxHealth;
    this.alive = true;
    this.opacity = 1;
    this.timeSinceLastSpriteFrame = 0;
}
  1. 定义attack()方法触发攻击:
Actor.prototype.attack = function(){
    this.attacking = true;
    this.canAttack = false;
    var that = this;
    setTimeout(function(){
       that.canAttack = true;
    }, this.minAttackInterval);
};
  1. 定义stop()方法停止演员移动:
Actor.prototype.stop = function(){
    this.moving = false;
};
  1. 定义isFacingRight()方法:
Actor.prototype.isFacingRight = function(){
    return this.facingRight;
};
  1. 定义moveRight()方法:
Actor.prototype.moveRight = function(){
    this.moving = true;
    this.facingRight = true;
};
  1. 定义moveLeft()方法:
Actor.prototype.moveLeft = function(){
    this.moving = true;
    this.facingRight = false;
};
  1. 定义jump()方法,触发角色跳跃:
Actor.prototype.jump = function(){
    if (!this.airborne) {
        this.airborne = true;
        this.vy = -1;
    }
};
  1. 定义draw()方法:
Actor.prototype.draw = function(pos){
    var context = this.controller.view.context;
    var sourceX = this.spriteSeq * this.SPRITE_SIZE;
    var sourceY = this.motion.index * this.SPRITE_SIZE;

    context.save();
    context.translate(pos.x, pos.y);

    if (this.facingRight) {
        context.translate(this.SPRITE_SIZE, 0);
        context.scale(-1, 1);
    }
    context.globalAlpha = this.opacity;
    context.drawImage(this.spriteSheet, sourceX, sourceY, this.SPRITE_SIZE, this.SPRITE_SIZE, 0, 0, this.SPRITE_SIZE, this.SPRITE_SIZE);
    context.restore();
};
  1. 定义fade()方法,当角色被击败时淡出:
Actor.prototype.fade = function(){
  var opacityChange = this.controller.anim.getTimeInterval() * this.FADE_RATE / 1000;
    this.opacity -= opacityChange;
    if (this.opacity < 0) {
        this.opacity = 0;
    }
};
  1. 定义updateSpriteMotion()方法:
Actor.prototype.updateSpriteMotion = function(){
  // if attack sequence has finished, set attacking = false
    if (this.attacking && this.spriteSeq == this.motion.numSprites - 1) {
        this.attacking = false;
    }

    if (this.attacking) {
        this.motion = this.motions.ATTACKING;
    }
    else {
        if (this.airborne) {
            this.motion = this.motions.AIRBORNE;
        }
        else {
            this.vy = 0;
            if (this.moving) {
                this.motion = this.motions.RUNNING;
            }
            else {
                this.motion = this.motions.STANDING;
            }
        }
    }
};
  1. 定义updateSpriteSeqNum()方法,递增或重置每个精灵间隔的精灵序列号:
Actor.prototype.updateSpriteSeqNum = function() {
    var anim = this.controller.anim;
    this.timeSinceLastSpriteFrame += anim.getTimeInterval();

    if (this.timeSinceLastSpriteFrame > this.spriteInterval) {
        if (this.spriteSeq < this.motion.numSprites - 1) {
            this.spriteSeq++;
        }
        else {
            if (this.motion.loop) {
                this.spriteSeq = 0;
            }
        }

        this.timeSinceLastSpriteFrame = 0;
    }

    if (this.motion != this.lastMotion) {
        this.spriteSeq = 0;
        this.lastMotion = this.motion;
    }
};
  1. 定义damage()方法,减少角色的健康值,并将精灵表设置为被击中的精灵表,导致角色在短暂的时间内闪烁白色:
Actor.prototype.damage = function(){
    this.health = this.health <= 0 ? 0 : this.health - 1;

    this.spriteSheet = this.hitSpriteSheet;
    var that = this;
    setTimeout(function(){
        that.spriteSheet = that.normalSpriteSheet;
    }, 200);
};
  1. 定义getCenter()方法,返回角色中心的位置:
Actor.prototype.getCenter = function(){
    return {
        x: Math.round(this.x) + this.SPRITE_SIZE / 2,
        y: Math.round(this.y) + this.SPRITE_SIZE / 2
    };
};

它是如何工作的...

Actor类的想法是创建一个可以用于实例化英雄和坏人的类。它包括控制角色的方法,如moveRight()moveLeft()jump()attack(),游戏引擎或人类玩家可以调用。游戏引擎将使用这些方法来控制坏人,人类玩家将使用这些方法通过键盘按键来控制英雄。

除了控件,Actor类还通过updateSpriteMotion()方法更新精灵动画,并通过updateSpriteSeqNum()方法递增或循环精灵序列号。

最后,draw()方法挑选出与角色动作对应的精灵图像,如果角色面向右侧,则水平翻转图像,然后使用画布上下文的drawImage()方法在屏幕上绘制角色。

另请参阅...

  • 在第三章中裁剪图像

  • 在第四章中翻译画布上下文

  • 在第四章中创建镜像变换

创建一个 Level 类

在这个示例中,我们将创建一个 Level 类,用于渲染关卡并提供对边界地图的 API。

如何做...

按照以下步骤创建一个 Level 类:

  1. 定义Level构造函数:
/*
 * Level class should have no knowledge
 * of the Actor or HealthBar classes to
 * keep it decoupled
 */
function Level(config){
  this.controller = config.controller;
    this.x = config.x;
    this.y = config.y;
    this.leftBounds = config.leftBounds;
    this.rightBounds = config.rightBounds;
  this.boundsData = null;
    this.GRAVITY = 3; // px / second²
    this.MID_RGB_COMPONENT_VALUE = 128; 
    this.LEVEL_WIDTH = 6944;

    this.setBoundsData();
}
  1. 定义setBoundsData()方法,从边界地图图像中提取区域数据:
Level.prototype.setBoundsData = function(){
  var controller = this.controller;
  var canvas = controller.view.canvas;
  var context = controller.view.context;
    canvas.width = 6944;
    context.drawImage(controller.images.levelBounds, 0, 0);
    imageData = context.getImageData(0, 0, 6944, 600);
    this.boundsData = imageData.data;
    canvas.width = 900;
};
  1. 定义draw()方法,绘制背景图像和关卡图像:
Level.prototype.draw = function(){
  var context = this.controller.view.context;
    context.drawImage(this.controller.images.background, 0, 0);
    context.drawImage(this.controller.images.level, this.x, this.y);
};
  1. 定义getZoneInfo()方法,返回边界地图中某一点的区域信息:
Level.prototype.getZoneInfo = function(pos){
  var x = pos.x;
  var y = pos.y;
    var red = this.boundsData[((this.LEVEL_WIDTH * y) + x) * 4];
    var green = this.boundsData[((this.LEVEL_WIDTH * y) + x) * 4 + 1];
    var blue = this.boundsData[((this.LEVEL_WIDTH * y) + x) * 4 + 2];

    var inBounds = false;
    var levitating = false;

    /*
     * COLOR KEY
     *
     * PINK: 255 0   255
     * CYAN: 0   255 255
     *
     * COLOR NOTATION
     *
     * PINK: player is in bounds and can jump
     * CYAN: player is in bounds and is levitating
     */
  var mid = this.MID_RGB_COMPONENT_VALUE;
    if ((red > mid && green < mid && blue > mid) || (red < mid && green > mid && blue > mid)) {
        inBounds = true;
    }
    if (red < mid && green > mid && blue > mid) {
        levitating = true;
    }

    return {
        inBounds: inBounds,
        levitating: levitating
    };
};

它是如何工作的...

Level类中的大部分工作是在setBoundsData()方法和getZoneInfo()方法中完成的。setBoundsData()方法将边界地图图像转换为像素数据数组,使用画布上下文的getImageData()方法。getZoneInfo()方法用于访问边界地图中的点,然后返回相应的区域信息。

对于 Canvas Hero,区域信息对象包含两个标志:inBoundslevitating。如果边界地图中对应的像素为青色,则该点对应于一个在边界内且也在悬浮区域内的区域。如果边界地图中对应的像素为品红色,则该点对应于一个在边界内但不在悬浮区域内的区域。最后,如果边界地图中对应的像素为黑色,则意味着该点不在边界内或悬浮区域内。

另请参阅...

  • 在第三章中绘制图像

  • 在第三章中获取图像数据

创建一个 Health Bar 类

在这个示例中,我们将创建一个 Health Bar 类,用于更新和渲染英雄的健康显示。

如何做...

按照以下步骤创建一个健康条类:

  1. 定义HealthBar构造函数:
/*
 * HealthBar class should have no knowledge
 * of the Actor or Level classes to
 * keep it decoupled
 */
function HealthBar(config){
  this.controller = config.controller;
    this.maxHealth = config.maxHealth;
    this.x = config.x;
    this.y = config.y;
    this.maxWidth = config.maxWidth;
    this.height = config.height;

    this.health = this.maxHealth;
}
  1. 定义setHealth()方法,设置健康值:
HealthBar.prototype.setHealth = function(health){
    this.health = health;
};
  1. 定义draw()方法,绘制健康条:
HealthBar.prototype.draw = function(){
  var context = this.controller.view.context;
    context.beginPath();
    context.rect(this.x, this.y, this.maxWidth, this.height);
    context.fillStyle = "black";
    context.fill();
    context.closePath();

    context.beginPath();
    var width = this.maxWidth * this.health / this.maxHealth;
    context.rect(this.x, this.y, width, this.eight);
    context.fillStyle = "red";
    context.fill();
    context.closePath();
};

它是如何工作的...

HealthBar对象有一个简单的构造函数,初始化了血条的位置和大小,并包含两个方法,setHealth()draw()setHealth()方法设置HealthBar对象的health属性,draw()方法使用画布上下文的rect()方法绘制血条。

创建一个控制器类

现在我们已经拥有游戏中对象的所有图像和类,我们下一个任务是构建游戏引擎。Canvas Hero 采用标准的 MVC 架构构建,将数据、呈现和控制方法分离。在这个示例中,我们将创建一个控制器类,负责实例化模型和视图,初始化游戏,控制游戏状态和管理键盘事件。

如何做...

按照以下步骤为 Canvas Hero 创建控制器:

  1. 定义Controller构造函数:
/*
 * Game controller
 * 
 * The controller is responsible for instantiating
 * the view and the model, initializing the game,
 * controlling the game state, and managing keyboard events
 */
function Controller(canvasId){
    this.imageSources = {
        levelBounds: "img/level_bounds.png",
        level: "img/level.png",
        heroSprites: "img/hero_sprites.png",
        heroHitSprites: "img/hero_hit_sprites.png",
        badGuySprites: "img/bad_guy_sprites.png",
        badGuyHitSprites: "img/bad_guy_hit_sprites.png",
        background: "img/background.png",
        readyScreen: "img/readyScreen.png",
        gameoverScreen: "img/gameoverScreen.png",
        winScreen: "img/winScreen.png"
    };
    this.images = {};

    this.states = {
        INIT: "INIT",
        READY: "READY",
        PLAYING: "PLAYING",
        WON: "WON",
        GAMEOVER: "GAMEOVER"
    };

  this.keys = {
    ENTER: 13,
    UP: 38,
    LEFT: 37,
    RIGHT: 39,
    A: 65 
  };

  this.anim = new Animation(canvasId);
    this.state = this.states.INIT;
    this.model = new Model(this);
    this.view = new View(this);
  this.avgFps = 0;
  this.leftKeyup = true;
  this.rightKeyup = true;
    this.addKeyboardListeners();
    this.loadImages();
}
  1. 定义loadImages()方法,加载所有游戏图像,然后在它们全部加载完毕时调用initGame()
Controller.prototype.loadImages = function(){
  /*
   * we need to load the loading image first
   * so go ahead and insert it into the dom
   * and them load the rest of the images
   */
  this.view.canvas.style.background = "url('img/loadingScreen.png')";

    var that = this;
    var loadedImages = 0;
    var numImages = 0;
    for (var src in this.imageSources) {
        numImages++;
    }
    for (var src in this.imageSources) {
        this.images[src] = new Image();
        this.images[src].onload = function(){
            if (++loadedImages >= numImages) {
                that.initGame();
            }
        };
        this.images[src].src = this.imageSources[src];
    }
};
  1. 定义addKeyboardListeners()方法,将键盘事件监听器附加到游戏上:
Controller.prototype.addKeyboardListeners = function(){
    var that = this;
    document.onkeydown = function(evt){
        that.handleKeydown(evt);
    };
    document.onkeyup = function(evt){
        that.handleKeyup(evt);
    };
};
  1. 定义handleKeyUp()方法,当释放键时触发:
Controller.prototype.handleKeyup = function(evt){
    keycode = ((evt.which) || (evt.keyCode));

    switch (keycode) {
        case this.keys.LEFT: 
            this.leftKeyup = true;
            if (this.leftKeyup && this.rightKeyup) {
                this.model.hero.stop();
            }
            break;

        case this.keys.UP: 
            break;

        case this.keys.RIGHT: 
            this.rightKeyup = true;
            if (this.leftKeyup && this.rightKeyup) {
                this.model.hero.stop();
            }
            break;
    }
};
  1. 定义handleKeyDown()方法,当按键按下时触发:
Controller.prototype.handleKeydown = function(evt){
    var that = this;
    keycode = ((evt.which) || (evt.keyCode));
    switch (keycode) {
        case this.keys.ENTER: // enter
            if (this.state == this.states.READY) {
                this.state = this.states.PLAYING;
                // start animation
                this.anim.start();
            }
            else if (this.state == this.states.GAMEOVER || this.state == this.states.WON) {
                this.resetGame();
                this.state = this.states.PLAYING;
            }
            break;
        case this.keys.LEFT: 
            this.leftKeyup = false;
            this.model.hero.moveLeft();
            break;

        case this.keys.UP: 
            this.model.hero.jump();
            break;

        case this.keys.RIGHT: 
            this.rightKeyup = false;
            this.model.hero.moveRight();
            break;

        case this.keys.A: // attack
          var model = this.model;
      var hero = model.hero; 
            hero.attack();
            setTimeout(function(){
                for (var n = 0; n < model.badGuys.length; n++) {
                    (function(){
                        var badGuy = model.badGuys[n];
                        if (model.nearby(hero, badGuy)
              && ((badGuy.x - hero.x > 0 && hero.isFacingRight()) || (hero.x - badGuy.x > 0 && !hero.isFacingRight()))) {
                            badGuy.damage();
                        }
                    })();
                }
            }, 200);
            break;
    }
};
  1. 定义initGame()方法,初始化游戏:
Controller.prototype.initGame = function(){
  var model = this.model;
  var view = this.view;
    model.initLevel();
    model.initHero();
    model.initBadGuys();
    model.initHealthBar();

    // set stage method
    this.anim.setStage(function(){
        model.updateStage();
        view.stage();
    });

    // game is now ready to play
    this.state = this.states.READY;
    view.drawScreen(this.images.readyScreen);
};
  1. 定义resetGame()方法,通过重新初始化游戏对象重置游戏:
Controller.prototype.resetGame = function(){
    var model = this.model;
    model.level = null;
    model.hero = null;
    model.healthBar = null;
    model.badGuys = [];

    model.initLevel();
    model.initHero();
    model.initBadGuys();
    model.initHealthBar();
};

工作原理...

游戏控制器最重要的作用是通过游戏状态控制游戏的流程。在 Canvas Hero 中,第一个游戏状态是加载状态。这是玩家可以在游戏加载时阅读游戏玩法的状态。一旦游戏加载完成,控制器负责将游戏状态更改为准备状态。在这个状态下,游戏等待用户按 Enter 键继续。一旦用户按下 Enter 键,控制器现在将游戏状态更改为游戏状态。

此刻,实际游戏开始,用户完全控制英雄。如果玩家的健康值降到零,或者玩家掉入洞中,控制器将把游戏状态更改为游戏结束状态。另一方面,如果玩家成功击败所有敌人,控制器将把游戏状态更改为胜利状态,并祝贺英雄取得了惊人的成就。看一下以下状态机:

工作原理...

除了控制游戏状态,控制器还负责管理键盘事件。键盘事件通过addKeyboardListeners()方法附加。

创建一个模型类

在这个示例中,我们将创建一个模型类,负责初始化和更新英雄、坏人、关卡和血条。这些对象可以被视为游戏的“数据”。

如何做...

按照以下步骤为 Canvas Hero 创建模型:

  1. 定义Model构造函数:
/*
 * Game model
 * 
 * The model is responsible for initializing and
 * updating the hero, level, bad guys, and health bar
 */
function Model(controller){
    this.controller = controller;
    this.healthBar = null;
    this.hero = null;
    this.level = null;
    this.badGuys = []; // array of bad guys
    this.heroCanvasPos = {};
}
  1. 定义removeDefeatedBadGuys()方法,循环遍历坏人数组,然后移除已经死亡的坏人:
Model.prototype.removeDefeatedBadGuys = function(){
    for (var n = 0; n < this.badGuys.length; n++) {
        var badGuy = this.badGuys[n];
        if (!badGuy.alive && badGuy.opacity == 0) {
            this.badGuys.splice(n, 1);
        }
    }
};
  1. 定义updateBadGuys()方法:
Model.prototype.updateBadGuys = function(){
    var that = this;
    for (var n = 0; n < this.badGuys.length; n++) {
        var badGuy = this.badGuys[n];
        if (badGuy.alive
      && this.hero.alive
      && !badGuy.attacking
      && badGuy.canAttack 
      && this.nearby(this.hero, badGuy)
      && ((badGuy.x - this.hero.x > 0 && !badGuy.isFacingRight()) || (this.hero.x - badGuy.x > 0 && badGuy.isFacingRight()))) {
      badGuy.attack();
            setTimeout(function(){
                that.hero.damage();
            }, 200);
        }
        this.updateActor(badGuy);
    }
};
  1. 定义updateStage()方法,更新每个动画帧的所有游戏对象:
Model.prototype.updateStage = function(){
    var controller = this.controller;
    var canvas = controller.view.canvas;
    if (controller.state == controller.states.PLAYING) {
        this.removeDefeatedBadGuys();

        // if hero dies then set state to GAMEOVER
        if (!this.hero.alive && controller.state == controller.states.PLAYING) {
            controller.state = controller.states.GAMEOVER;
        }

        // if all bad guys defeated, change state to WON
        if (this.badGuys.length == 0) {
            controller.state = controller.states.WON;
        }

        // move bad guys around
        this.moveBadGuys();

        // update level position
        this.updateLevel();

    /*
     * update bad guys and also see
     * if they can attack the hero
     */
        this.updateBadGuys();

        // update hero
        var oldHeroX = this.hero.x;
        this.updateActor(this.hero);
        this.updateHeroCanvasPos(oldHeroX);
        // update health bar
        this.healthBar.setHealth(this.hero.health);

        // if hero falls into a hole set health to zero
        if (this.hero.y > canvas.height - this.hero.spriteSize * 2 / 3) {
            this.hero.health = 0;
        }

        // update avg fps
        var anim = controller.anim;
        if (anim.getFrame() % 20 == 0) {
            this.controller.avgFps = Math.round(anim.getFps() * 10) / 10;
        }
    }
};
  1. 定义initHealthBar()方法,初始化血条:
Model.prototype.initHealthBar = function(){
    this.healthBar = new HealthBar({
        controller: this.controller,
        maxHealth: this.hero.maxHealth,
        x: 10,
        y: 10,
        maxWidth: 150,
        height: 20
    });
};
  1. 定义initLevel()方法,初始化关卡:
Model.prototype.initLevel = function(){
    this.level = new Level({
        controller: this.controller,
        x: 0,
        y: 0,
        leftBounds: 100,
        rightBounds: 500
    });
};
  1. 定义initHero()方法,初始化英雄:
	Model.prototype.initHero = function(){
    // initialize Hero
    var heroMotions = {
        STANDING: {
            index: 0,
            numSprites: 5,
            loop: true
        },
        AIRBORNE: {
            index: 1,
            numSprites: 5,
            loop: false
        },
        RUNNING: {
            index: 2,
            numSprites: 6,
            loop: true
        },
        ATTACKING: {
            index: 3,
            numSprites: 5,
            loop: false
        }
    };

    this.hero = new Actor({
        controller: this.controller,
        normalSpriteSheet: this.controller.images.heroSprites,
        hitSpriteSheet: this.controller.images.heroHitSprites,
        x: 30,
        y: 381,
        playerSpeed: 300,
        motions: heroMotions,
        startMotion: heroMotions.STANDING,
        facingRight: true,
        moving: false,
        spriteInterval: 90,
        maxHealth: 3,
        attackRange: 100,
        minAttackInterval: 200
    });

    this.heroCanvasPos = {
        x: this.hero.x,
        y: this.hero.y
    };
};
  1. 定义initBadGuys()方法,初始化一个坏人数组:
Model.prototype.initBadGuys = function(){
    // notice that AIRBORNE and RUNNING
    // both use the same sprite animation
    var badGuyMotions = {
        RUNNING: {
            index: 0,
            numSprites: 6,
            loop: true
        },
        AIRBORNE: {
            index: 0,
            numSprites: 4,
            loop: false
        },
        ATTACKING: {
            index: 1,
            numSprites: 4,
            loop: false
        }
    };

    var badGuyStartConfig = [{
        x: 600,
        facingRight: true
    }, {
        x: 1460,
        facingRight: true
    }, {
        x: 2602,
        facingRight: true
    }, {
        x: 3000,
        facingRight: true
    }, {
        x: 6402,
        facingRight: true
    }, {
        x: 6602,
        facingRight: true
    }];

    for (var n = 0; n < badGuyStartConfig.length; n++) {
        this.badGuys.push(new Actor({
            controller: this.controller,
            normalSpriteSheet: this.controller.images.badGuySprites,
            hitSpriteSheet: this.controller.images.badGuyHitSprites,
            x: badGuyStartConfig[n].x,
            y: 381,
            playerSpeed: 100,
            motions: badGuyMotions,
            startMotion: badGuyMotions.RUNNING,
            facingRight: badGuyStartConfig[n].facingRight,
            moving: true,
            spriteInterval: 160,
            maxHealth: 3,
            attackRange: 100,
            minAttackInterval: 2000
        }));
    }
};
  1. 定义moveBadGuys()方法,作为简单的 AI 引擎:
Model.prototype.moveBadGuys = function(){
    var level = this.level;
    for (var n = 0; n < this.badGuys.length; n++) {
        var badGuy = this.badGuys[n];

        if (badGuy.alive) {
            if (badGuy.isFacingRight()) {
                badGuy.x += 5;
                if (!level.getZoneInfo(badGuy.getCenter()).inBounds) {
                    badGuy.facingRight = false;
                }
                badGuy.x -= 5;
            }

            else {
                badGuy.x -= 5;
                if (!level.getZoneInfo(badGuy.getCenter()).inBounds) {
                    badGuy.facingRight = true;
                }
                badGuy.x += 5;
            }
        }
    }
};
  1. 定义updateLevel()方法:
Model.prototype.updateLevel = function(){
    var hero = this.hero;
    var level = this.level;
    level.x = -hero.x + this.heroCanvasPos.x;
};
  1. 定义updateHeroCanvasPos()方法,更新英雄相对于画布的位置:
Model.prototype.updateHeroCanvasPos = function(oldHeroX){
    this.heroCanvasPos.y = this.hero.y;
    var heroDiffX = this.hero.x - oldHeroX;
    var newHeroCanvasPosX = this.heroCanvasPos.x + heroDiffX;
    // if moving right and not past right bounds
    if (heroDiffX > 0 && newHeroCanvasPosX < this.level.rightBounds) {
        this.heroCanvasPos.x += heroDiffX;
    }
    // if moving left and not past left bounds
    if (heroDiffX < 0 && newHeroCanvasPosX > this.level.leftBounds) {
        this.heroCanvasPos.x += heroDiffX;
    }

  if (this.hero.x < this.level.leftBounds) {
    this.heroCanvasPos.x = this.hero.x;
  }
};
  1. 定义updateActor()方法:
Model.prototype.updateActor = function(actor){
    if (actor.alive) {
        if (actor.health <= 0 || actor.y + actor.SPRITE_SIZE > this.controller.view.canvas.height) {
            actor.alive = false;
        }
        else {
      this.updateActorVY(actor);            
      this.updateActorY(actor);
      this.updateActorX(actor);

            actor.updateSpriteMotion();
      actor.updateSpriteSeqNum();
        }
    }
    else {
        if (actor.opacity > 0) {
            actor.fade();
        }
    }
};
  1. 定义updateActorVY()方法,使用重力的向下力和升力舱的向上力来更新角色的垂直速度:
Model.prototype.updateActorVY = function(actor) {
  var anim = this.controller.anim;
  var level = this.level;

    // apply gravity (+y)
    var gravity = this.controller.model.level.GRAVITY;
    var speedIncrementEachFrame = gravity * anim.getTimeInterval() / 1000; // pixels / second
    actor.vy += speedIncrementEachFrame;        

    // apply levitation (-y)
    if (level.getZoneInfo(actor.getCenter()).levitating) {
        actor.vy = (65 - actor.y) / 200;
    }
};
  1. 定义updateActorY()方法,根据角色的垂直速度更新角色的 y 位置:
Model.prototype.updateActorY = function(actor) {
  var anim = this.controller.anim;
  var level = this.level;
    var oldY = actor.y;
    actor.y += actor.vy * anim.getTimeInterval();

    if (level.getZoneInfo(actor.getCenter()).inBounds) {
        actor.airborne = true;
    }
    else {
        actor.y = oldY;

        // handle case where player has fallen to the ground
        // if vy is less than zero, this means the player has just
        // hit the ceiling, in which case we can simply leave
        // this.y as oldY to prevent the player from going
        // past the ceiling
        if (actor.vy > 0) {
            while (level.getZoneInfo(actor.getCenter()).inBounds){
                actor.y++;
            }
            actor.y--;
            actor.vy = 0;
            actor.airborne = false;
        }
    }
};
  1. 定义updateActorX()方法,更新角色的 x 位置:
Model.prototype.updateActorX = function(actor) {
  var anim = this.controller.anim;
  var level = this.level;
  var oldX = actor.x;
  var changeX = actor.playerSpeed * (anim.getTimeInterval() / 1000);
    if (actor.moving) {
        actor.facingRight ? actor.x += changeX : actor.x -= changeX;
    }

    if (!level.getZoneInfo(actor.getCenter()).inBounds) {
        actor.x = oldX;

        while (level.getZoneInfo(actor.getCenter()).inBounds) {
            actor.facingRight ? actor.x++ : actor.x--;
        }

        // reposition to nearest placement in bounds
        actor.facingRight ? actor.x-- : actor.x++;
    }
};
  1. 定义nearby()方法,确定两个角色是否彼此靠近:
Model.prototype.nearby = function(actor1, actor2){
    return (Math.abs(actor1.x - actor2.x) < actor1.attackRange)
    && Math.abs(actor1.y - actor2.y) < 30;
};

它是如何工作的...

在 MVC 架构中,模型被认为是架构的“核心”,因为它代表数据层。由于 Canvas Hero 是一个游戏,我们的数据包括英雄、坏家伙、级别和血条对象。这些对象中的每一个都包含必须在每个动画帧期间更新和访问的属性。

Canvas Hero 的模型有三个关键责任:

  • 初始化游戏对象

  • 更新游戏对象

  • 处理坏家伙的人工智能

在我们的模型中,可以说最有趣的方法是moveBadGuys()方法。这个方法可以被认为是我们游戏引擎的“人工智能”。我加了引号是因为说实话,在 Canvas Hero 中,坏家伙们相当愚蠢。moveBadGuys()方法循环遍历所有坏家伙对象,使用Level对象的getZoneInfo()方法确定它们是否靠近墙壁,然后在它们即将撞到墙壁时改变它们的方向。

还有更多...

如果你想创建一个更具挑战性的游戏,你可以考虑通过让坏家伙们具有跳跃或使用升空舱的能力来加强moveBadGuys()方法。

另请参阅...

  • 在第五章中创建一个动画类

创建一个 View 类

在这个示例中,我们将创建 View 类,这是三个 MVC 类中最简单的一个。View 类负责绘制状态屏幕图像,并通过调用draw()方法为每个级别、每个坏家伙、英雄和血条渲染每个动画帧。此外,View 类还在屏幕右上角渲染一个方便的 FPS 显示,以便我们可以看到游戏的表现如何。

如何做...

按照以下步骤为 Canvas Hero 创建视图:

  1. 定义View构造函数:
/*
 * Game view
 * 
 * The view has access to the canvas context
 * and is responsible for the drawing logic
 */
function View(controller){
    this.controller = controller;
    this.canvas = controller.anim.getCanvas();
    this.context = controller.anim.getContext();
}
  1. 定义drawScreen()方法,绘制加载、就绪、游戏结束或胜利状态屏幕:
View.prototype.drawScreen = function(screenImg){
    this.context.drawImage(screenImg, 0, 0, this.canvas.width, this.canvas.height);
};
  1. 定义drawBadGuys()方法,绘制坏家伙们:
View.prototype.drawBadGuys = function() {
    var controller = this.controller;
    var model = controller.model;
  for (var n = 0; n < model.badGuys.length; n++) {
      var badGuy = model.badGuys[n];
    var offsetPos = {
      x: badGuy.x + model.level.x,
      y: badGuy.y + model.level.y
    };
      badGuy.draw(offsetPos);
  }
};
  1. 定义drawFps()方法,绘制游戏右上角的 FPS 值,以便我们可以看到游戏的表现如何:
View.prototype.drawFps = function() {
    var context = this.context;
    context.fillStyle = "black";
    context.fillRect(this.canvas.width - 100, 0, 100, 30);
    context.font = "18pt Calibri";
    context.fillStyle = "white";
    context.fillText("fps: " + this.cntroller.avgFps.toFixed(1), this.canvas.width - 93, 22);
};
  1. 定义stage()方法,绘制屏幕上的所有对象:
View.prototype.stage = function(){
    var controller = this.controller;
    var model = controller.model;
    if (controller.state == controller.states.PLAYING || controller.state == controller.states.GAMEOVER || controller.state == controller.states.WON) {
        model.level.draw();
    this.drawBadGuys();
        model.hero.draw(model.heroCanvasPos);
        model.healthBar.draw();

        // draw screen overlay
        if (controller.state == controller.states.GAMEOVER) {
            this.drawScreen(controller.images.gameoverScreen);
        }
        else if (controller.state == controller.states.WON) {
            this.drawScreen(controller.images.winScreen);
        }

        this.drawFps();
    }
    else if (controller.state == controller.states.READY) {
        this.drawScreen(controller.images.readyScreen);
    }
};

它是如何工作的...

如前所述,View类的主要责任是绘制状态屏幕和游戏屏幕。Canvas Hero 有四个不同的状态屏幕:

  • 加载状态

  • 就绪状态

  • 游戏结束状态

  • 胜利状态

每当游戏状态改变并且需要状态屏幕时,控制器调用View对象的drawScreen()方法。以下是每个游戏状态屏幕的截图:

加载状态:

它是如何工作的...

就绪状态:

它是如何工作的...

游戏结束状态:

它是如何工作的...

胜利状态:

它是如何工作的...

另请参阅...

  • 强调画布并显示 FPS在第五章中

设置 HTML 文档并开始游戏

现在我们已经拥有游戏的所有部分,包括图形、角色类、级别、血条和一个完整的游戏引擎,是时候通过设置 HTML 文档并开始游戏来将它们全部联系在一起了。

如何做...

按照以下步骤设置 HTML 文档并开始游戏:

  1. 链接到 JavaScript 文件:
</style>
<script src="img/animation.js">
</script>
<script src="img/Controller.js">
</script>
<script src="img/Model.js">
</script>
<script src="img/View.js">
</script>
<script src="img/Level.js">
</script>
<script src="img/Actor.js">
</script>
<script src="img/HealthBar.js">
</script>
  1. 初始化控制器:
<script>
    window.onload = function(){
        new Controller("myCanvas");
    };
</script>
  1. 将画布嵌入 HTML 文档的主体内:
<canvas id="myCanvas" width="900" height="600">
</canvas>

它是如何工作的...

正如您所看到的,HTML 标记非常简单。它的目的纯粹是链接到所需的 JavaScript 文件,嵌入画布标记,并初始化控制器。控制器初始化模型和视图。模型初始化英雄、坏人、关卡和生命条。一旦图像加载完成,游戏状态改变为准备状态,玩家按下回车键游戏开始。

还有更多...

您现在已经准备好玩游戏并拯救世界了!如果您按照模型配方中定义的方式初始化了英雄和坏人的健康值为三个单位,那么英雄在游戏结束之前最多可以承受三次打击,每个坏人需要三次打击才能被击败。我发现最容易击败坏人的方法是跳过他们并反复击打他们的背部,直到他们被击败(我知道这很便宜,但它有效)。跳进升降舱并在空中飘浮一段时间,等待合适的时机,像忍者一样从上方袭击坏人也非常有趣。

如果您将本章作为自己的横向卷轴游戏的基础,这里有一些其他功能可以考虑添加:

  • 使用 HTML5 音频标记进行跳跃、着陆和拳击的音效

  • 暂停功能,可以冻结游戏直到恢复

  • 计时器和最高分

  • 更多关卡、敌人和 boss

  • 增强道具

  • 使用 HTML5 本地存储保存游戏状态或通过在在线数据库中保存状态的选项

  • 您可以想象的其他任何东西

另请参阅...

  • 在第五章中创建一个动画类

第九章:介绍 WebGL

在本章中,我们将涵盖:

  • 创建一个简化 WebGL API 的 WebGL 包装器

  • 创建一个三角形平面

  • 在 3D 空间中旋转一个三角形平面

  • 创建一个旋转的立方体

  • 添加纹理和光照

  • 创建一个可以探索的 3D 世界

介绍

最初,当我开始写这本书时,我原本打算只涵盖 HTML5 画布的 2D 上下文(我坚信大多数使用画布的人都会使用这个上下文)。我也原本打算覆盖在 2D 上下文中使用 3D 投影方法和矢量运算渲染 3D 形状的技术。人们已经忙着为 2D 上下文创建一些非常令人难以置信的 3D JavaScript 库,包括 Kevin Roast 的 K3D 库(本书的审阅者之一),以及 Dean McNamee 的 Pre3d 库。

当我接近写这一章时,WebGL——一个真正的 3D 上下文——开始在 Web 上主导 3D 画布演示。WebGL 代表基于 Web 的图形库,它基于 OpenGL ES 2.0,提供了一个用于 3D 图形的 API。因为 WebGL 通过直接将缓冲区推送到图形卡上来渲染 3D 模型,利用硬件加速,它的性能要比 2D 上下文、3D 投影库的对手好得多。此外,它还暴露了 OpenGL 已经完成的多年工作。正如你现在可能已经想到的那样,我决定覆盖 WebGL,而不是覆盖 2D 上下文的 3D 投影库,因为我非常相信 WebGL 将成为不久的将来 3D 应用的标准。WebGL 对于想要在 Web 上创建 3D 游戏或 3D 模型的人来说特别有趣。

本章将通过涵盖缓冲区、着色器、透视和模型视图矩阵、法线、纹理、光照、摄像机处理等概念,让你开始学习 WebGL 的基础知识。让我们开始吧!

创建一个简化 WebGL API 的 WebGL 包装器

如果你已经提前查看了这个食谱的代码,并且对 OpenGL 或 WebGL 不是很熟悉,你可能会感到非常不知所措,这是有充分理由的。尽管 WebGL 非常强大,但初次接触时学习曲线相当陡峭。坦率地说,执行简单任务需要很多行代码。因此,我发现使用 WebGL 包装器非常方便,它可以将繁琐的代码块简化为简单的方法。这个食谱提供了创建一个简单的 WebGL 包装器的步骤,这个包装器将用于本章的所有食谱。让我们开始吧!

提示

由于 WebGL 包装器相当复杂,你可能会考虑从本书的在线资源中获取 WebGL 包装器代码www.html5canvastutorials.com/cookbook/

操作步骤...

按照以下步骤创建一个 WebGL 包装器对象来简化 WebGL API,或者转到www.html5canvastutorials.com/cookbook并从资源部分下载WebGL.js

  1. 开始定义 WebGL 构造函数,初始化画布上下文并定义动画属性:
var WebGL = function(canvasId){
    this.canvas = document.getElementById(canvasId);
    this.context = this.canvas.getContext("experimental-webgl");
    this.stage = undefined;

    // Animation 
    this.t = 0;
    this.timeInterval = 0;
    this.startTime = 0;
    this.lastTime = 0;
    this.frame = 0;
    this.animating = false;
  1. 使用Paul IrishrequestAnimFrame shim 来创建一个跨浏览器的requestAnimationFrame函数,它使浏览器能够处理我们动画的 FPS:
    // provided by Paul Irish
    window.requestAnimFrame = (function(callback){
        return window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.oRequestAnimationFrame ||
        window.msRequestAnimationFrame ||
        function(callback){
            window.setTimeout(callback, 1000 / 60);
        };
    })();
  1. 由于Brandon JonesglMatrix使用全局变量,我们可以封装它们,这样这些变量就不能在包装器外部被改变:
   /*
   * encapsulte mat3, mat4, and vec3 from
   * glMatrix globals
   */
    this.mat3 = mat3;
    this.mat4 = mat4;
 this.vec3 = vec3;
  1. 定义着色器类型常量并初始化模型视图矩阵、透视矩阵和视口尺寸:
    // shader type constants
    this.BLUE_COLOR = "BLUE_COLOR";
    this.VARYING_COLOR = "VARYING_COLOR";
    this.TEXTURE = "TEXTURE";
    this.TEXTURE_DIRECTIONAL_LIGHTING = "TEXTURE_DIRECTIONAL_LIGHTING";

    this.shaderProgram = null;
    this.mvMatrix = this.mat4.create();
    this.pMatrix = this.mat4.create();
    this.mvMatrixStack = [];
    this.context.viewportWidth = this.canvas.width;
    this.context.viewportHeight = this.canvas.height;
  1. 启用深度测试:
    // init depth test
    this.context.enable(this.context.DEPTH_TEST);
};
  1. 为上下文和画布属性定义 getter 方法:
WebGL.prototype.getContext = function(){
    return this.context;
};

WebGL.prototype.getCanvas = function(){
    return this.canvas;
};
  1. 定义一个clear()方法,清除 WebGL 视口:
WebGL.prototype.clear = function(){
    this.context.viewport(0, 0, this.context.viewportWidth, this.context.viewportHeight);
    this.context.clear(this.context.COLOR_BUFFER_BIT | this.context.DEPTH_BUFFER_BIT);
};
  1. 定义setStage()方法:
WebGL.prototype.setStage = function(func){
    this.stage = func;
};
  1. 定义isAnimating()方法,返回动画是否正在运行:
WebGL.prototype.isAnimating = function(){
    return this.animating;
};
  1. 定义getFrame()方法,它返回当前帧数:
WebGL.prototype.getFrame = function(){
    return this.frame;
};
  1. 定义start()方法,开始动画:
WebGL.prototype.start = function(){
    this.animating = true;
    var date = new Date();
    this.startTime = date.getTime();
    this.lastTime = this.startTime;

    if (this.stage !== undefined) {
        this.stage();
    }

    this.animationLoop();
};
  1. 定义stopAnimation()方法,用于停止动画:
WebGL.prototype.stopAnimation = function(){
    this.animating = false;
};
  1. 定义getTimeInterval()方法,返回自上一帧渲染以来经过的毫秒数:
WebGL.prototype.getTimeInterval = function(){
    return this.timeInterval;
};
  1. 定义getTime()方法,返回自动画开始以来经过的毫秒数:
WebGL.prototype.getTime = function(){
    return this.t;
};
  1. 定义getFps()方法,返回浏览器确定的当前 FPS 值:
WebGL.prototype.getFps = function(){
    return this.timeInterval > 0 ? 1000 / this.timeInterval : 0;
};
  1. 定义animationLoop()方法,负责更新动画属性、绘制舞台并请求新的动画帧:
WebGL.prototype.animationLoop = function(){
    var that = this;

    this.frame++;
    var date = new Date();
    var thisTime = date.getTime();
    this.timeInterval = thisTime - this.lastTime;
    this.t += this.timeInterval;
    this.lastTime = thisTime;

    if (this.stage !== undefined) {
        this.stage();
    }

    if (this.animating) {
        requestAnimFrame(function(){
            that.animationLoop();
        });
    }
};
  1. 定义save()方法,通过将当前状态推送到模型视图矩阵堆栈上保存模型视图矩阵状态:
WebGL.prototype.save = function(){
    var copy = this.mat4.create();
    this.mat4.set(this.mvMatrix, copy);
    this.mvMatrixStack.push(copy);
};
  1. 定义restore()方法,恢复先前的模型视图状态:
WebGL.prototype.restore = function(){
    if (this.mvMatrixStack.length == 0) {
        throw "Invalid popMatrix!";
    }
    this.mvMatrix = this.mvMatrixStack.pop();
};
  1. 定义getFragmentShaderGLSL()方法,根据着色器类型参数获取GLSLGL Shader Language)片段代码。本质上,该方法包含四种不同的独立 GLSL 片段着色器程序,通过case语句进行选择:
WebGL.prototype.getFragmentShaderGLSL = function(shaderType){
    switch (shaderType) {
        case this.BLUE_COLOR:
            return "#ifdef GL_ES\n" +
            "precision highp float;\n" +
            "#endif\n" +
            "void main(void) {\n" +
            "gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);\n" +
            "}";
        case this.VARYING_COLOR:
            return "#ifdef GL_ES\n" +
            "precision highp float;\n" +
            "#endif\n" +
            "varying vec4 vColor;\n" +
            "void main(void) {\n" +
            "gl_FragColor = vColor;\n" +
            "}";
        case this.TEXTURE:
            return "#ifdef GL_ES\n" +
            "precision highp float;\n" +
            "#endif\n" +
            "varying vec2 vTextureCoord;\n" +
            "uniform sampler2D uSampler;\n" +
            "void main(void) {\n" +
            "gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));\n" +
            "}";
        case this.TEXTURE_DIRECTIONAL_LIGHTING:
            return "#ifdef GL_ES\n" +
            "precision highp float;\n" +
            "#endif\n" +
            "varying vec2 vTextureCoord;\n" +
            "varying vec3 vLightWeighting;\n" +
            "uniform sampler2D uSampler;\n" +
            "void main(void) {\n" +
            "vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));\n" +
            "gl_FragColor = vec4(textureColor.rgb * vLightWeighting, textureColor.a);\n" +
            "}";
    }
};
  1. 定义getVertexShaderGLSL()方法,根据着色器类型参数获取 GLSL 顶点代码:
WebGL.prototype.getVertexShaderGLSL = function(shaderType){
    switch (shaderType) {
        case this.BLUE_COLOR:
            return "attribute vec3 aVertexPosition;\n" +
            "uniform mat4 uMVMatrix;\n" +
            "uniform mat4 uPMatrix;\n" +
            "void main(void) {\n" +
            "gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);\n" +
            "}";
        case this.VARYING_COLOR:
            return "attribute vec3 aVertexPosition;\n" +
            "attribute vec4 aVertexColor;\n" +
            "uniform mat4 uMVMatrix;\n" +
            "uniform mat4 uPMatrix;\n" +
            "varying vec4 vColor;\n" +
            "void main(void) {\n" +
            "gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);\n" +
            "vColor = aVertexColor;\n" +
            "}";
        case this.TEXTURE:
            return "attribute vec3 aVertexPosition;\n" +
            "attribute vec2 aTextureCoord;\n" +
            "uniform mat4 uMVMatrix;\n" +
            "uniform mat4 uPMatrix;\n" +
            "varying vec2 vTextureCoord;\n" +
            "void main(void) {\n" +
            "gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);\n" +
            "vTextureCoord = aTextureCoord;\n" +
            "}";
        case this.TEXTURE_DIRECTIONAL_LIGHTING:
            return "attribute vec3 aVertexPosition;\n" +
            "attribute vec3 aVertexNormal;\n" +
            "attribute vec2 aTextureCoord;\n" +
            "uniform mat4 uMVMatrix;\n" +
            "uniform mat4 uPMatrix;\n" +
            "uniform mat3 uNMatrix;\n" +
            "uniform vec3 uAmbientColor;\n" +
            "uniform vec3 uLightingDirection;\n" +
            "uniform vec3 uDirectionalColor;\n" +
            "uniform bool uUseLighting;\n" +
            "varying vec2 vTextureCoord;\n" +
            "varying vec3 vLightWeighting;\n" +
            "void main(void) {\n" +
            "gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);\n" +
            "vTextureCoord = aTextureCoord;\n" +
            "if (!uUseLighting) {\n" +
            "vLightWeighting = vec3(1.0, 1.0, 1.0);\n" +
            "} else {\n" +
          "vec3 transformedNormal = uNMatrix * aVertexNormal;\n" +
            "float directionalLightWeighting = max(dot(transformedNormal, uLightingDirection), 0.0);\n" +
            "vLightWeighting = uAmbientColor + uDirectionalColor * directionalLightWeighting;\n" +
            "}\n" +
            "}";
    }
};

  1. 定义initShaders()方法,根据着色器类型参数初始化适当的着色器:
WebGL.prototype.initShaders = function(shaderType){
    this.initPositionShader();

    switch (shaderType) {
        case this.VARYING_COLOR:
            this.initColorShader();
            break;
        case this.TEXTURE:
            this.initTextureShader();
            break;
        case this.TEXTURE_DIRECTIONAL_LIGHTING:
            this.initTextureShader();
            this.initNormalShader();
            this.initLightingShader();
            break;
    }
};
  1. 定义setShaderProgram()方法,根据着色器类型参数设置着色器程序:
WebGL.prototype.setShaderProgram = function(shaderType){
    var fragmentGLSL = this.getFragmentShaderGLSL(shaderType);
    var vertexGLSL = this.getVertexShaderGLSL(shaderType);

    var fragmentShader = this.context.createShader(this.context.FRAGMENT_SHADER);
    this.context.shaderSource(fragmentShader, fragmentGLSL);
    this.context.compileShader(fragmentShader);

    var vertexShader = this.context.createShader(this.context.VERTEX_SHADER);
    this.context.shaderSource(vertexShader, vertexGLSL);
    this.context.compileShader(vertexShader);

    this.shaderProgram = this.context.createProgram();
    this.context.attachShader(this.shaderProgram, vertexShader);
    this.context.attachShader(this.shaderProgram, fragmentShader);
    this.context.linkProgram(this.shaderProgram);

    if (!this.context.getProgramParameter(this.shaderProgram, this.context.LINK_STATUS)) {
        alert("Could not initialize shaders");
    }

    this.context.useProgram(this.shaderProgram);

    // once shader program is loaded, it's time to init the shaders
    this.initShaders(shaderType);
};
  1. 定义perspective()方法,包装了 glMatrix 的perspective()方法,用于操作透视矩阵:
WebGL.prototype.perspective = function(viewAngle, minDist, maxDist){
    this.mat4.perspective(viewAngle, this.context.viewportWidth / this.context.viewportHeight, minDist, maxDist, this.pMatrix);
};
  1. 定义identity()方法,包装了 glMatrix 的identity()方法,用于操作模型视图矩阵:
WebGL.prototype.identity = function(){
    this.mat4.identity(this.mvMatrix);
};
  1. 定义translate()方法,包装了 glMatrix 的translate()方法,用于操作模型视图矩阵:
WebGL.prototype.translate = function(x, y, z){
    this.mat4.translate(this.mvMatrix, [x, y, z]);
};
  1. 定义rotate()方法,包装了 glMatrix 的rotate()方法,用于操作模型视图矩阵:
WebGL.prototype.rotate = function(angle, x, y, z){
    this.mat4.rotate(this.mvMatrix, angle, [x, y, z]);
};
  1. 定义initPositionShader()方法,初始化用于位置缓冲的位置着色器:
WebGL.prototype.initPositionShader = function(){
    this.shaderProgram.vertexPositionAttribute = this.context.getAttribLocation(this.shaderProgram, "aVertexPosition");
    this.context.enableVertexAttribArray(this.shaderProgram.vertexPositionAttribute);
    this.shaderProgram.pMatrixUniform = this.context.getUniformLocation(this.shaderProgram, "uPMatrix");
    this.shaderProgram.mvMatrixUniform = this.context.getUniformLocation(this.shaderProgram, "uMVMatrix");
};
  1. 定义initColorShader()方法,初始化用于颜色缓冲的颜色着色器:
WebGL.prototype.initColorShader = function(){
    this.shaderProgram.vertexColorAttribute = this.context.getAttribLocation(this.shaderProgram, "aVertexColor");
    this.context.enableVertexAttribArray(this.shaderProgram.vertexColorAttribute);
};
  1. 定义initTextureShader()方法,初始化用于纹理缓冲的纹理着色器:
WebGL.prototype.initTextureShader = function(){
    this.shaderProgram.textureCoordAttribute = this.context.getAttribLocation(this.shaderProgram, "aTextureCoord");
    this.context.enableVertexAttribArray(this.shaderProgram.textureCoordAttribute);
    this.shaderProgram.samplerUniform = this.context.getUniformLocation(this.shaderProgram, "uSampler");
};
  1. 定义initNormalShader()方法,初始化用于法线缓冲的法线着色器:
WebGL.prototype.initNormalShader = function(){
    this.shaderProgram.vertexNormalAttribute = this.context.getAttribLocation(this.shaderProgram, "aVertexNormal");
    this.context.enableVertexAttribArray(this.shaderProgram.vertexNormalAttribute);
    this.shaderProgram.nMatrixUniform = this.context.getUniformLocation(this.shaderProgram, "uNMatrix");
};
  1. 定义initLightingShader()方法,初始化环境光和定向光照着色器:
WebGL.prototype.initLightingShader = function(){
    this.shaderProgram.useLightingUniform = this.context.getUniformLocation(this.shaderProgram, "uUseLighting");
    this.shaderProgram.ambientColorUniform = this.context.getUniformLocation(this.shaderProgram, "uAmbientColor");
    this.shaderProgram.lightingDirectionUniform = this.context.getUniformLocation(this.shaderProgram, "uLightingDirection");
    this.shaderProgram.directionalColorUniform = this.context.getUniformLocation(this.shaderProgram, "uDirectionalColor");
};
  1. 定义initTexture()方法,包装了初始化 WebGL 纹理对象所需的 WebGL API 代码:
WebGL.prototype.initTexture = function(texture){
    this.context.pixelStorei(this.context.UNPACK_FLIP_Y_WEBGL, true);
    this.context.bindTexture(this.context.TEXTURE_2D, texture);
    this.context.texImage2D(this.context.TEXTURE_2D, 0, this.context.RGBA, this.context.RGBA, this.context.UNSIGNED_BYTE, texture.image);
    this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_MAG_FILTER, this.context.NEAREST);
    this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_MIN_FILTER, this.context.LINEAR_MIPMAP_NEAREST);
    this.context.generateMipmap(this.context.TEXTURE_2D);
    this.context.bindTexture(this.context.TEXTURE_2D, null);
};
  1. 定义createArrayBuffer()方法,包装了创建数组缓冲所需的 WebGL API 代码:
WebGL.prototype.createArrayBuffer = function(vertices){
    var buffer = this.context.createBuffer();
    buffer.numElements = vertices.length;
    this.context.bindBuffer(this.context.ARRAY_BUFFER, buffer);
    this.context.bufferData(this.context.ARRAY_BUFFER, new Float32Array(vertices), this.context.STATIC_DRAW);
    return buffer;
};
  1. 定义createElementArrayBuffer()方法,包装了创建元素数组缓冲所需的 WebGL API 代码:
WebGL.prototype.createElementArrayBuffer = function(vertices){
    var buffer = this.context.createBuffer();
    buffer.numElements = vertices.length;
    this.context.bindBuffer(this.context.ELEMENT_ARRAY_BUFFER, buffer);
    this.context.bufferData(this.context.ELEMENT_ARRAY_BUFFER, new Uint16Array(vertices), this.context.STATIC_DRAW);
    return buffer;
};
  1. 定义pushPositionBuffer()方法,将位置缓冲推送到显卡上:
WebGL.prototype.pushPositionBuffer = function(buffers){
    this.context.bindBuffer(this.context.ARRAY_BUFFER, buffers.positionBuffer);
    this.context.vertexAttribPointer(this.shaderProgram.vertexPositionAttribute, 3, this.context.FLOAT, false, 0, 0);
};
  1. 定义pushColorBuffer()方法,将颜色缓冲推送到显卡上:
WebGL.prototype.pushColorBuffer = function(buffers){
    this.context.bindBuffer(this.context.ARRAY_BUFFER, buffers.colorBuffer);
    this.context.vertexAttribPointer(this.shaderProgram.vertexColorAttribute, 4, this.context.FLOAT, false, 0, 0);
};
  1. 定义pushTextureBuffer()方法,将纹理缓冲推送到显卡上:
WebGL.prototype.pushTextureBuffer = function(buffers, texture){
    this.context.bindBuffer(this.context.ARRAY_BUFFER, buffers.textureBuffer);
    this.context.vertexAttribPointer(this.shaderProgram.textureCoordAttribute, 2, this.context.FLOAT, false, 0, 0);
    this.context.activeTexture(this.context.TEXTURE0);
    this.context.bindTexture(this.context.TEXTURE_2D, texture);
    this.context.uniform1i(this.shaderProgram.samplerUniform, 0);
};
  1. 定义pushIndexBuffer()方法,将索引缓冲推送到显卡上:
WebGL.prototype.pushIndexBuffer = function(buffers){
    this.context.bindBuffer(this.context.ELEMENT_ARRAY_BUFFER, buffers.indexBuffer);
};
  1. 定义pushNormalBuffer()方法,将法线缓冲推送到显卡上:
WebGL.prototype.pushNormalBuffer = function(buffers){
    this.context.bindBuffer(this.context.ARRAY_BUFFER, buffers.normalBuffer);
    this.context.vertexAttribPointer(this.shaderProgram.vertexNormalAttribute, 3, this.context.FLOAT, false, 0, 0);
};
  1. 定义setMatrixUniforms()方法,包装了设置矩阵统一变量所需的 WebGL API 代码:
WebGL.prototype.setMatrixUniforms = function(){
    this.context.uniformMatrix4fv(this.shaderProgram.pMatrixUniform, false, this.pMatrix);
    this.context.uniformMatrix4fv(this.shaderProgram.mvMatrixUniform, false, this.mvMatrix);

    var normalMatrix = this.mat3.create();
    this.mat4.toInverseMat3(this.mvMatrix, normalMatrix);
    this.mat3.transpose(normalMatrix);
    this.context.uniformMatrix3fv(this.shaderProgram.nMatrixUniform, false, normalMatrix);
};
  1. 定义drawElements()方法,包装了根据索引缓冲绘制非三角形位置缓冲所需的 WebGL API 代码:
WebGL.prototype.drawElements = function(buffers){
    this.setMatrixUniforms();

    // draw elements
    this.context.drawElements(this.context.TRIANGLES, buffers.indexBuffer.numElements, this.context.UNSIGNED_SHORT, 0);
};

  1. 定义drawArrays()方法,包装了绘制三角形位置缓冲所需的 WebGL API 代码:
WebGL.prototype.drawArrays = function(buffers){
    this.setMatrixUniforms();

    // draw arrays
    this.context.drawArrays(this.context.TRIANGLES, 0, buffers.positionBuffer.numElements / 3);
};
  1. 定义enableLighting()方法,包装了启用光照所需的 WebGL API 代码:
WebGL.prototype.enableLighting = function(){
    this.context.uniform1i(this.shaderProgram.useLightingUniform, true);
};
  1. 定义setAmbientLighting()方法,该方法包装了设置环境光照所需的 WebGL API 代码:
WebGL.prototype.setAmbientLighting = function(red, green, blue){
    this.context.uniform3f(this.shaderProgram.ambientColorUniform, parseFloat(red), parseFloat(green), parseFloat(blue));
};
  1. 定义setDirectionalLighting()方法,该方法包装了设置定向光照所需的 WebGL API 代码:
WebGL.prototype.setDirectionalLighting = function(x, y, z, red, green, blue){
    // directional lighting
    var lightingDirection = [x, y, z];
    var adjustedLD = this.vec3.create();
    this.vec3.normalize(lightingDirection, adjustedLD);
    this.vec3.scale(adjustedLD, -1);
    this.context.uniform3fv(this.shaderProgram.lightingDirectionUniform, adjustedLD);

    // directional color
    this.context.uniform3f(this.shaderProgram.directionalColorUniform, parseFloat(red), parseFloat(green), parseFloat(blue));
};

工作原理...

WebGL 包装器对象的想法是处理 WebGL API 没有提供的一些东西,并包装一些繁琐的代码块,这些代码块是执行简单事情所必需的。

WebGL 中有两个主要组件没有内置在 API 中——矩阵变换数学和着色器程序。在本章中,我们将使用由Brandon Jones专门为 WebGL 构建的一个方便的矩阵库 glMatrix 来处理所有的向量操作。至于缺少对着色器程序的支持,我们的 WebGL 包装器对象包括预先构建的 GLSL 着色器程序。着色器程序是用 GLSL 编写的,GLSL 是 OpenGL 着色语言的缩写,用于以编程方式定义顶点和片段的渲染方式。顶点着色器操作构成我们的 3D 模型形状的每个顶点,片段着色器操作由光栅化产生的每个片段。要使用着色器程序,我们实际上需要将 GLSL 代码的字符串传递给 WebGL API。

除了包装器方法之外,WebGL 包装器对象还包括我们在第五章中组合的动画方法,通过动画使画布生动起来

我们的 WebGL 包装器对象中剩余的大部分方法只是简单地包装了一些必要的代码块,用于将缓冲区推送到显卡,然后绘制结果。在接下来的五个示例中,我们将更深入地了解每种缓冲区类型,包括位置缓冲区、颜色缓冲区、索引缓冲区、纹理缓冲区和法线缓冲区。

还有更多...

要更深入地了解 WebGL 和 OpenGL,请查看这两个很棒的资源:

另请参阅...

  • 附录 A, 检测 Canvas 支持

创建三角形平面

现在我们已经设置好了 WebGL 包装器,让我们通过在屏幕上绘制一个简单的三角形来创建我们的第一个 WebGL 应用程序。这将为创建更复杂的 3D 模型所需的典型步骤奠定良好的基础。在这个示例中,我们将介绍位置缓冲区的概念,它们只是用于定义 3D 模型的位置和形状的顶点数组。

创建三角形平面

如何做...

按照以下步骤使用 WebGL 渲染 2D 三角形:

  1. 链接到glMatrix库和 WebGL 包装器:
<script type="text/javascript" src="img/glMatrix-1.0.1.min.js">
</script>
<script type="text/javascript" src="img/WebGL.js">
</script>
  1. 定义initBuffers()函数,用于初始化三角形的位置缓冲区:
    function initBuffers(gl){
        var triangleBuffers = {};
        triangleBuffers.positionBuffer = gl.createArrayBuffer([
            0, 1, 0,
            -1, -1, 0,
            1, -1, 0
        ]);
        return triangleBuffers;
    }
  1. 定义stage()函数,设置透视矩阵,将模型视图矩阵设置为单位矩阵,将模型视图矩阵在 z 方向上平移-5 个单位,将位置缓冲区推送到显卡,然后使用drawArrays()绘制三角形:
    function stage(gl, triangleBuffers){
        gl.clear();
        // set field of view at 45 degrees
        // set viewing range between 0.1 and 100.0 units away.
        gl.perspective(45, 0.1, 100.0);
        gl.identity();

        // translate model-view matrix
        gl.translate(0, 0, -5);

        gl.pushPositionBuffer(triangleBuffers);
        gl.drawArrays(triangleBuffers);
    }
  1. 当页面加载时,创建 WebGL 包装器对象的新实例,将着色器程序设置为"BLUE_COLOR",初始化三角形缓冲区,然后绘制舞台:
    window.onload = function(){
        var gl = new WebGL("myCanvas", "experimental-webgl");
        gl.setShaderProgram("BLUE_COLOR");
        var triangleBuffers = initBuffers(gl);
        stage(gl, triangleBuffers);
    };
  1. 在 HTML 文档的 body 内嵌入 canvas 标签:
        <canvas id="myCanvas" width="600" height="250"
            style="border:1px solid black;"></canvas>

它是如何工作的...

当页面加载时,我们需要做的第一件事是使用experimental-webgl上下文初始化 WebGL 包装器对象。在撰写本文时,experimental-webgl上下文是唯一在所有主要支持 WebGL 的浏览器中支持的画布上下文,包括 Google Chrome、Firefox 和 Safari。

接下来,我们可以将着色器程序设置为"BLUE_COLOR",这将使用一个预先构建的 GLSL 程序来渲染蓝色顶点和片段。一旦着色器程序设置好,我们需要初始化我们的缓冲区。缓冲区是用来定义我们的 3D 模型的顶点数组。对于这个教程,我们只会使用一个位置缓冲区,它定义了我们三角形的顶点位置。在以后的教程中,我们将介绍其他类型的缓冲区,包括索引缓冲区、纹理缓冲区和法线缓冲区。对于这个教程,位置缓冲区包含九个元素,代表三个顶点(每个顶点有 x、y 和 z 分量)。

一旦三角形缓冲区被初始化,我们可以绘制舞台。stage()函数首先清除画布,然后设置透视矩阵。我们的 WebGL 包装对象的perspective()方法接受三个参数,一个视角,一个最小可见距离和一个最大可见距离。在这个教程中,我们将最小可见距离设置为 0.1 个单位,最大可见距离设置为 100 个单位。任何距离小于 0.1 个单位的对象将是不可见的,任何距离大于 100 个单位的对象也将是不可见的。如果我们的舞台包含了许多复杂的模型分布在整个空间中,那么有一个很大的最大可见距离可能会导致性能问题,因为屏幕上渲染了太多东西。

接下来,我们可以使用identity()函数将模型视图矩阵设置为单位矩阵,然后将模型视图矩阵平移至(0, 0, -5)。这意味着我们只是将我们的模型向 z 方向移动了-5 个单位,即离用户 5 个单位。

最后,我们可以使用pushPositionBuffer()方法将位置缓冲区推送到显卡上,然后使用drawArrays()绘制三角形。

在 3D 空间中旋转一个三角形平面

现在我们可以在 3D 空间中绘制一个 2D 三角形,让我们尝试使用我们添加到 WebGL 包装对象的动画方法围绕 y 轴旋转它。

在 3D 空间中旋转三角形平面

如何做...

按照以下步骤在 WebGL 中围绕 y 轴旋转一个三角形:

  1. 链接到glMatrix库和 WebGL 包装器:
<script type="text/javascript" src="img/glMatrix-1.0.1.min.js">
</script>
<script type="text/javascript" src="img/WebGL.js">
</script>
  1. 定义initBuffers()函数,初始化我们三角形的位置缓冲区:
    function initBuffers(gl){
        var triangleBuffers = {};
        triangleBuffers.positionBuffer = gl.createArrayBuffer([
            0, 1, 0,
            -1, -1, 0,
            1, -1, 0
        ]);

        return triangleBuffers;
    }
  1. 定义stage()函数,设置透视,将模型视图矩阵设置为单位矩阵,平移三角形,围绕 y 轴旋转三角形,将位置缓冲区推送到显卡上,并使用drawArrays()绘制三角形:
    function stage(gl, triangleBuffers, angle){                
        // set field of view at 45 degrees
        // set viewing range between 0.1 and 100.0 units away.
        gl.perspective(45, 0.1, 100.0);
        gl.identity();

        // translate model-view matrix
        gl.translate(0, 0, -5);
        // rotate model-view matrix about y-axis
        gl.rotate(angle, 0, 1, 0);

        gl.pushPositionBuffer(triangleBuffers);
        gl.drawArrays(triangleBuffers);
    }
  1. 当页面加载时,初始化 WebGL 包装对象,设置着色器程序,初始化缓冲区,为动画设置stage函数,然后开始动画:
    window.onload = function(){
        var gl = new WebGL("myCanvas", "experimental-webgl");
        gl.setShaderProgram("BLUE_COLOR");
        var triangleBuffers = initBuffers(gl);
        var angle = 0;

        gl.setStage(function(){
            // update angle
            var angularVelocity = Math.PI / 2; // radians / second
            var angleEachFrame = angularVelocity * gl.getTimeInterval() / 1000;
            angle += angleEachFrame;

            this.clear();

            stage(gl, triangleBuffers, angle);
        });
        gl.start();
    };
  1. 在 HTML 文档的 body 内嵌入 canvas 标签:
        <canvas id="myCanvas" width="600" height="250"
            style="border:1px solid black;"></canvas>

它是如何工作的...

为了围绕 y 轴旋转我们的三角形,我们首先需要通过设置 WebGL 包装对象的stage()函数(类似于我们在第五章中使用Animation对象所做的)来设置一个动画阶段,然后用start()开始动画。对于每一帧动画,我们可以通过使用rotate()方法来旋转模型视图矩阵来增加三角形围绕 y 轴的角度。

另请参阅...

  • 在第五章中创建一个动画类

创建一个旋转的立方体

好了,现在真正的乐趣开始了。在这个教程中,我们将创建一个旋转的 3D 立方体,其面颜色不同。为此,我们将引入两种新的缓冲区——颜色缓冲区和索引缓冲区。

创建一个旋转的立方体

如何做...

按照以下步骤在 WebGL 中创建一个旋转的立方体:

  1. 链接到glMatrix库和 WebGL 包装器:
<script type="text/javascript" src="img/glMatrix-1.0.1.min.js">
</script>
<script type="text/javascript" src="img/WebGL.js">
</script>
  1. 定义initBuffers()函数,初始化我们立方体的位置缓冲区、颜色缓冲区和索引缓冲区:
    function initBuffers(gl){ 
        var cubeBuffers = {}
        cubeBuffers.positionBuffer = gl.createArrayBuffer([
            // Front face
            -1, -1,  1,
             1, -1,  1,
             1,  1,  1,
            -1,  1,  1,

            // Back face
            -1, -1, -1,
            -1,  1, -1,
             1,  1, -1,
             1, -1, -1,

            // Top face
            -1,  1, -1,
            -1,  1,  1,
             1,  1,  1,
             1,  1, -1,

            // Bottom face
            -1, -1, -1,
             1, -1, -1,
             1, -1,  1,
            -1, -1,  1,

            // Right face
             1, -1, -1,
             1,  1, -1,
             1,  1,  1,
             1, -1,  1,

            // Left face
            -1, -1, -1,
            -1, -1,  1,
            -1,  1,  1,
            -1,  1, -1
        ]);

        // build color Vertices
        var colors = [
            [1, 0, 1, 1], // Front face - Pink
            [0, 1, 0, 1], // Back face - Green
            [0, 0, 1, 1], // Top face - Blue
            [0, 1, 1, 1], // Bottom face - Turquoise
            [1, 1, 0, 1], // Right face - Yellow
            [1, 0, 0, 1]  // Left face - Red
        ];

        var colorVertices = [];

        for (var n in colors) {
            var color = colors[n];
            for (var i=0; i < 4; i++) {
                colorVertices = colorVertices.concat(color);
            }
        }

        cubeBuffers.colorBuffer = gl.createArrayBuffer(colorVertices);
        cubeBuffers.indexBuffer = gl.createElementArrayBuffer([
            0, 1, 2,      0, 2, 3,    // Front face
            4, 5, 6,      4, 6, 7,    // Back face
            8, 9, 10,     8, 10, 11,  // Top face
            12, 13, 14,   12, 14, 15, // Bottom face
            16, 17, 18,   16, 18, 19, // Right face
            20, 21, 22,   20, 22, 23  // Left face
        ]);

        return cubeBuffers;
    }
  1. 定义stage()函数,该函数设置透视,将模型视图矩阵设置为单位矩阵,平移立方体,旋转立方体,将位置缓冲、颜色缓冲和索引缓冲推送到图形卡上,最后使用drawElements()绘制立方体,因为我们的模型的面不是三角形的:
    function stage(gl, cubeBuffers, angle){         
        // set field of view at 45 degrees
        // set viewing range between 0.1 and 100.0 units away.
        gl.perspective(45, 0.1, 100);
        gl.identity();

        // translate model-view matrix
        gl.translate(0, 0, -5);
        // rotate model-view matrix about x-axis (tilt box downwards)
        gl.rotate(Math.PI * 0.15, 1, 0, 0);
        // rotate model-view matrix about y-axis
        gl.rotate(angle, 0, 1, 0);

        gl.pushPositionBuffer(cubeBuffers);
        gl.pushColorBuffer(cubeBuffers);
        gl.pushIndexBuffer(cubeBuffers);
        gl.drawElements(cubeBuffers);
    }
  1. 当页面加载时,初始化 WebGL 包装器对象,将着色器程序设置为"VARYING_COLOR",因为每个面的颜色是可变的,并且依赖于颜色缓冲,初始化缓冲区,为动画设置stage函数,然后开始动画:
    window.onload = function(){
        var gl = new WebGL("myCanvas", "experimental-webgl");
        gl.setShaderProgram("VARYING_COLOR");
        var cubeBuffers = initBuffers(gl);
        var angle = 0;
        gl.setStage(function(){
            // update angle
            var angularVelocity = Math.PI / 4; // radians / second
            var angleEachFrame = angularVelocity * this.getTimeInterval() / 1000;
            angle += angleEachFrame;

            this.clear();

            stage(this, cubeBuffers, angle);
        });
        gl.start();
    };
  1. 将 canvas 标签嵌入到 HTML 文档的 body 中:
        <canvas id="myCanvas" width="600" height="250"
            style="border:1px solid black;"></canvas>

它是如何工作的...

这个教程介绍了索引缓冲和颜色缓冲的概念。在前两个教程中,我们创建了一个三角形平面,因为具有三角形面的模型在 WebGL 中最容易实现,因为只需要一个缓冲区——位置缓冲。当我们想要创建一个具有非三角形面的 3D 模型,比如立方体时,就会复杂一些,因为我们需要一种方法来将立方体表示为一组三角形面。我们可以通过创建一个索引缓冲来实现这一点,该缓冲将三角形映射到位置缓冲的顶点。

看一下前面代码中的索引缓冲顶点。您会注意到前六个元素是[0, 1, 2, 0, 2, 3]。前三个元素[0, 1, 2]指的是位置缓冲的第 0、1、2 个顶点,形成了一个覆盖立方体正面一半的三角形。第二组元素[0, 2, 3]对应于位置缓冲的第 0、2、3 个顶点,形成了覆盖立方体正面另一半的第二个三角形。这两个三角形一起形成了立方体正面的实心面。当索引缓冲完成时,它将包含一个映射,该映射将位置缓冲顶点组成的三角形面覆盖立方体的六个面。

除了索引缓冲,这个教程还需要使用颜色缓冲。颜色缓冲用于定义模型面的颜色。在这个教程中,颜色缓冲将为我们的立方体的六个面定义六种不同的颜色。与索引缓冲类似,颜色缓冲用于将颜色映射到位置缓冲中的每个顶点。每种颜色由四个元素定义,[红,绿,蓝,alpha]。根据位置缓冲的定义,我们的立方体由六个面组成,每个面有四个顶点。因此,我们的颜色缓冲数组应包含(6 个面) * (每个面 4 个顶点) * (每种颜色 4 个元素) = 96 个元素。

一旦我们定义了位置缓冲、颜色缓冲和索引缓冲,我们所要做的就是将每个缓冲推送到图形卡上并渲染模型。与前两个教程不同的是,我们使用drawArrays()方法直接渲染三角形,而在这个教程中,我们将不得不使用drawElements()方法,因为我们的模型由非三角形面组成,需要一个索引缓冲将三角形面映射到模型的方形面。

另请参阅...

  • 在第五章中创建一个动画类

添加纹理和光照

现在我们知道如何使用位置缓冲和索引缓冲创建一个简单的 3D 模型,让我们通过使用一个箱子纹理包裹我们的模型,然后添加一些环境和定向光照来创建阴影表面。这个教程介绍了纹理缓冲来创建纹理和需要处理光照效果的法线缓冲。

添加纹理和光照

如何做...

按照以下步骤在 WebGL 中创建一个旋转的带光照的箱子:

  1. 链接到glMatrix库和 WebGL 包装器:
<script type="text/javascript" src="img/glMatrix-1.0.1.min.js">
</script>
<script type="text/javascript" src="img/WebGL.js">
</script>
  1. 定义initBuffers()函数,该函数初始化了我们的立方体的位置缓冲、法线缓冲、纹理缓冲和索引缓冲:
    function initBuffers(gl){
        var cubeBuffers = {};
        cubeBuffers.positionBuffer = gl.createArrayBuffer([
            // Front face
            -1, -1, 1, 
            1, -1, 1, 
            1, 1, 1, 
            -1, 1, 1, 

            // Back face
            -1, -1, -1, 
            -1, 1, -1, 
            1, 1, -1, 
            1, -1, -1, 

            // Top face
            -1, 1, -1, 
            -1, 1, 1, 
            1, 1, 1, 
            1, 1, -1, 

            // Bottom face
            -1, -1, -1, 
            1, -1, -1, 
            1, -1, 1, 
            -1, -1, 1, 

            // Right face
            1, -1, -1, 
            1, 1, -1, 
            1, 1, 1, 
            1, -1, 1, 

            // Left face
            -1, -1, -1, 
            -1, -1, 1, 
            -1, 1, 1, 
            -1, 1, -1
        ]);

        cubeBuffers.normalBuffer = gl.createArrayBuffer([
            // Front face
             0,  0,  1,
             0,  0,  1,
             0,  0,  1,
             0,  0,  1,

            // Back face
             0,  0, -1,
             0,  0, -1,
             0,  0, -1,
             0,  0, -1,

            // Top face
             0,  1,  0,
             0,  1,  0,
             0,  1,  0,
             0,  1,  0,

            // Bottom face
             0, -1,  0,
             0, -1,  0,
             0, -1,  0,
             0, -1,  0,

            // Right face
             1,  0,  0,
             1,  0,  0,
             1,  0,  0,
             1,  0,  0,

            // Left face
            -1,  0,  0,
            -1,  0,  0,
            -1,  0,  0,
            -1,  0,  0
        ]);

        cubeBuffers.textureBuffer = gl.createArrayBuffer([ 
            // Front face
            0, 0, 
            1, 0, 
            1, 1, 
            0, 1, 

            // Back face
             1, 0, 
            1, 1, 
            0, 1, 
            0, 0, 

            // Top face
             0, 1, 
            0, 0, 
            1, 0, 
            1, 1, 

            // Bottom face
             1, 1, 
            0, 1, 
            0, 0, 
            1, 0, 

            // Right face
             1, 0, 
            1, 1, 
            0, 1, 
            0, 0, 

            // Left face
             0, 0, 
            1, 0, 
            1, 1, 
            0, 1
        ]);

        cubeBuffers.indexBuffer = gl.createElementArrayBuffer([
             0, 1, 2,         0, 2, 3, // Front face
             4, 5, 6,         4, 6, 7, // Back face
             8, 9, 10,         8, 10, 11, // Top face
             12, 13, 14,     12, 14, 15, // Bottom face
             16, 17, 18,     16, 18, 19, // Right face
             20, 21, 22,     20, 22, 23 // Left face
        ]); 

        return cubeBuffers;            
    }
  1. 定义stage()函数,该函数设置透视,将模型视图矩阵设置为单位矩阵,平移立方体,旋转立方体,启用光照,设置环境光,设置定向光,将位置缓冲区、法线缓冲区、纹理缓冲区和索引缓冲区推送到显卡上,并最终使用drawElements()绘制立方体:
    function stage(gl, cubeBuffers, crateTexture, angle){
        // set field of view at 45 degrees
        // set viewing range between 0.1 and 100 units away.
        gl.perspective(45, 0.1, 100.0);
        gl.identity();

        // translate model-view matrix
        gl.translate(0, 0.0, -5);
        // rotate model-view matrix about x-axis (tilt box downwards)
        gl.rotate(Math.PI * 0.15, 1, 0, 0);
        // rotate model-view matrix about y-axis
        gl.rotate(angle, 0, 1, 0);

            // enable lighting
        gl.enableLighting();
        gl.setAmbientLighting(0.5, 0.5, 0.5);
        gl.setDirectionalLighting(-0.25, -0.25, -1, 0.8, 0.8, 0.8);

        gl.pushPositionBuffer(cubeBuffers);
        gl.pushNormalBuffer(cubeBuffers);
        gl.pushTextureBuffer(cubeBuffers, crateTexture);
        gl.pushIndexBuffer(cubeBuffers);
        gl.drawElements(cubeBuffers);
    }
  1. 定义init()方法,该方法初始化板条箱纹理,设置stage()函数,并开始动画:
    function init(gl, crateTexture){
        var cubeBuffers = initBuffers(gl);
        var angle = 0;
        gl.initTexture(crateTexture);
        gl.setStage(function(){
            // update angle
            var angularVelocity = Math.PI / 4; // radians / second
            var angleEachFrame = angularVelocity * this.getTimeInterval() / 1000;
            angle += angleEachFrame;
            this.clear();

            stage(this, cubeBuffers, crateTexture, angle);
        });
        gl.start();
    }
  1. 定义loadTexture()函数,该函数创建一个新的纹理对象,创建一个新的图像对象,初始化纹理并在纹理图像加载后开始动画:
    function loadTexture(gl){
        var crateTexture = gl.getContext().createTexture();
        crateTexture.image = new Image();

        crateTexture.image.onload = function(){
            init(gl, crateTexture);
        };
        crateTexture.image.src = "crate.jpg";
    }
  1. 页面加载时,初始化 WebGL 包装器对象,将着色器程序设置为"TEXTURE_DIRECTIONAL_LIGHTING",并加载纹理:
    window.onload = function(){
        var gl = new WebGL("myCanvas", "experimental-webgl");
        gl.setShaderProgram("TEXTURE_DIRECTIONAL_LIGHTING");
        loadTexture(gl);
    };
  1. 在 HTML 文档的 body 中嵌入 canvas 标签:
        <canvas id="myCanvas" width="600" height="250"
            style="border:1px solid black;"></canvas>

工作原理...

本示例介绍了纹理缓冲区和法线缓冲区的概念。纹理缓冲区允许我们为 3D 模型的每个面定义纹理图像的方向和比例。要定义木箱的纹理缓冲区,我们需要将纹理图像的四个角映射到立方体每个面的四个角。

为了处理 WebGL 的光照效果,我们需要使用法线缓冲区定义立方体构成的面的法线。法线是垂直于表面的向量。例如,地板的法线指向正上方,天花板的法线指向正下方。一旦我们定义了法线,我们现在可以设置环境光和定向光。

尽管在 WebGL 中可以实现许多其他类型的光照效果,但本示例侧重于两种最常见的——环境光和定向光,它们可以一起使用或独立使用:

  • 环境光指的是房间或世界的一般照明,并用 RGB 定义。具有环境光值[0,0,0]的房间将完全黑暗,而具有环境光值[1,1,1]的房间将完全照亮。此外,例如,如果我们有一个环境光值为[1,0,0]的房间,那么房间将被红光照亮。

  • 定向光使得面向光源的 3D 模型的面更亮,而背对光源的 3D 模型的面更暗。定向光通常用于模拟远处非常强的光源,比如太阳。

要同时使用纹理和定向光,我们可以使用setShaderProgram()方法将着色器程序设置为TEXTURE_DIRECTIONAL_LIGHTING,并使用enableLighting()方法启用光照。最后,我们可以使用setAmbientLighting()方法设置世界的环境光,并使用setDirectionalLighting()方法设置定向光。

另请参阅...

  • 在第五章中创建一个动画类

创建一个可以探索的 3D 世界

现在我们知道如何使用纹理和光照创建一些基本的 3D 模型,我们现在可以创建自己的 3D 世界。在这个示例中,我们将创建三组缓冲区——立方体缓冲区、墙壁缓冲区和地板缓冲区。我们可以使用立方体缓冲区在世界各处随机放置板条箱,使用墙壁缓冲区创建四面墙,使用地板缓冲区创建地板和天花板(我们可以重用地板缓冲区作为天花板缓冲区,因为它们是相同的形状)。接下来,我们将在文档中添加键盘事件监听器,以便我们可以使用箭头键和鼠标探索世界。让我们开始吧!

创建一个可以探索的 3D 世界

操作步骤...

按照以下步骤在 WebGL 中创建一个充满随机放置的板条箱的 3D 世界,可以使用键盘和鼠标进行探索:

  1. 链接到glMatrix库和 WebGL 包装器:
<script type="text/javascript" src="img/glMatrix-1.0.1.min.js">
</script>
<script type="text/javascript" src="img/WebGL.js">
</script>
  1. 定义Controller构造函数,用于初始化视图、WebGL 包装对象和模型,附加键盘事件侦听器,并加载世界纹理:
    /*************************************
     * Controller
     */
    function Controller(){
        this.view = new View(this);
        this.gl = new WebGL("myCanvas");
        this.gl.setShaderProgram("TEXTURE_DIRECTIONAL_LIGHTING");
        this.model = new Model(this);

        this.attachListeners();

        var sources = {
            crate: "crate.jpg",
            metalFloor: "metalFloor.jpg",
            metalWall: "metalWall.jpg",
            ceiling: "ceiling.jpg"
        };

        this.mouseDownPos = null;
        this.mouseDownPitch = 0;
        this.mouseDownYaw = 0;

        var that = this;
        this.loadTextures(sources, function(){
            that.gl.setStage(function(){
                that.view.stage();
            });

            that.gl.start();
        });
    }
  1. 定义loadTextures()方法,用于加载世界纹理:
    Controller.prototype.loadTextures = function(sources, callback){
        var gl = this.gl;
        var context = gl.getContext();
        var textures = this.model.textures;
        var loadedImages = 0;
        var numImages = 0;
        for (var src in sources) {
            // anonymous function to induce scope
            (function(){
                var key = src;
                numImages++;
                textures[key] = context.createTexture();
                textures[key].image = new Image();
                textures[key].image.onload = function(){
                    gl.initTexture(textures[key]);
                    if (++loadedImages >= numImages) {
                        callback();
                    }
                };

                textures[key].image.src = sources[key];
            })();
        }
    };
  1. 定义getMousePos()方法,用于获取鼠标位置:
    Controller.prototype.getMousePos = function(evt){
        return {
            x: evt.clientX,
            y: evt.clientY
        };
    };
  1. 定义handleMouseDown()方法,用于捕获起始鼠标位置、摄像机俯仰和摄像机偏航:
    Controller.prototype.handleMouseDown = function(evt){
        var camera = this.model.camera;
        this.mouseDownPos = this.getMousePos(evt);
        this.mouseDownPitch = camera.pitch;
        this.mouseDownYaw = camera.yaw;
    };
  1. 定义handleMouseMove()方法,用于更新摄像机:
    Controller.prototype.handleMouseMove = function(evt){
        var mouseDownPos = this.mouseDownPos;
        var gl = this.gl;
        if (mouseDownPos !== null) {
            var mousePos = this.getMousePos(evt);

            // update pitch
            var yDiff = mousePos.y - mouseDownPos.y;
            this.model.camera.pitch = this.mouseDownPitch + yDiff / gl.getCanvas().height;

            // update yaw
            var xDiff = mousePos.x - mouseDownPos.x;
            this.model.camera.yaw = this.mouseDownYaw + xDiff / gl.getCanvas().width;
        }
    };
  1. 定义handleKeyDown()方法,用于控制用户在世界中的移动:
    Controller.prototype.handleKeyDown = function(evt){
        var keycode = ((evt.which) || (evt.keyCode));
        var model = this.model;
        switch (keycode) {
            case 37:
                // left key
                model.sideMovement = model.LEFT;
                break;
            case 38:
                // up key
                model.straightMovement = model.FORWARD;
                break;
            case 39:
                // right key
                model.sideMovement = model.RIGHT;
                break;
            case 40:
                // down key
                model.straightMovement = model.BACKWARD;
                break;
        }
    };
  1. 定义handleKeyUp()方法,如果释放了左右箭头键,则将用户侧向移动设置为STILL,如果释放了上下箭头键,则将用户直线移动设置为STILL
    Controller.prototype.handleKeyUp = function(evt){
        var keycode = ((evt.which) || (evt.keyCode));
        var model = this.model;
        switch (keycode) {
            case 37:
                // left key
                model.sideMovement = model.STILL;
                break;
            case 38:
                // up key
                model.straightMovement = model.STILL;
                break;
            case 39:
                // right key
                model.sideMovement = model.STILL;
                break;
            case 40:
                // down key
                model.straightMovement = model.STILL;
                break;
        }
    };
  1. 定义attachListeners()方法,用于将侦听器附加到画布和文档:
    Controller.prototype.attachListeners = function(){
        var gl = this.gl;
        var that = this;
        gl.getCanvas().addEventListener("mousedown", function(evt){
            that.handleMouseDown(evt);
        }, false);

        gl.getCanvas().addEventListener("mousemove", function(evt){
            that.handleMouseMove(evt);
        }, false);

        document.addEventListener("mouseup", function(evt){
            that.mouseDownPos = null;
        }, false);

        document.addEventListener("mouseout", function(evt){
            // same as mouseup functionality
            that.mouseDownPos = null;
        }, false);

        document.addEventListener("keydown", function(evt){
            that.handleKeyDown(evt);
        }, false);

        document.addEventListener("keyup", function(evt){
            that.handleKeyUp(evt);
        }, false);
    };
  1. 定义Model构造函数,用于初始化摄像机和箱子、地板和墙壁的缓冲区:
    /*************************************
     * Model
     */
    function Model(controller){
        this.controller = controller;
        this.cubeBuffers = {};
        this.floorBuffers = {};
        this.wallBuffers = {};
        this.angle = 0;
        this.textures = {};
        this.cratePositions = [];

        // movements
        this.STILL = "STILL";
        this.FORWARD = "FORWARD";
        this.BACKWARD = "BACKWARD";
        this.LEFT = "LEFT";
        this.RIGHT = "RIGHT";

        // camera
        this.camera = {
            x: 0,
            y: 1.5,
            z: 5,
            pitch: 0,
            yaw: 0
        };

        this.straightMovement = this.STILL;
        this.sideMovement = this.STILL;
        this.speed = 8; // units per second    
        this.initBuffers();
        this.initCratePositions();
    }
  1. 定义initCratePositions()方法,用于在世界中生成 20 个具有随机位置的箱子,并随机堆叠箱子:
    Model.prototype.initCratePositions = function(){
        var crateRange = 45;
        // randomize 20 floor crates
        for (var n = 0; n < 20; n++) {
            var cratePos = {};
            cratePos.x = (Math.random() * crateRange * 2) - crateRange;
            cratePos.y = 0;
            cratePos.z = (Math.random() * crateRange * 2) - crateRange;
            cratePos.rotationY = Math.random() * Math.PI * 2;
            this.cratePositions.push(cratePos);

            if (Math.round(Math.random() * 3) == 3) {
                var stackedCratePosition = {};
                stackedCratePosition.x = cratePos.x;
                stackedCratePosition.y = 2.01;
                stackedCratePosition.z = cratePos.z;
                stackedCratePosition.rotationY = cratePos.rotationY + ((Math.random() * Math.PI / 8) - Math.PI / 16);
                this.cratePositions.push(stackedCratePosition);
            }
        }
    };
  1. 定义initCubeBuffers()方法,用于初始化箱子的立方体缓冲区:
    Model.prototype.initCubeBuffers = function(){
        var gl = this.controller.gl;
        this.cubeBuffers.positionBuffer = gl.createArrayBuffer([    
            -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, // Front face    
            -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, -1, -1, // Back face    
            -1, 1, -1, -1, 1, 1, 1, 1, 1, 1, 1, -1, // Top face    
            -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, // Bottom face    
            1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, // Right face    
            -1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, -1 // Left face
        ]);

        this.cubeBuffers.normalBuffer = gl.createArrayBuffer([    
            0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // Front face    
            0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, // Back face   
            0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, // Top face    
            0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, // Bottom face    
            1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // Right face    
            -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0 // Left face
        ]);

        this.cubeBuffers.textureBuffer = gl.createArrayBuffer([    
            0, 0, 1, 0, 1, 1, 0, 1, // Front face   
            1, 0, 1, 1, 0, 1, 0, 0, // Back face   
            0, 1, 0, 0, 1, 0, 1, 1, // Top face    
            1, 1, 0, 1, 0, 0, 1, 0, // Bottom face   
            1, 0, 1, 1, 0, 1, 0, 0, // Right face    
            0, 0, 1, 0, 1, 1, 0, 1 // Left face
        ]);

        this.cubeBuffers.indexBuffer = gl.createElementArrayBuffer([
            0, 1, 2, 0, 2, 3, // Front face
             4, 5, 6, 4, 6, 7, // Back face
             8, 9, 10, 8, 10, 11, // Top face
             12, 13, 14, 12, 14, 15, // Bottom face
             16, 17, 18, 16, 18, 19, // Right face
             20, 21, 22, 20, 22, 23 // Left face
        ]);
    };
  1. 定义initFloorBuffers()方法,用于初始化地板缓冲区(这些缓冲区也将用于天花板):
    Model.prototype.initFloorBuffers = function(){
        var gl = this.controller.gl;
        this.floorBuffers.positionBuffer = gl.createArrayBuffer([
            -50, 0, -50, -50, 0, 50, 50, 0, 50, 50, 0, -50
        ]);

        this.floorBuffers.textureBuffer = gl.createArrayBuffer([
            0, 25, 0, 0, 25, 0, 25, 25
        ]);

        this.floorBuffers.indexBuffer = gl.createElementArrayBuffer([
            0, 1, 2, 0, 2, 3
        ]);

        // floor normal points upwards
        this.floorBuffers.normalBuffer = gl.createArrayBuffer([
            0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0
        ]);
    };
  1. 定义initWallBuffers()方法,用于初始化墙壁缓冲区:
    Model.prototype.initWallBuffers = function(){
        var gl = this.controller.gl;
        this.wallBuffers.positionBuffer = gl.createArrayBuffer([
            -50, 5, 0, 50, 5, 0, 50, -5, 0, -50, -5, 0
        ]);

        this.wallBuffers.textureBuffer = gl.createArrayBuffer([
            0, 0, 25, 0, 25, 1.5, 0, 1.5
        ]);

        this.wallBuffers.indexBuffer = gl.createElementArrayBuffer([
            0, 1, 2, 0, 2, 3
        ]);

        // floor normal points upwards
        this.wallBuffers.normalBuffer = gl.createArrayBuffer([
            0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1
        ]);
    };
  1. 定义initBuffers()方法,用于初始化立方体、地板和墙壁缓冲区:
    Model.prototype.initBuffers = function(){
        this.initCubeBuffers();
        this.initFloorBuffers();
        this.initWallBuffers();
    };
  1. 定义updateCameraPos()方法,用于更新每个动画帧的摄像机位置:
    Model.prototype.updateCameraPos = function(){
        var gl = this.controller.gl;
        if (this.straightMovement != this.STILL) {
            var direction = this.straightMovement == this.FORWARD ? -1 : 1;
            var distEachFrame = direction * this.speed * gl.getTimeInterval() / 1000;
            this.camera.z += distEachFrame * Math.cos(this.camera.yaw);
            this.camera.x += distEachFrame * Math.sin(this.camera.yaw);
        }

        if (this.sideMovement != this.STILL) {
            var direction = this.sideMovement == this.RIGHT ? 1 : -1;
            var distEachFrame = direction * this.speed * gl.getTimeInterval() / 1000;
            this.camera.z += distEachFrame * Math.cos(this.camera.yaw + Math.PI / 2);
            this.camera.x += distEachFrame * Math.sin(this.camera.yaw + Math.PI / 2);
        }
    };
  1. 定义View构造函数,用于设置画布尺寸:
    /*************************************
     * View
     */
    function View(controller){
        this.controller = controller;
        this.canvas = document.getElementById("myCanvas");
        this.canvas.width = window.innerWidth;
        this.canvas.height = window.innerHeight;
    }
  1. 定义drawFloor()方法,用于绘制地板:
    View.prototype.drawFloor = function(){
        var controller = this.controller;
        var gl = controller.gl;
        var model = controller.model;
        var floorBuffers = model.floorBuffers;

        gl.save();
        gl.translate(0, -1.1, 0);
        gl.pushPositionBuffer(floorBuffers);
        gl.pushNormalBuffer(floorBuffers);
        gl.pushTextureBuffer(floorBuffers, model.textures.metalFloor);
        gl.pushIndexBuffer(floorBuffers);
        gl.drawElements(floorBuffers);
        gl.restore();
    };
  1. 定义drawCeiling()方法,用于绘制天花板:
    View.prototype.drawCeiling = function(){
        var controller = this.controller;
        var gl = controller.gl;
        var model = controller.model;
        var floorBuffers = model.floorBuffers;

        gl.save();
        gl.translate(0, 8.9, 0);
        // use floor buffers with ceiling texture
        gl.pushPositionBuffer(floorBuffers);
        gl.pushNormalBuffer(floorBuffers);
        gl.pushTextureBuffer(floorBuffers, model.textures.ceiling);
        gl.pushIndexBuffer(floorBuffers);
        gl.drawElements(floorBuffers);
        gl.restore();
    };
  1. 定义drawCrates()方法,用于绘制箱子:
    View.prototype.drawCrates = function(){
        var controller = this.controller;
        var gl = controller.gl;
        var model = controller.model;
        var cubeBuffers = model.cubeBuffers;

        for (var n = 0; n < model.cratePositions.length; n++) {
            gl.save();
            var cratePos = model.cratePositions[n];
            gl.translate(cratePos.x, cratePos.y, cratePos.z);
            gl.rotate(cratePos.rotationY, 0, 1, 0);
            gl.pushPositionBuffer(cubeBuffers);
            gl.pushNormalBuffer(cubeBuffers);
            gl.pushTextureBuffer(cubeBuffers, model.textures.crate);
            gl.pushIndexBuffer(cubeBuffers);
            gl.drawElements(cubeBuffers);
            gl.restore();
        }
    };
  1. 定义drawWalls()方法,用于绘制墙壁:
    View.prototype.drawWalls = function(){
        var controller = this.controller;
        var gl = controller.gl;
        var model = controller.model;
        var wallBuffers = model.wallBuffers;
        var metalWallTexture = model.textures.metalWall;

        gl.save();
        gl.translate(0, 3.9, -50);
        gl.pushPositionBuffer(wallBuffers);
        gl.pushNormalBuffer(wallBuffers);
        gl.pushTextureBuffer(wallBuffers, metalWallTexture);
        gl.pushIndexBuffer(wallBuffers);
        gl.drawElements(wallBuffers);
        gl.restore();

        gl.save();
        gl.translate(0, 3.9, 50);
        gl.rotate(Math.PI, 0, 1, 0);
        gl.pushPositionBuffer(wallBuffers);
        gl.pushNormalBuffer(wallBuffers);
        gl.pushTextureBuffer(wallBuffers, metalWallTexture);
        gl.pushIndexBuffer(wallBuffers);
        gl.drawElements(wallBuffers);
        gl.restore();

        gl.save();
        gl.translate(50, 3.9, 0);
        gl.rotate(Math.PI * 1.5, 0, 1, 0);
        gl.pushPositionBuffer(wallBuffers);
        gl.pushNormalBuffer(wallBuffers);
        gl.pushTextureBuffer(wallBuffers, metalWallTexture);
        gl.pushIndexBuffer(wallBuffers);
        gl.drawElements(wallBuffers);
        gl.restore();

        gl.save();
        gl.translate(-50, 3.9, 0);
        gl.rotate(Math.PI / 2, 0, 1, 0);
        gl.pushPositionBuffer(wallBuffers);
        gl.pushNormalBuffer(wallBuffers);
        gl.pushTextureBuffer(wallBuffers, metalWallTexture);
        gl.pushIndexBuffer(wallBuffers);
        gl.drawElements(wallBuffers);
        gl.restore();
    };
  1. 定义stage()方法,用于更新摄像机位置,清除画布,将世界相对于摄像机位置定位,然后绘制地板、墙壁、天花板和箱子:
    View.prototype.stage = function(){
        var controller = this.controller;
        var gl = controller.gl;
        var model = controller.model;
        var view = controller.view;
        var camera = model.camera;
        model.updateCameraPos();
        gl.clear();

        // set field of view at 45 degrees
        // set viewing range between 0.1 and 100 units away.
        gl.perspective(45, 0.1, 150.0);
        gl.identity();

        gl.rotate(-camera.pitch, 1, 0, 0);
        gl.rotate(-camera.yaw, 0, 1, 0);
        gl.translate(-camera.x, -camera.y, -camera.z);

        // enable lighting
        gl.enableLighting();
        gl.setAmbientLighting(0.5, 0.5, 0.5);
        gl.setDirectionalLighting(-0.25, -0.25, -1, 0.8, 0.8, 0.8);

        view.drawFloor();
        view.drawWalls();
        view.drawCeiling();
        view.drawCrates();
    };
  1. 页面加载时,初始化Controller
    window.onload = function(){
        new Controller();
    };
  1. 将 canvas 标签嵌入到 HTML 文档的 body 中:
        <canvas id="myCanvas" width="" height="">
        </canvas>

工作原理...

此示例使用 MVC(模型、视图、控制器)设计模式,将绘图逻辑与数据逻辑分离。

Controller类负责指导模型和视图,还管理用户操作。它使用handleKeyDown()handleKeyUp()方法处理箭头键事件,并使用handleMouseDown()handleMouseMove()方法处理屏幕拖动。此外,控制器还负责在模拟开始之前预加载所有纹理。

接下来,模型负责处理所有数据设置逻辑。我们模拟的数据包括立方体、地板和墙壁缓冲区、纹理、箱子位置、摄像机位置、俯仰和偏航,以及用户移动。箱子位置使用initCratePositions()方法初始化,世界的缓冲区使用initCubeBuffers()initFloorBuffers()initWallBuffers()方法初始化,摄像机位置、俯仰和偏航使用updateCameraPos()方法更新。

最后,视图负责使用模型数据渲染 3D 世界。缓冲区被推送到图形卡,并使用drawFloor()drawCeiling()drawCrates()drawWalls()方法进行渲染。对于每个动画帧,调用stage()方法更新摄像机位置,清除画布,设置照明,并使用上述绘图方法绘制场景。

还有更多...

如果您想扩展此示例,以下是一些更多的想法:

  • 添加边界条件,使玩家无法穿过箱子和墙壁

  • 使玩家能够跳跃,甚至跳上箱子

  • 创建通往其他房间的门

  • 创建楼梯,以便玩家可以探索其他楼层

  • 使用 HTML5 画布音频标签添加行走声音

现在你能够创建带有纹理和光照的 3D 模型,并将它们组合在一起形成 3D 世界的部分,你和真实的 Tron 之间唯一的障碍就是你自己的想象力。玩得开心!

另请参阅...

  • 在第五章中创建一个动画类

附录 A. 检测 Canvas 支持

Canvas 回退内容

由于所有浏览器都不支持 canvas,因此最好提供回退内容,以便用户知道如果他们选择的浏览器不支持 canvas,则某些功能无法正常工作。处理不支持 canvas 的浏览器最简单和最直接的技术是在 canvas 标签内添加回退内容。通常,这个内容将是文本或图像,告诉用户他们过时的浏览器不支持 canvas,并建议下载一个在本年代开发的浏览器。使用支持 canvas 的浏览器的用户将看不到内部内容:

<canvas id="myCanvas" width="578" height="250">
            Yikes!  Your browser doesn't support canvas.  Try using 
Google Chrome or Firefox instead.
</canvas>

Canvas 回退内容并不总是最好的解决方案。例如,如果浏览器不支持 canvas,您可能希望警告错误消息,将用户重定向到不同的 URL,甚至使用应用程序的 Flash 版本作为回退。检测浏览器是否支持 canvas 的最简单方法是创建一个虚拟 canvas 元素,然后检查我们是否可以执行 getContext 方法:

function isCanvasSupported(){
            return !!document.createElement('canvas').getContext;
        }

当页面加载时,我们可以调用 isCanvasSupported()函数来确定浏览器是否支持 canvas,然后适当处理结果。

这个函数使用了我最喜欢的 JavaScript 技巧之一,双非技巧(!!),它确定了 getContext 方法是否成功执行。双非的第一个非将数据类型强制转换为布尔值。由于强制转换数据类型的行为产生了我们不想要的相反结果,我们可以添加第二个非(!!)来翻转结果。双非技巧是检查一段代码是否成功执行的一种非常方便的方式,在我看来,它比用 try/catch 块包装一行代码要优雅得多。

检测可用的 WebGL 上下文

如果您的 canvas 应用程序利用了 WebGL,您可能还想知道浏览器支持哪些上下文,以便您可以成功初始化一个 WebGL 应用程序。

在撰写本文时,有五个主要的上下文:

  • 2D

  • webgl

  • 实验性的 WebGL

  • moz-webgl

  • webkit-3d

包括 Google Chrome,Firefox,Safari,Opera 和 IE9 在内的所有主要浏览器都支持 2D 上下文。然而,当涉及到 WebGL 支持时,情况完全不同。在撰写本文时,Google Chrome 和 Safari 支持实验性的 WebGL 和 webkit-3d 上下文,Firefox 支持实验性的 WebGL 和 moz-webgl 上下文,IE9 不支持任何形式的 WebGL。

要自己看到这一点,您可以创建一个名为 getCanvasSupport()的函数,该函数循环遍历所有可能的上下文,并使用双非技巧来确定哪些上下文是可用的:

function getCanvasSupport(){
    // initialize return object
    var returnObj = {
        canvas: false,
        webgl: false,
        context_2d: false,
        context_webgl: false,
        context_experimental_webgl: false,
        context_moz_webgl: false,
        context_webkit_3d: false
    };
    // check if canvas is supported
    if (!!document.createElement('canvas').getContext) {
        returnObj.canvas = true;
    }

    // check if WebGL rendering context is supported
    if (window.WebGLRenderingContext) {
        returnObj.webgl = true;
    }

    // check specific contexts
    var contextMapping = {
        context_2d: "2d",
        context_webgl: "webgl",
        context_experimental_webgl: "experimental-webgl",
        context_moz_webgl: "moz-webgl",
        context_webkit_3d: "webkit-3d"
    };

    for (var key in contextMapping) {
        try {
            if (!!document.createElement('canvas').getContext(contextMapping[key])) {
                returnObj[key] = true;
            }
        } 
        catch (e) {
        }
    }

    return returnObj;
}

function showSupport(obj){
    var str = "";

    str += "-- General Support --<br>";
    str += "canvas: " + (obj.canvas ? "YES" : "NO") + "<br>";
    str += "webgl: " + (obj.webgl ? "YES" : "NO") + "<br>";

    str += "<br>-- Successfully Initialized Contexts --<br>";
    str += "2d: " + (obj.context_2d ? "YES" : "NO") + "<br>";
    str += "webgl: " + (obj.context_webgl ? "YES" : "NO") + "<br>";
    str += "experimental-webgl: " + (obj.context_experimental_webgl ? "YES" : "NO") + "<br>";
    str += "moz-webgl: " + (obj.context_moz_webgl ? "YES" : "NO") + "<br>";
    str += "webkit-3d: " + (obj.context_webkit_3d ? "YES" : "NO") + "<br>";

    document.write(str);
}

window.onload = function(){
    showSupport(getCanvasSupport());
};

附录 B. 画布安全

为了保护网站上图像、视频和画布的像素数据,HTML5 画布规范中有防护措施,防止其他域的脚本访问这些媒体,操纵它们,然后创建新的图像、视频或画布。

在画布上绘制任何内容之前,画布标签的 origin-clean 标志设置为 true。这基本上意味着画布是“干净的”。如果您在托管代码运行的同一域上的画布上绘制图像,则 origin-clean 标志保持为 true。但是,如果您在托管在另一个域上的画布上绘制图像,则 origin-clean 标志将设置为 false,画布现在是“脏的”。

根据 HTML5 画布规范,一旦发生以下任何操作,画布就被视为脏:

    • 调用元素的 2D 上下文的drawImage()方法时,使用的HTMLImageElementHTMLVideoElement的原点与拥有画布元素的文档对象不同。
    • 调用元素的 2D 上下文的drawImage()方法时,使用的HTMLCanvasElement的 origin-clean 标志为 false。
    • 元素的 2D 上下文的fillStyle属性设置为从HTMLImageElementHTMLVideoElement创建的CanvasPattern对象,当时该模式的原点与拥有画布元素的文档对象不同。
    • 元素的 2D 上下文的fillStyle属性设置为从HTMLCanvasElement创建的CanvasPattern对象,当时该模式的 origin-clean 标志为 false。
  • 元素的 2D 上下文的strokeStyle属性设置为从HTMLImageElementHTMLVideoElement创建的CanvasPattern对象,当时该模式的原点与拥有画布元素的文档对象不同。

    • 元素的 2D 上下文的strokeStyle属性设置为从HTMLCanvasElement创建的CanvasPattern对象,当时该模式的 origin-clean 标志为 false。
    • 调用元素的 2D 上下文的fillText()strokeText()方法,并考虑使用原点与拥有画布元素的文档对象不同的字体。(甚至不必使用字体;重要的是字体是否被用于绘制任何字形。)
  • 此外,如果您在本地计算机上执行以下任何操作(而不是在 Web 服务器上),则 origin-clean 标志将自动设置为 false,因为资源将被视为来自不同的来源。

接下来,根据规范,如果在脏画布上发生以下任何操作,将抛出SECURITY_ERR异常:

    • 调用toDataURL()方法
    • 调用getImageData()方法
    • 使用measureText()方法时,使用的字体的原点与文档对象不同

尽管画布安全规范是出于良好意图创建的,但它可能会给我们带来更多麻烦。举个例子,假设您想创建一个绘图应用程序,该应用程序可以连接到 Flickr API,从公共域中获取图像以添加到您的绘图中。如果您希望您的应用程序能够使用toDataURL()方法将该绘图保存为图像,或者如果您希望您的应用程序使用getImageData()方法具有复杂的像素操作算法,那么您将遇到一些麻烦。在脏画布上执行这些操作将抛出 JavaScript 错误,并阻止您的应用程序正常工作。

解决这个问题的一种方法是创建一个代理,从另一个域获取图像,然后传递回客户端,使其看起来像图像来自您的域。如果您曾经使用过跨域 AJAX 应用程序,您会感到非常熟悉。

附录 C. 其他主题

画布与 CSS3 过渡和动画

除了画布之外,HTML5 规范还引入了两个令人兴奋的 CSS3 规范补充——过渡动画

过渡使开发人员能够创建简单的动画,可以在一定时间内改变 DOM 元素的样式。例如,如果你鼠标悬停在一个按钮上,希望它在一秒钟内逐渐变淡到不同的颜色,你可以使用 CSS3 过渡。

动画使开发人员能够通过定义指定的关键帧来创建更复杂的动画,这些关键帧可以被视为一系列链接的过渡。例如,如果你想通过移动DIV元素来创建动画,先向上移动,然后向左移动,然后向下移动,最后回到原来的位置,你可以使用 CSS3 动画,并为路径上的每个点定义一个关键帧。

所以,这就是人们困惑的地方。什么时候应该使用画布,什么时候应该使用 CSS3 来进行动画?如果你是一位经验丰富的开发人员,我相信你知道正确的答案是“这取决于情况”。作为一个经验法则,如果你要对 DOM 节点进行动画,或者动画简单且定义明确,最好使用 CSS3 过渡和动画。另一方面,如果你要对更复杂的东西进行动画,比如物理模拟器或在线游戏,那么使用画布可能更合适。

移动设备上的画布性能

随着移动和平板市场继续侵蚀传统的台式机和笔记本市场,重要的是要关注画布在移动空间中的作用。在撰写本文时,几乎所有移动设备上的画布动画性能都非常差,因为它们的 CPU 性能不足以处理。平板通常有更好的性能。不过也有好消息。除了软件改进和更强大的 CPU 外,移动设备和平板正在努力更好地利用硬件加速,帮助动画更流畅地运行。如果你考虑构建一个图形密集型的 Web 应用程序,大量使用画布动画,确保在移动设备上运行良好,那么最好事先做一些研究。

posted @ 2024-05-24 11:08  绝不原创的飞龙  阅读(0)  评论(0编辑  收藏  举报