浏览器包含一些非常强大的图形编程工具,如:SVG、Canvas、WebGL
学习 JavaScript 在 元素中绘图的基础知识。
当浏览器开始支持 HTML 画布元素 和相关的 Canvas API(由苹果公司在 2004 年前后发明,后来其他的浏览器开始跟进) -大约在 2006 - 2007 年,Mozilla 开始测试 3D 画布。后来演化为 WebGL,它获得了各大浏览器厂商的认可,于是大约在 2009 - 2010 年间得到了标准化。WebGL 可以让你在 web 浏览器中生成真正的 3D 图形。
画布的基本功能有良好的跨浏览器支持。以下是例外:IE 8 及以下不支持 2D 画布,IE 11 及以下不支持WebGL。
<canvas class="myCanvas">
<p>添加恰当的反馈信息。</p>
</canvas>
我们为 <canvas> 元素添加了一个 class,使得在网页中选择多个画布时会容易些。不明确指定宽高的画布,默认尺寸为 300 × 150 像素。
var canvas = document.querySelector('.myCanvas');
var width = canvas.width = window.innerWidth;
var height = canvas.height = window.innerHeight;
如果现在保存文件,浏览器中什么也不会显示,这并没有问题,但是滚动条还是可见的,这就是问题了。原因是我们的“全窗尺寸画布”包含 <body> 元素的外边距(margin),使得文档比窗口略宽。 为使滚动条消失,需要删除 <body> 元素的 margin 并将 overflow 设置为 hidden。在文档的 <head> 中添加以下代码即可:
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
我们需要获得一个对绘画区域的特殊的引用(称为 上下文)来在画布上绘图。可通过 HTMLCanvasElement.getContext() 方法获得基础的绘画功能,需要提供一个字符串参数来表示所需上下文的类型
这里我们需要一个 2d 画布: var ctx = canvas.getContext('2d');
注:可选上下文还包括 WebGL(webgl)、WebGL 2(webgl2)等等,但本文暂不涉及。
好啦,现在已经万事具备!ctx变量包含一个 CanvasRenderingContext2D 对象,画布上所有绘画操作都会涉及到这个对象。
开始前我们先初尝一下 canvas API。在 JS 代码中添加以下两行,将画布背景涂成黑色:
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(0, 0, width, height);
这里我们使用画布的 fillStyle 属性(和CSS属性 色值 一致)设置填充色,然后使用 fillRect 方法绘制一个覆盖整个区域的矩形(前两个参数是矩形左上顶点的坐标,后两个参数是矩形的长宽,现在你知道 width 和 height 的作用了吧)。
所有绘画操作都离不开 CanvasRenderingContext2D 对象(这里叫做 ctx)。许多操作都需要提供坐标来指示绘图的确切位置。画布左上角的坐标是(0, 0),横坐标(x)轴向右延伸,纵坐标(y)轴向下延伸。
绘图操作可基于原始矩形模型实现,也可通过追踪一个特定路径后填充颜色实现。
1、画布模板:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Canvas</title>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<canvas class="myCanvas">
<p>Add suitable fallback here.</p>
</canvas>
<script>
var canvas = document.querySelector('.myCanvas');
var width = canvas.width = window.innerWidth;
var height = canvas.height = window.innerHeight;
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgb(0,0,0)';
ctx.fillRect(0,0,width,height);
</script>
</body>
</html>
2、在 JS 代码末尾添加下面两行:
ctx.fillStyle = 'rgb(255, 0, 0)';
ctx.fillRect(50, 50, 100, 150);
保存并刷新,画布上将出现一个红色的矩形。其左边和顶边与画布边缘距离均为 50 像素(由前两个参数指定),宽 100 像素、高 150 像素(由后两个参数指定)。
3、然后再添加一个绿色矩形。在 JS 代码末尾添加下面两行:
ctx.fillStyle = 'rgb(0, 255, 0)';
ctx.fillRect(75, 75, 100, 100);
这里引出了一个新问题:绘制矩形、线等操作按出现的顺序依次进行。就像粉刷墙面时,两层重叠时新层总会覆盖旧层。这一点是无法改变的,因此在绘制图形时一定要慎重考虑顺序问题。
4、还可以通过指定半透明的颜色来绘制半透明的图形,比如使用 rgba()。 a 指定了“α 通道”的值,也就是颜色的透明度。值越高透明度越高,底层的内容就越清晰。
ctx.fillStyle = 'rgba(255, 0, 255, 0.75)';
ctx.fillRect(25, 100, 175, 50);
你可以使用 strokeStyle 属性来设置描边颜色,使用 strokeRect 来绘制一个矩形的轮廓。
1、在上文的 JS 代码的末尾添加以下代码:
ctx.strokeStyle = 'rgb(255, 255, 255)';
ctx.strokeRect(25, 25, 175, 200);
2、默认的描边宽度是 1 像素,可以通过调整 lineWidth 属性(接受一个表示描边宽度像素值的数字)的值来修改。在上文两行后添加以下代码:
ctx.lineWidth = 5;
注意:lineWidth 应在 strokeRect 才会生效。
可以通过绘制路径来绘制比矩形更复杂的图形。路径中至少要包含钢笔运行精确路径的代码以确定图形的形状。画布提供了许多函数用来绘制直线、圆、贝塞尔曲线,等等。
一些通用的方法和属性将贯穿以下全部内容:
以下是一个典型的简单路径绘制选项:
ctx.fillStyle = 'rgb(255, 0, 0)';
ctx.beginPath();
ctx.moveTo(50, 50);
// 绘制路径
ctx.fill();
在画布上绘制一个等边三角形。
1、首先,在代码底部添加下面的辅助函数。它可以将角度换算为弧度,在为 JavaScript 提供角度值时非常实用,JS 基本上只接受弧度值,而人类更习惯用角度值。
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);
var triHeight = 50 * Math.tan(degToRad(60));
ctx.lineTo(100, 50+triHeight);
ctx.lineTo(50, 50);
ctx.fill();
逐行解释:
4、有了三角形的高,我们来绘制另一条线,终点坐标为 (100, 50+triHeight)。X 坐标值很简单,应在刚才绘制的水平线两顶点正中间位置。Y 值应为 50 加上三角形的高,因为高即三角形底边到顶点的距离
5、下一条线的终点坐标为绘制整个三角形的起点坐标。
6、最后,运行 ctx.fill() 来终止路径,并为图形填充颜色。
可在画布中绘制圆的方法—— arc() ,通过连续的点来绘制整个圆或者弧(arc,即局部的圆)。
1、在代码中添加以下几行,以向画布中添加一条弧。
ctx.fillStyle = 'rgb(0, 0, 255)';
ctx.beginPath();
ctx.arc(150, 106, 50, degToRad(0), degToRad(360), false);
ctx.fill();
arc() 函数有六个参数。前两个指定圆心的位置坐标,第三个是圆的半径,第四、五个是绘制弧的起、止角度(给定 0° 和 360° 便能绘制一个完整的圆),第六个是绘制方向(false 是顺时针,true 是逆时针)。
注:0° 设定为水平向右。
2、我们再来画一条弧:
ctx.fillStyle = 'yellow';
ctx.beginPath();
ctx.arc(200, 106, 50, degToRad(-45), degToRad(45), true);
ctx.lineTo(200, 106);
ctx.fill();
模式基本一样,但有两点不同:
learn more:用画布绘图 入门课程
以下两个函数用于绘制文本:
这两个函数有三个基本的参数:需要绘制的文字、文本框(顾名思义,围绕着需要绘制文字的方框)左上顶点的X、Y坐标。
还有一系列帮助控制文本渲染的属性:比如用于指定字体族、字号的 font,它的值和语法与 CSS 的 font 属性一致。
在 JS 代码底部添加以下内容:
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);
将绘制两行文字,一行描边文字一行填充颜色的文字
绘制文本 获得关于画布文本选项的更多信息
可在画布上渲染外部图片,简单图片文件、视频帧、其他画布内容都可以。这里我们只考虑简单图片文件的情况:
1、 drawImage() 方法可将图片绘制在画布上。 最简单的版本需要三个参数:需要渲染的图片、图片左上角的X、Y坐标。
2、将图片源嵌入画布中,代码如下:
var 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);
不学习动画你就无法体会画布的强大。画布是提供可编程图形的。在画布中使用循环是件有趣的事,你可以在 for 循环中运行画布命令,和其他 JS 代码一样。
1.在画布模板 js 末尾创建新方法 translate(),可用于移动画布的原点。
ctx.translate(width/2, height/2);
这会使原点 (0, 0) 从画布左上顶点移动至画布正中心。这个功能在许多场合非常实用,就像本示例,我们的绘制操作都是围绕着画布的中心点展开的。
2、在 JS 代码末尾添加以下内容:
function degToRad(degrees) {
return degrees * Math.PI / 180;
};
function rand(min, max) {
return Math.floor(Math.random() * (max-min+1)) + (min);
}
var length = 250;
var moveOffset = 20;
for(var i = 0; i < length; i++) {
}
这里我们实现了一个与上文三角形示例中相同的 degToRad() 函数、一个返回给定范围内随机数rand()函数、length 和 moveOffset 变量(见下文),以及一个空的 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);
var 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));
在每次迭代中:
在重度画布应用(比如游戏或实时可视化)中恒定循环是至关重要的支持组件。如果期望画布显示的内容像一部电影,屏幕最好能够以 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);
while(balls.length < 25) {
var ball = new Ball();
balls.push(ball);
}
for(i = 0; i < balls.length; i++) {
balls[i].draw();
balls[i].update();
balls[i].collisionDetect();
}
requestAnimationFrame(loop);
}
loop();
我们在代码底部运行了一次 loop() 函数,它启动了整个循环,绘制了第一帧动画。接着 loop() 函数接管了requestAnimationFrame(loop) 的调用工作,即运行下一帧、再下一帧……的动画。
请注意每一帧我们都整体清除画布并重新渲染所有内容。(每帧创建一个新球(25 个封顶),然后绘制每个球,更新它们的位置,检查是否撞到了其它球。)向画布中绘制的新图形不能像 DOM 元素那样单独操作。你无法再画布中单独操作某一个球,因为只要绘制完毕了,它就是画布的一部分,而不是一个单独的球。你需要擦除再重画,可以将整帧擦除再重画整个画面,也可通过编程选择最小的部分进行擦除和重画。
一般地,在画布上制作动画需要以下步骤:
注:save() 和 restore() 这里暂不展开,可以访问 变形 教程(及后续内容)来获取详细信息。
1、画布模板,下载 walk-right.png 并放在同一文件夹。
2、在 JS 代码末尾添加下面一行,再次将画布的原点设置为中心点。
ctx.translate(width/2, height/2);
3、创建一个新的 HTMLImageElement 对象,把它的 src 设置为所需图片,添加一个 onload 事件处理器,使 draw() 函数在图片载入后触发。
var image = new Image();
image.src = 'walk-right.png';
image.onload = draw;
4、添加一些变量,来追踪精灵图在屏幕上的位置,以及当前需要显示的精灵图的序号。
var sprite = 0;
var posX = 0;
我们来解释一下“精灵图序列。图中包含六个精灵,它们组成了一趟完整的行走序列。每个精灵的尺寸为 102 × 148 像素。为了整齐的显示一个精灵,可以通过 drawImage() 来从序列中裁切出单独的精灵并隐藏其他部分,就像上文中操作 Firefox 图标的方法。切片的 X 坐标应为 102 的倍数,Y 坐标恒为 0。切片尺寸恒为 102 × 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);
8、现在,我们在每帧绘制完毕(部分完毕)后修改 sprite 的值。在 draw() 函数底部添加以下内容:
if (posX % 13 === 0) {
if (sprite === 5) {
sprite = 0;
} else {
sprite++;
}
}
将整个功能块放置在 if (posX % 13 === 0) { ... } 内。用“模(%)运算符”(即 求余运算符)来检测 posX 是否可以被 13 整除。如果整除,则通过增加 sprite 的值转至下一个精灵(到 5 号精灵时归零)。这实际上意味着每隔 13 帧才更新一次精灵,每秒大约更新 5 帧(requestAnimationFrame() 每秒最多调用 60 帧)。我们故意放慢了帧率,因为精灵图只有六个,且如果每秒显示 60 帧的话,这个角色就会快到起飞。
外部程序块中用一个 if...else 语句来检测 sprite 的值是否为 5(精灵序号在 0 - 5 间循环,因此 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 的值是否超出了 width/2,那意味着角色走到了屏幕右侧边缘。如果这样就计算出一个让角色出现在屏幕左侧边缘的 X 坐标,然后将 posX 设置为最接近这个数的 13 的倍数。这里必须限定 13 的倍数这个条件,这是因为 posX 不可能是 13 的倍数,若不限定的话上一段代码就不会运行了。
如果角色没有走到屏幕边缘,只需为 posX 加 2。这将让他在下次绘制时更靠右些。
10、最后,通过在 draw() 函数末尾添加 requestAnimationFrame() 调用以实现动画的循环。
window.requestAnimationFrame(draw);
动画循环与用户输入(本例中为鼠标移动)结合起来
来看看代码的精华部分:先,用 curX、curY 和 pressed 这三个变量来跟踪鼠标的 X、Y 坐标和点击状态。当鼠标移动时,触发一个函数作为 onmousemove 事件处理器,其应捕获当前的 X 和 Y 值。再用 onmousedown 和 onmouseup 事件处理器来修改鼠标键按下时 pressed 的值(按下为 true,释放为 false)。
var curX;
var curY;
var 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;
}
在按下 Clear canvas(清除画布)按钮时,我们运行一个简单的函数来清除整个画布的内容至纯黑色,和刚才的方法一致:
clearBtn.onclick = function() {
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(0, 0, width, height);
}
这次的绘图循环非常简单,如果 pressed 为 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();
注:range 和 color 两个 <input> 的类型有良好的跨浏览器支持,但 Safari 暂不支持 color,IE 10 以下版本两者均不支持。如果你的浏览器不支持这些输入类型,它们将降格为简单文字输入区域,可以直接输入合法的数字来表示半径和颜色的值。
2D 内容告一段落,现在简单了解一下 3D 画布。3D 画布内容可通过的 WebGL API 实现,尽管它和 2D canvas API 都可在 元素上进行渲染,但两者是彼此独立的。
WebGL 基于 OpenGL 图形编程语言实现,可直接与 GPU 通信,基于此,编写纯 WebGL 代码与常规的 JavaScript 不尽相同,更像 C++ 那样的底层语言,更加复杂,但无比强大。
由于 3D 绘图的复杂性,大多数人写代码时会使用第三方 JavaScript 库(比如 Three.js、PlayCanvas 或 Babylon.js)。大多数库的原理都基本类似,提供创建基本的、自定义性状的功能、视图定位摄影和光效、表面纹理覆盖,等等。库负责 与 WebGL 通信,你只需完成更高阶工作。
接触任何一个库都意味着要学一套全新的API(这里是第三方的版本),但与纯 WebGL 编程都大同小异。
看一个简单的示例,用一套 WebGL 库(这里我们选择 Three.js,最流行的 3D 绘图库之一)来创建我们在本文开头看到的旋转魔方。
1、首先,下载 index.html、metal003.png 并保存在同一个文件夹。图片将用于魔方的表面纹理。
2、在 main.js 中添加新的代码
var scene = new THREE.Scene();
Scene() 构造器创建一个新的场景,表示即将显示的整个 3D 世界。
3、下一步,我们需要一部摄影机来看到整个场景。在 3D 绘图语境中,摄影机表示观察者在世界里的位置,可通过下面代码创建一部摄影机:
var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
PerspectiveCamera() 构造器有四个参数:
将摄像机的位置设定为距 Z 轴 5 个距离单位的位置。与 CSS 类似,在屏幕之外你(观察者)的位置。
4、第三个重要参数是渲染器。我们用它来渲染给定的场景,可通过给定位值得摄影机观察。现在我们使用 WebGLRenderer() 构造器创建一个渲染器供稍后使用。添加以下代码:
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
第一行创建一个新的渲染器,第二行设定渲染器在当前摄影机视角下的尺寸,第三行将渲染好的 对象加入HTML的 中。现在渲染器绘制的内容将在窗口中显示出来。
5、下一步,在画布中创建魔方。把以下代码添加到 JS 文件中:
var cube;
var loader = new THREE.TextureLoader();
loader.load( 'metal003.png', function (texture) {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(2, 2);
var geometry = new THREE.BoxGeometry(2.4, 2.4, 2.4);
var material = new THREE.MeshLambertMaterial( { map: texture, shading: THREE.FlatShading } );
cube = new THREE.Mesh(geometry, material);
scene.add(cube);
draw();
});
首先,创建一个全局变量 cube,这样就可以在代码任意位置访问我们的魔方。
然后,创建一个 TextureLoader 对象,并调用 load()。 这里 load() 包含两个参数(其它情况可以有更多参数):需要调用的纹理图(PNG 文件)和纹理加载成功后调用的函数。
函数内部,我们用 texture 对象的属性指明我们要在魔方的每个面渲染 2 × 2 的图片,然后创建一个 BoxGeometry 对象和一个 MeshLambertMaterial 对象,将两者作为 Mesh 的参数来创建我们的魔方。 Mesh 一般就需要两个参数:一个几何(形状)和一个素材(形状表面外观)。
最后,将魔方添加进场景中,调用我们的 draw() 函数开始动画。
6、定义 draw() 函数前,我们需要先为场景打光,以照亮场景中的物体。请添加以下代码:
var light = new THREE.AmbientLight('rgb(255, 255, 255)'); // soft white light
scene.add(light);
var spotLight = new THREE.SpotLight('rgb(255, 255, 255)');
spotLight.position.set( 100, 1000, 1000 );
spotLight.castShadow = true;
scene.add(spotLight);
AmbientLight 对象是可以轻度照亮整个场景的柔光,就像户外的阳光。而 SpotLight 对象是直射的硬光,就像闪光灯和手电筒(或者它的英文字面意思——聚光灯)。
7、最后,在代码末尾添加我们的 draw() 函数:
function draw() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
requestAnimationFrame(draw);
}
这段代码很直观,每一帧我们都沿 X 轴 和 Y 轴将魔方轻微转动,然后按摄像机视角渲染场景,最后调用 requestAnimationFrame() 来准备下一帧。
你可以 到 Github 下载最终代码。
这里只涉及到画布最为基本的内容,以下内容帮你探索更多:
Violent theramin:用 Web 音频 API 创建声音,用画布显示漂亮的视觉效果以配合音乐。
Voice change-o-matic:用画布为 Web 音频 API 产生的音效提供实时的视觉效果。

