Drawing graphics

 

该浏览器包含一些非常强大的图形编程工具,从可伸缩矢量图形( SVG )语言,到用于在HTML <canvas>元素上绘图的API(请参阅Canvas APIWebGL ). 本文提供了canvas的介绍,并提供了更多资源来使您了解更多.

Prerequisites: JavaScript基础知识(请参阅第一步构建基块JavaScript对象 ), 客户端API基础知识
Objective: 了解使用JavaScript在<canvas>元素上绘制的基础知识.

Graphics on the Web

正如我们在HTML 多媒体和嵌入模块中讨论的那样,Web最初只是文本,非常无聊,因此引入了图像-首先通过<img>元素引入,然后通过CSS属性(例如background-imageSVG)引入.

但是,这还不够. 尽管可以使用CSSJavaScript来动画化(或操纵)SVG矢量图像(以标记表示),但仍然无法对位图图像执行相同的操作,并且可用的工具相当有限. Web仍然没有办法有效地创建动画,游戏,3D场景以及通常由较低级别的语言(例如C ++或Java)处理的其他要求.

当浏览器开始支持<canvas>元素和相关的Canvas API时,情况开始改善.Apple于2004年左右发明了该元素,随后的几年中,其他浏览器又实现了它. 正如您将在下面看到的那样,canvas提供了许多有用的工具来创建2D动画,游戏,数据可视化以及其他类型的应用程序,尤其是与Web平台提供的其他一些API结合使用时.

下面的示例显示了一个简单的基于2D画布的弹跳球动画,这是我们最初在JavaScript对象介绍模块中遇到的:

在2006–2007年左右,Mozilla开始进行实验性3D画布实现的工作. 这就是WebGL ,在浏览器供应商中吸引了人们的注意力,并在2009-2010年左右进行了标准化. WebGL允许您在Web浏览器中创建真实的3D图形. 下例显示了一个简单的旋转WebGL多维数据集:

由于原始的WebGL代码非常复杂,因此本文将主要关注2D canvas. 但是,我们将展示如何使用WebGL库更轻松地创建3D场景,并且您可以在其他地方找到涵盖原始WebGL的教程-请参阅WebGL入门 .

注意 :基本的画布功能在所有浏览器中均受良好支持,但2D画布的IE 8及以下版本以及WebGL的IE 11及以下版本除外.

Active learning: Getting started with a <canvas>

如果要在网页上创建2D 3D场景,则需要从HTML <canvas>元素开始. 此元素用于定义页面上要在其中绘制图像的区域. 这就像在页面上包含元素一样简单:

<canvas width="320" height="240"></canvas>

这将在页面上创建一个尺寸为320 x 240像素的画布.

在canvas标记内,您可以放置​​一些后备内容,如果用户的浏览器不支持画布,则会显示这些内容.

<canvas width="320" height="240">
  <p>Your browser doesn't support canvas. Boo hoo!</p>
</canvas>

当然,以上消息确实没有帮助! 在一个真实的示例中,您希望将后备内容与画布内容相关联. 例如,如果您要绘制一个不断更新的股价图,则后备内容可能是最新股价图的静态图像,其中的替代文字说明了价格在文字中的含义.

Creating and sizing our canvas

让我们从创建自己的画布开始,然后将其用于将来的实验.

  1. 首先,对我们的0_canvas_start.html文件进行本地复制,然后在文本编辑器中将其打开.

  2. 在开始的<body>标签下面,添加以下代码:

    <canvas class="myCanvas">
      <p>Add suitable fallback here.</p>
    </canvas>

    我们已经在<canvas>元素中添加了一个class ,因此如果页面上有多个画布,则选择起来会更加容易,但是现在我们已经删除了widthheight属性(如果需要,可以重新添加它们,但是我们将在下面的部分中使用JavaScript进行设置). 没有明确的宽度和高度的画布默认为300像素宽x 150像素高.

  3. 现在,在<script>元素内添加以下JavaScript行:

    const canvas = document.querySelector('.myCanvas');
    const width = canvas.width = window.innerWidth;
    const height = canvas.height = window.innerHeight;

    在这里,我们在canvas常量中存储了对canvas的引用. 在第二行中,我们既设置了新的恒定width又设置了画布的width属性,该属性等于Window.innerWidth (这为我们提供了视口宽度). 在第三行中,我们既设置了一个新的恒定height又设置了画布的height属性,该属性等于Window.innerHeight (这为我们提供了视口高度). 现在,我们有了一个画布,它可以填充浏览器窗口的整个宽度和高度!

    您还将看到我们将赋值与多个等号链接在一起-JavaScript允许这样做,并且如果要使多个变量都等于相同的值,这是一种很好的技术. 我们希望在width / height变量中易于访问画布的宽度和高度,因为它们是有用的值,可在以后使用(例如,如果要在画布的宽度上绘制一半的东西).

  4. 如果您现在将示例保存并加载到浏览器中,您将看不到任何内容,这很好,但是您还会看到滚动条,这对我们来说是个问题,因为<body>元素的margin添加到我们全窗口大小的画布中,得到的文档比窗口宽. 要摆脱滚动条,我们需要删除margin并将overflow设置为hidden . 将以下内容添加到文档的<head>中:

    <style>
      body {
        margin: 0;
        overflow: hidden;
      }
    </style>

    The scrollbars should now be gone.

注意 :如上所述,通常应该使用HTML属性或DOM属性设置图像的大小. 您可以使用CSS,但麻烦的是在画布渲染后完成大小调整,就像其他任何图像(渲染的画布只是图像)一样,该图像可能会像素化/扭曲.

Getting the canvas context and final setup

在考虑画布模板完成之前,我们需要做最后一件事. 要在画布上绘制,我们需要获得对绘制区域的特殊引用,称为上下文. 这是使用HTMLCanvasElement.getContext()方法完成的,对于基本用法,该方法将单个字符串作为参数表示您要检索的上下文类型.

在这种情况下,我们需要一个2d画布,因此在<script>元素中的其他行下方添加以下JavaScript行:

const ctx = canvas.getContext('2d');

注意 :您可以选择的其他上下文值包括用于WebGL的webgl ,用于WebGL 2的webgl2等,但是在本文中我们将不需要它们.

就是这样-我们的画布现在已经上底漆并准备好进行绘图了! ctx变量现在包含一个CanvasRenderingContext2D对象,并且在画布上进行的所有绘制操作都将涉及对该对象的操作.

在继续之前,让我们做最后一件事. 我们将画布背景涂成黑色,以使您对画布API有所了解. 在JavaScript底部添加以下行:

ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(0, 0, width, height);

在这里,我们使用canvas的fillStyle属性设置填充颜色(就像CSS属性一样使用颜色值 ),然后使用fillRect方法绘制一个覆盖画布整个区域的矩形(前两个参数是矩形的左上角;最后两个是您要绘制矩形的宽度和高度-我们告诉您,这些widthheight变量将很有用)!

好的,我们的模板已经制作好了,该继续了.

2D canvas basics

如上所述,所有绘制操作都是通过操作CanvasRenderingContext2D对象(在我们的示例中为ctx )完成的. 需要为许多操作提供坐标以精确指出要在哪里绘制东西–画布的左上方是点(0,0),水平(x)轴从左到右,垂直(y)轴从从上到下.

绘制形状倾向于使用矩形形状图元完成,或者通过沿特定路径描画一条线然后填充该形状来完成. 下面我们将展示如何做到这两种.

Simple rectangles

让我们从一些简单的矩形开始.

  1. 首先,获取新编码的画布模板的副本(如果不执行上述步骤,请复制1_canvas_template.html的本地副本).

  2. 接下来,将以下行添加到JavaScript的底部:

    ctx.fillStyle = 'rgb(255, 0, 0)';
    ctx.fillRect(50, 50, 100, 150);

    如果保存并刷新,您应该看到画布上出现了一个红色矩形. 它的左上角距离画布边缘的顶部和左侧50像素(由前两个参数定义),并且它的宽度为100像素,高度为150像素(由第三和第四参数定义).

  3. 让我们在混合中添加另一个矩形-这次是绿色. 在JavaScript的底部添加以下内容:

    ctx.fillStyle = 'rgb(0, 255, 0)';
    ctx.fillRect(75, 75, 100, 100);

    保存并刷新,您将看到新的矩形. 这就提出了一个重要的观点:图形操作(如绘制矩形,线条等)以它们发生的顺序执行. 可以把它想象成是一堵墙,每层油漆都重叠在一起,甚至可能隐藏下面的东西. 您无法采取任何措施来更改此设置,因此您必须仔细考虑绘制图形的顺序.

  4. Note that you can draw semi-transparent graphics by specifying a semi-transparent color, for example by using rgba(). The a value defines what's called the "alpha channel, " or the amount of transparency the color has. The higher its value, the more it will obscure whatever's behind it. Add the following to your code:

    ctx.fillStyle = 'rgba(255, 0, 255, 0.75)';
    ctx.fillRect(25, 100, 175, 50);
  5. 现在尝试绘制更多自己的矩形. 玩得开心!

Strokes and line widths

So far we've looked at drawing filled rectangles, but you can also draw rectangles that are just outlines (called strokes in graphic design). To set the color you want for your stroke, you use the strokeStyle property; drawing a stroke rectangle is done using strokeRect.

  1. 将以下内容添加到前面的示例中,再次在前面的JavaScript行下面:

    ctx.strokeStyle = 'rgb(255, 255, 255)';
    ctx.strokeRect(25, 25, 175, 200);
  2. 笔划的默认宽度为1像素; 您可以调整lineWidth属性值来更改此值(它需要一个数字来表示笔触的像素宽). 在前两行之间添加以下行:

    ctx.lineWidth = 5;

现在,您应该看到白色轮廓变得更粗了! 现在就这样. 此时,您的示例应如下所示:

注意 :完成的代码可在GitHub上作为2_canvas_rectangles.html获得 .

Drawing paths

如果要绘制比矩形更复杂的内容,则需要绘制路径. 基本上,这涉及编写代码以确切指定笔应在画布上沿着什么路径移动以追踪要绘制的形状. 画布包括用于绘制直线,圆,贝塞尔曲线等的功能.

让我们从制作画布模板的新副本( 1_canvas_template.html )开始,开始绘制新示例.

我们将在以下所有部分中使用一些通用的方法和属性:

  • beginPath() —在笔当前在画布上的位置开始绘制路径. 在新的画布上,笔从(0,0)开始.
  • moveTo() —将笔移动到画布上的其他点,而无需记录或跟踪线条; 笔只是"跳"到新位置.
  • fill() —通过填充到目前为止已跟踪的路径来绘制填充形状.
  • stroke() —通过沿到目前为止绘制的路径绘制笔触来绘制轮廓形状.
  • 您还可以将lineWidthfillStyle / strokeStyle与路径以及矩形一起使用.

一个典型的简单路径绘制操作如下所示:

ctx.fillStyle = 'rgb(255, 0, 0)';
ctx.beginPath();
ctx.moveTo(50, 50);
// draw your path
ctx.fill();

Drawing lines

让我们在画布上绘制一个等边三角形.

  1. 首先,在代码底部添加以下帮助器函数. 这会将度值转换为弧度,这很有用,因为每当您需要在JavaScript中提供角度值时,它几乎总是以弧度为单位,但是人类通常以度为单位.

    function degToRad(degrees) {
      return degrees * Math.PI / 180;
    };
  2. 接下来,通过在之前添加的内容下面添加以下内容来开始您的工作; 在这里,我们为三角形设置颜色,开始绘制路径,然后将笔移至(50,50),而未绘制任何内容. 那就是我们开始绘制三角形的地方.

    ctx.fillStyle = 'rgb(255, 0, 0)';
    ctx.beginPath();
    ctx.moveTo(50, 50);
  3. 现在,在脚本底部添加以下行:

    ctx.lineTo(150, 50);
    let triHeight = 50 * Math.tan(degToRad(60));
    ctx.lineTo(100, 50+triHeight);
    ctx.lineTo(50, 50);
    ctx.fill();

    让我们依次执行以下操作:

    First we draw a line across to (150, 50) — our path now goes 100 pixels to the right along the x axis.

    其次,我们使用一些简单的三角函数求出等角三角形的高度. 基本上,我们绘制的三角形朝下. 等角三角形中的角度始终为60度. 要计算高度,我们可以将其分成两个直角三角形,中间分别成90度,60度和30度角. 从侧面看:

    • 最长的边称为斜边
    • 60度角旁边的一侧称为相邻边 -我们知道它是50像素,因为它只是我们绘制的线的一半.
    • 与60度角相反的一侧称为相反 ,这是我们要计算的三角形的高度.

    一个基本的三角公式指出,相邻的长度乘以该角度的切线等于相反的值,因此得出50 * Math.tan(degToRad(60)) . 我们使用degToRad()函数将60度转换为弧度,因为Math.tan()期望以弧度为单位的输入值.

  4. 计算出高度后,我们再画一条线到(100, 50 + triHeight) . X坐标很简单; 它必须位于我们设置的前两个X值之间. 另一方面,Y值必须为50加三角形的高度,因为我们知道三角形的顶部距离画布的顶部50个像素.

  5. 下一条线将一条线绘制回三角形的起点.

  6. 最后,我们运行ctx.fill()结束路径并填写形状.

Drawing circles

现在让我们看一下如何在画布上绘制一个圆. 这可以使用arc()方法完成,该方法在指定点绘制全部或部分圆.

  1. 让我们在画布上添加一条弧线-在代码底部添加以下内容:

    ctx.fillStyle = 'rgb(0, 0, 255)';
    ctx.beginPath();
    ctx.arc(150, 106, 50, degToRad(0), degToRad(360), false);
    ctx.fill();

    arc()接受六个参数. 前两个指定圆弧中心的位置(分别为X和Y). 第三个是圆的半径,第四个和第五个是绘制圆的开始和结束角度(因此,指定0和360度可得到一个完整的圆),第六个参数定义是否应逆时针绘制圆(逆时针)或顺时针( false为顺时针).

    注意 :0度位于水平右侧.

  2. 让我们尝试添加另一个弧:

    ctx.fillStyle = 'yellow';
    ctx.beginPath();
    ctx.arc(200, 106, 50, degToRad(-45), degToRad(45), true);
    ctx.lineTo(200, 106);
    ctx.fill();

    这里的模式非常相似,但是有两个区别:

    • 我们已将arc()的最后一个参数设置为true ,这意味着将沿逆时针方向绘制弧,这意味着即使将弧指定为从-45度开始并以45度结束,我们仍将在270度附近绘制弧不在这部分内. 如果将true更改为false ,然后重新运行代码,则只会绘制该圆的90度切片.
    • 在调用fill()之前,我们在圆心处画一条线. 这意味着我们得到了相当不错的吃豆人风格的抠图. 如果删除了这条线(尝试一下!),然后重新运行代码,您将仅在圆弧的起点和终点之间切掉圆的边缘. 这说明了画布的另一个重要点-如果您尝试填充不完整的路径(即未关闭的路径),浏览器将在起点和终点之间填充一条直线,然后将其填充.

现在就这样; 您的最后一个示例应如下所示:

注意 :完成的代码可在GitHub上作为3_canvas_paths.html获得 .

注意 :要了解有关高级路径绘制功能(例如贝塞尔曲线)的更多信息,请查看我们的"画布图形绘制"教程.

Text

画布还具有用于绘制文本的功能. 让我们简要地探讨这些. 首先制作另一个新的画布模板副本( 1_canvas_template.html ),在其中绘制新示例.

使用两种方法绘制文本:

两者在基本用法上都具有三个属性:要绘制的文本字符串以及文本框 (实际上是围绕所绘制文本的框)左上角的X和Y坐标.

还有许多属性可帮助控制文本呈现,例如font ,它使您可以指定字体系列,大小等.它的值与CSS font属性的语法相同.

Try adding the following block to the bottom of your JavaScript:

ctx.strokeStyle = 'white';
ctx.lineWidth = 1;
ctx.font = '36px arial';
ctx.strokeText('Canvas text', 50, 50);

ctx.fillStyle = 'red';
ctx.font = '48px georgia';
ctx.fillText('Canvas text', 50, 150);

在这里,我们绘制两行文本,一个轮廓,另一个描边. 最后的示例应如下所示:

注意 :完整的代码在GitHub上可作为4_canvas_text.html获得 .

玩一下,看看你能想到什么! 您可以在" 绘图文本"中找到有关画布文本可用选项的更多信息.

Drawing images onto canvas

可以将外部图像渲染到画布上. 这些可以是简单的图像,视频的帧或其他画布的内容. 目前,我们仅研究在画布上使用一些简单图像的情况.

  1. 和以前一样,为我们的画布模板制作另一个新副本( 1_canvas_template.html ),以在其中绘制新示例. 在这种情况下,您还需要将我们的示例图像的副本firefox.png保存在同一目录中.

    使用drawImage()方法将图像绘制到画布上. 最简单的版本具有三个参数-对要渲染的图像的引用,以及图像左上角的X和Y坐标.

  2. Let's start by getting an image source to embed in our canvas. Add the following lines to the bottom of your JavaScript:

    let image = new Image();
    image.src = 'firefox.png';

    在这里,我们使用Image()构造函数创建一个新的HTMLImageElement对象. 返回的对象与您获取对现有<img>元素的引用时返回的对象具有相同的类型. 然后,将其src属性设置为等于我们的Firefox徽标图像. 此时,浏览器开始加载图像.

  3. 现在,我们可以尝试使用drawImage()嵌入图像,但是我们需要确保首先加载了图像文件,否则代码将失败. 我们可以使用onload事件处理程序来实现此目的,该处理程序仅在映像完成加载后才被调用. 在上一个模块下面添加以下模块:

    image.onload = function() {
      ctx.drawImage(image, 50, 50);
    }

    如果现在在浏览器中加载示例,则应该看到图像嵌入在画布中.

  4. 但是还有更多! 如果我们只想显示图像的一部分或调整其大小怎么办? 我们可以使用更复杂的drawImage()来完成这两个任务. 像这样更新您的ctx.drawImage()行:

    ctx.drawImage(image, 20, 20, 185, 175, 50, 50, 185, 175);
    • 和以前一样,第一个参数是图像参考.
    • 参数2和3定义要从加载的图像中切出的区域的左上角相对于图像本身的左上角的坐标. 在第一个参数左侧或第二个参数上方不会绘制任何内容.
    • 参数4和5定义了我们要从加载的原始图像中切出的区域的宽度和高度.
    • 参数6和7定义相对于画布的左上角要在其上绘制图像的切除部分的左上角的坐标.
    • 参数8和9定义了宽度和高度,以绘制图像的剪切区域. 在这种情况下,我们指定了与原始切片相同的尺寸,但是您可以通过指定不同的值来调整尺寸.

最后的示例应如下所示:

注意 :完成的代码可在GitHub上作为5_canvas_images.html获得 .

Loops and animations

到目前为止,我们已经介绍了2D画布的一些非常基本的用法,但实际上,除非您以某种方式进行更新或设置动画,否则您将不会体验到画布的全部功能. 毕竟,画布确实提供了可脚本化的图像! 如果您不打算进行任何更改,那么您最好只使用静态图像并保存所有工作.

Creating a loop

在canvas中使用循环非常有趣-您可以像其他JavaScript代码一样在for (或其他类型)循环中运行canvas命令.

让我们建立一个简单的例子.

  1. 再次制作我们的画布模板( 1_canvas_template.html ),然后在代码编辑器中将其打开.

  2. 将以下行添加到JavaScript的底部. 它包含一个新方法translate() ,它可以移动画布的原点:

    ctx.translate(width/2, height/2);

    这将导致坐标原点(0,0)移至画布的中心,而不是位于左上角. 这在许多情况下非常有用,例如这种情况,我们希望相对于画布的中心绘制设计.

  3. 现在,将以下代码添加到JavaScript的底部:

    function degToRad(degrees) {
      return degrees * Math.PI / 180;
    };
    
    function rand(min, max) {
      return Math.floor(Math.random() * (max-min+1)) + (min);
    }
    
    const length = 250;
    const moveOffset = 20;
    
    for(var i = 0; i < length; i++) {
    
    }

    在这里,我们实现了与在上面的三角形示例中看到的相同的degToRad()函数,一个rand()函数,该函数返回给定下限和上限, lengthmoveOffset变量之间的随机数(稍后我们将详细介绍) ,以及一个空的for循环.

  4. 这里的想法是,我们将在for循环内的画布上绘制一些东西,并在每次循环上对其进行迭代,以便我们可以创建一些有趣的东西. 在您的for循环内添加以下代码:

    ctx.fillStyle = 'rgba(' + (255-length) + ', 0, ' + (255-length) + ', 0.9)';
    ctx.beginPath();
    ctx.moveTo(moveOffset, moveOffset);
    ctx.lineTo(moveOffset+length, moveOffset);
    let triHeight = length/2 * Math.tan(degToRad(60));
    ctx.lineTo(moveOffset+(length/2), moveOffset+triHeight);
    ctx.lineTo(moveOffset, moveOffset);
    ctx.fill();
    
    length--;
    moveOffset += 0.7;
    ctx.rotate(degToRad(5));

    因此,在每次迭代中,我们:

    • fillStyle设置为略带透明的紫色阴影,每次都会根据length的值进行更改. 稍后您将看到,每次循环运行时长度都会变小,因此这里的效果是,绘制每个连续的三角形后颜色会变亮.
    • 开始这条路.
    • 将笔移动到(moveOffset, moveOffset)坐标; 此变量定义每次绘制新三角形时要移动的距离.
    • (moveOffset+length, moveOffset)的坐标上画一条线. 这将画一条length与X轴平行的线.
    • 像以前一样计算三角形的高度.
    • 在三角形的向下角画一条线,然后再回到三角形的起点画一条线.
    • 调用fill()填充三角形.
    • 更新描述三角形序列的变量,以便我们准备绘制下一个三角形. 我们将length值减少1,因此每次三角形都变小; moveOffset增加moveOffset ,使每个连续的三角形稍远一些,然后使用另一个新函数rotate() ,它可以旋转整个画布! 在绘制下一个三角形之前,我们将其旋转5度.

而已! 最后的示例应如下所示:

在这一点上,我们希望鼓励您尝试并自己制作示例! 例如:

  • 绘制矩形或弧形而不是三角形,甚至嵌入图像.
  • 播放lengthmoveOffset值.
  • 使用上面包括但未使用的rand()函数引入一些随机数.

注意 :完整的代码在GitHub上可作为6_canvas_for_loop.html获得 .

Animations

上面我们构建的循环示例很有趣,但是实际上您需要一个恒定的循环,该循环对于任何严肃的画布应用程序(例如游戏和实时可视化)都将不断发展. 如果您认为画布就像电影一样,您确实希望显示器在每一帧上进行更新以显示更新后的视图,并且理想的刷新率为每秒60帧,从而使人眼看起来动感流畅.

有一些JavaScript函数可以让您每秒重复运行几次函数,其中最适合我们的目的是window.requestAnimationFrame() . 它有一个参数-您要为每帧运行的函数的名称. 下次浏览器准备更新屏幕时,将调用您的函数. 如果该函数将新的更新绘制到动画中,然后在函数结束之前再次调用requestAnimationFrame() ,则动画循环将继续运行. 当您停止调用requestAnimationFrame()或在调用requestAnimationFrame()但在调用框架之前调用window.cancelAnimationFrame()时,循环结束.

注意:在使用完动画后,最好从主代码中调用cancelAnimationFrame() ,以确保没有更新等待运行.

浏览器可以计算出复杂的细节,例如使动画以一致的速度运行,而不是浪费资源使看不见的东西动起来.

要查看其工作原理,让我们快速回顾一下"弹跳球"示例( 实时观看 ,并查看源代码 ). 使一切保持移动的循环代码如下所示:

function loop() {
  ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
  ctx.fillRect(0, 0, width, height);

  for(let i = 0; i < balls.length; i++) {
    balls[i].draw();
    balls[i].update();
    balls[i].collisionDetect();
  }

  requestAnimationFrame(loop);
}

loop();

我们在代码底部运行一次loop()函数以开始循环,绘制第一个动画帧. 然后, loop()函数负责再次调用requestAnimationFrame(loop)以运行动画的下一帧.

请注意,在每一帧上,我们都完全清除了画布并重新绘制了所有内容. 对于存在的每个球,我们都将其绘制,更新其位置,并检查其是否与其他任何球发生碰撞. 一旦将图形绘制到画布上,就无法像使用DOM元素那样单独地操纵该图形. 您无法在画布上四处移动每个球,因为一旦绘制,它便是画布的一部分,而不是单个可访问的元素或对象. 取而代之的是,您必须擦除或重新绘制,方法是擦除整个框架并重新绘制所有内容,或者通过具有确切知道需要擦除哪些部分的代码而仅擦除并重新绘制画布的最小区域.

优化图形动画是编程的全部专长,可以使用许多巧妙的技术. 但是,这些超出了我们示例所需的范围!

通常,制作画布动画的过程涉及以下步骤:

  1. 清除画布内容(例如,使用fillRect()clearRect() ).
  2. 使用save()保存状态(如有必要)-当您想要保存在画布上更新的设置然后继续操作时,这是必需的,这对于更高级的应用程序很有用.
  3. 绘制要制作动画的图形.
  4. 使用restore()您在步骤2中保存的设置.
  5. 调用requestAnimationFrame()安排动画下一帧的绘制.

注意 :我们不会在这里介绍save()restore() ,但在我们的Transformations教程(及其后面的教程)中对它们进行了很好的解释.

A simple character animation

现在,让我们创建自己的简单动画-我们将从某款很棒的复古电脑游戏中获得一个角色,在屏幕上行走.

  1. 再次制作我们的画布模板( 1_canvas_template.html ),然后在代码编辑器中将其打开. 在同一目录中复制walk-right.png .

  2. 在JavaScript的底部,添加以下行以再次使坐标原点位于画布的中间:

    ctx.translate(width/2, height/2);
  3. 现在让我们创建一个新的HTMLImageElement对象,将其src设置为我们要加载的图像,并添加一个onload事件处理程序,该事件处理程序将导致draw()函数在加载图像时触发:

    let image = new Image();
    image.src = 'walk-right.png';
    image.onload = draw;
  4. 现在,我们将添加一些变量以跟踪精灵将在屏幕上绘制的位置以及要显示的精灵编号.

    let sprite = 0;
    let posX = 0;

    让我们解释一下spritesheet图片(我们谨向Mike Thomas借用了CSS动画创建sprite sheet步行周期 ). 图像如下所示:

    它包含六个精灵,这些精灵构成了整个行走过程-每个精灵宽102像素,高148像素. 为了清晰地显示每个子画面,我们将不得不使用drawImage()从子画面中切出一个子画面图像,并仅显示该部分,就像我们上面对Firefox徽标所做的那样. 切片的X坐标必须是102的倍数,Y坐标必须始终为0.切片大小始终为102 x 148像素.

  5. 现在,让我们在代码底部插入一个空的draw()函数,准备填充一些代码:

    function draw() {
    
    };
  6. 本节中的其余代码在draw()内部. 首先,添加以下行,这将清除画布以准备绘制每帧. 注意,我们必须将矩形的左上角指定为-(width/2), -(height/2)因为我们早先将原点位置指定为width/2, height/2 .

    ctx.fillRect(-(width/2), -(height/2), width, height);
  7. 接下来,我们将使用drawImage(9参数版本)绘制图像. 添加以下内容:

    ctx.drawImage(image, (sprite*102), 0, 102, 148, 0+posX, -74, 102, 148);

    如你看到的:

    • 我们指定image作为要嵌入的图像.
    • 参数2和3指定要从源图像中切出的切片的左上角,其中X值是sprite乘以102(其中sprite是0到5之间的sprite编号),Y值始终是0.
    • 参数4和5指定要剪切的切片的大小-102像素乘以148像素.
    • 参数6和7指定要在画布上绘制切片的框的左上角-X位置为0 + posX ,这意味着我们可以通过更改posX值来更改绘图位置.
    • 参数8和9指定画布上图像的大小. 我们只想保留其原始大小,因此我们将宽度指定为102和148.
  8. 现在,我们将在每次绘制后更改sprite值-好吧,无论如何在其中一些绘制之后. 将以下块添加到draw()函数的底部:

      if (posX % 13 === 0) {
        if (sprite === 5) {
          sprite = 0;
        } else {
          sprite++;
        }
      }

    我们将整个块包装在if (posX % 13 === 0) { ... } . 我们使用模( % )运算符(也称为余数运算符 )来检查posX值是否可以精确地除以13而没有余数. 如果是这样,我们将通过递增sprite进入下一个sprite(在完成#5 sprite之后,将其包装为0). 这实际上意味着我们仅在第13帧(或大约每秒5帧)上更新子画面requestAnimationFrame()如果可能, requestAnimationFrame()以每秒60帧的速度调用我们). 我们故意放慢帧速率,因为我们只有六个精灵可以使用,如果每60秒显示一个精灵,我们的角色移动得太快了!

    在外部块中,我们使用if ... else语句检查sprite值是否为5(最后一个子画面,假定子画面号从0到5). 如果我们显示已经是最后一个精灵,我们重置sprite回到0; 如果不是,我们只需将其增加1.

  9. 接下来,我们需要研究如何更改每帧上的posX值-在最后一个代码块的下面添加以下代码块.

      if(posX > width/2) {
        newStartPos = -((width/2) + 102);
        posX = Math.ceil(newStartPos / 13) * 13;
        console.log(posX);
      } else {
        posX += 2;
      }

    我们正在使用另一个if ... else语句来查看posX的值posX已大于width/2 ,这意味着我们的角色已经走离屏幕的右边缘. 如果是这样,我们计算一个位置,该位置将字符放置在屏幕左侧的左侧,然后将posX设置为等于最接近该数字的13的倍数. 这必须是13的倍数,因为否则否则以前的代码块将无法工作,因为posX永远不会等于13的倍数!

    如果我们的角色尚未离开屏幕边缘,我们只需将posX增加2.这将使他在下次绘制时向右移动一点.

  10. 最后,我们需要通过调用draw()函数底部的requestAnimationFrame()来制作动画循环:

    window.requestAnimationFrame(draw);

而已! 最后的示例应如下所示:

注意 :完整的代码可在GitHub上以7_canvas_walking_animation.html的形式获得 .

A simple drawing application

作为最后一个动画示例,我们想向您展示一个非常简单的绘图应用程序,以说明如何将动画循环与用户输入(在这种情况下,如鼠标移动)结合在一起. 我们不会让您逐步构建这个. 我们将只探讨代码中最有趣的部分.

该示例可以在GitHub上找到,名称为8_canvas_drawing_app.html ,您可以在下面实时使用它:

让我们看看最有趣的部分. 首先,我们跟踪鼠标的X和Y坐标以及是否被点击或不与三个变量是: curXcurYpressed . 当鼠标移动时,我们将触发一个函数设置为onmousemove事件处理程序,该函数将捕获当前的X和Y值. 我们还使用onmousedownonmouseup事件处理程序在pressed鼠标按钮时将pressed的值更改为true ,在释放鼠标键时将其再次更改为false .

let curX;
let curY;
let pressed = false;

document.onmousemove = function(e) {
  curX = (window.Event) ? e.pageX : e.clientX + (document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft);
  curY = (window.Event) ? e.pageY : e.clientY + (document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop);
}

canvas.onmousedown = function() {
  pressed = true;
};

canvas.onmouseup = function() {
  pressed = false;
}

当按下"清除画布"按钮时,我们运行一个简单的功能,将整个画布变回黑色,就像我们之前看到的一样:

clearBtn.onclick = function() {
  ctx.fillStyle = 'rgb(0, 0, 0)';
  ctx.fillRect(0, 0, width, height);
}

这次的绘制循环非常简单-如果按下true ,我们将绘制一个圆形,其填充样式等于颜色选择器中的值,并且半径等于在范围输入中设置的值.

function draw() {
  if(pressed) {
    ctx.fillStyle = colorPicker.value;
    ctx.beginPath();
    ctx.arc(curX, curY-85, sizePicker.value, degToRad(0), degToRad(360), false);
    ctx.fill();
  }

  requestAnimationFrame(draw);
}

draw();

注意<input> rangecolor类型在所有浏览器中都得到很好的支持,但Internet Explorer版本低于10除外; Safari也不支持color . 如果您的浏览器不支持这些输入,它们将退回到简单的文本字段,您只需要自己输入有效的颜色/数字值即可.

WebGL

现在是时候抛弃2D,快速看一下3D画布了. 使用WebGL API可以指定3D画布内容,这是与2D画布API完全独立的API,即使它们都渲染到<canvas>元素上也是如此.

WebGL基于OpenGL (开放图形库),并允许您直接与计算机的GPU通信. 因此,与常规JavaScript相比,编写原始WebGL更接近C ++等低级语言. 它非常复杂,但是功能强大.

Using a library

由于其复杂性,大多数人使用第三方JavaScript库(例如Three.jsPlayCanvasBabylon.js)编写3D图形代码. 这些功能大多数都以类似的方式工作,提供创建原始和自定义形状,定位摄像头和照明设备,覆盖具有纹理的表面等功能. 他们为您处理WebGL,使您可以在更高层次上工作.

是的,使用这些方法之一意味着要学习另一个新的API(在这种情况下为第三方),但是它们比编写原始WebGL要简单得多.

Recreating our cube

让我们看一下如何使用WebGL库创建内容的简单示例. 我们选择Three.js ,因为它是最受欢迎的之一. 在本教程中,我们将创建我们之前看到的3D旋转立方体.

  1. 首先,在新文件夹中创建index.html的本地副本,然后在同一文件夹中保存metal003.png的副本. 这是我们稍后将用作多维数据集的表面纹理的图像.

  2. 接下来,再次在与之前相同的文件夹中创建一个名为main.js的新文件.

  3. 如果在代码编辑器中打开index.html ,您将看到它具有两个<script>元素-第一个将three.min.js附加到页面,第二个将main.js文件附加到页面. 您需要下载three.min.js库 ,并将其保存在与以前相同的目录中.

  4. 现在我们在three.js附加了three.js ,我们可以开始编写将JavaScript用作main.js . 让我们从创建一个新场景开始-将以下内容添加到main.js文件中:

    const scene = new THREE.Scene();

    Scene()构造函数创建一个新场景,该场景代表了我们试图显示的整个3D世界.

  5. 接下来,我们需要一个摄像头,以便我们可以看到场景. 用3D图像术语来说,相机代表了观看者在世界上的位置. 要创建相机,请在下面添加以下几行:

    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.z = 5;
    

    PerspectiveCamera()构造函数采用四个参数:

    • 视野:应在屏幕上以度为单位显示摄像机前面的区域的宽度.
    • 纵横比:通常,这是场景的宽度除以场景的高度的比率. 使用另一个值会使场景变形(这可能是您想要的,但通常不是).
    • 接近平面:在我们停止将其呈现到屏幕之前,它们与摄影机对象之间的距离可能有多近. 想一想,当您的指尖越来越靠近眼睛之间的空间时,最终看不到它了.
    • 远平面:物体不再被渲染之前离相机有多远.

    我们还将相机的位置设置为Z轴外5个距离单位,就像CSS中一样,它在屏幕之外朝向观看者朝向您.

  6. 第三个重要成分是提炼剂. 这是一个对象,可通过给定的摄像机查看给定的场景. 我们现在将使用WebGLRenderer()构造函数创建一个,但直到稍后再使用. 接下来添加以下行:

    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    第一行创建一个新的渲染器,第二行设置渲染器绘制相机视图的大小,第三行将渲染器创建的<canvas>元素附加到文档的<body> . 现在,渲染器绘制的所有内容都将显示在我们的窗口中.

  7. 接下来,我们要创建要在画布上显示的多维数据集. 在JavaScript底部添加以下代码块:

    let cube;
    
    let loader = new THREE.TextureLoader();
    
    loader.load( 'metal003.png', function (texture) {
      texture.wrapS = THREE.RepeatWrapping;
      texture.wrapT = THREE.RepeatWrapping;
      texture.repeat.set(2, 2);
    
      let geometry = new THREE.BoxGeometry(2.4, 2.4, 2.4);
      let material = new THREE.MeshLambertMaterial( { map: texture, shading: THREE.FlatShading } );
      cube = new THREE.Mesh(geometry, material);
      scene.add(cube);
    
      draw();
    });

    这里还有更多内容,因此我们分阶段进行:

    • 我们首先创建一个cube全局变量,以便我们可以从代码中的任何位置访问多维数据集.
    • 接下来,我们创建一个新的TextureLoader对象,然后在其上调用load() . 在这种情况下, load()需要两个参数(尽管可能需要更多参数):我们要加载的纹理(我们的PNG),以及在纹理加载后将运行的函数.
    • Inside this function we use properties of the texture object to specify that we want a 2 x 2 repeat of the image wrapped around all sides of the cube. Next, we create a new BoxGeometry object and a new MeshLambertMaterial object, and bring them together in a Mesh to create our cube. An object typically requires a geometry (what shape it is) and a material (what its surface looks like).
    • 最后,我们将立方体添加到场景中,然后调用draw()函数以开始动画.
  8. Before we get to defining draw(), we'll add a couple of lights to the scene, to liven things up a bit; add the following blocks next:

    let light = new THREE.AmbientLight('rgb(255, 255, 255)'); // soft white light
    scene.add(light);
    
    let spotLight = new THREE.SpotLight('rgb(255, 255, 255)');
    spotLight.position.set( 100, 1000, 1000 );
    spotLight.castShadow = true;
    scene.add(spotLight);

    AmbientLight对象是一种柔和的灯光,可以稍微照亮整个场景,就像在户外时一样. 另一方面, SpotLight对象是定向光束,更像是手电筒/手电筒(实际上是聚光灯).

  9. 最后,让我们在代码底部添加draw()函数:

    function draw() {
      cube.rotation.x += 0.01;
      cube.rotation.y += 0.01;
      renderer.render(scene, camera);
    
      requestAnimationFrame(draw);
    }

    This is fairly intuitive; on each frame, we rotate our cube slightly on its X and Y axes, then render the scene as viewed by our camera, then finally call requestAnimationFrame() to schedule drawing our next frame.

让我们再快速看一看成品的外观:

您可以在GitHub上找到完成的代码 .

注意 :在我们的GitHub存储库中,您还可以找到另一个有趣的3D多维数据集示例— Three.js Video Cube (也请现场观看 ). 它使用getUserMedia()从计算机网络摄像头获取视频流,并将其作为纹理投影到立方体的侧面!

Summary

在这一点上,您应该对使用Canvas和WebGL进行图形编程的基础知识以及如何使用这些API有所了解,以及对进一步信息的了解. 玩得开心!

See also

在这里,我们仅介绍了画布的真实基础知识-还有很多东西要学习! 以下文章将带您更进一步.

  • 画布教程 —一个非常详细的教程系列,比这里介绍的内容更详细地介绍了您应该了解的有关2D画布的知识. 基本阅读.
  • WebGL教程 —一个系列,讲授原始WebGL编程的基础.
  • 使用Three.js构建基本演示 -基础Three.js教程. 我们也有PlayCanvasBabylon.js的等效指南.
  • 游戏开发 -MDN上的网络游戏开发的登录页面. 这里有一些与2D和3D画布相关的非常有用的教程和技术-请参阅"技术和教程"菜单选项.

Examples

  • 暴力theramin-使用Web Audio API生成声音,并使用canvas生成漂亮的可视化效果并与之一起使用.
  • 语音自动更改 -使用画布来可视化来自Web Audio API的实时音频数据.

In this module