js中的动画与Canvas图


<canvas>是 HTML5 最受欢迎的新特性。这个元素会占据一块页面区域,让 JavaScript可以动态在上面绘制图片。

使用 requestAnimationFrame

Firefox 4 率先在浏览器中为 JavaScript动画增加了一个名为 mozRequestAnimationFrame()方法的 API。这个方法会告诉浏览器要执行动画了,于是浏览器可以通过最优方式确定重绘的时序。现在作为requestAnimationFrame()方法已经得到各大浏览器的支持。

早期定时动画

早期,JavaScript 中创建动画基本上就是使用 setInterval()来控制动画的执行。

(function() { 
 function updateAnimations() { 
 doAnimation1(); 
 doAnimation2(); 
 // 其他任务
 } 
 setInterval(updateAnimations, 100); 
})();

定时动画的问题在于无法准确知晓循环之间的延时。定时间隔必须足够短,这样才能让不同的动画类型都能平滑顺畅,但又要足够长,以便产生浏览器可以渲染出来的变化。实现平滑动画最佳的重绘间隔为 1000 毫秒/60,大约 17 毫秒。但是不能保证时间精度的。作为第二个参数的延时只能保证何时会把代码添加到浏览器的任务队列,不能保证添加到队列就会立即运行。如果队列前面还有其他任务,那么就要等这些任务执行完再执行。简单来讲,这里毫秒延时并不是说何时这些代码会执行,而只是说到时候会把回调加到任务队列。如果添加到队列后,主线程还被其他任务占用,比如正在处理用户操作,那么回调就不会马上执行。

时间间隔的问题

浏览器的计时器精度不足毫秒。

  • IE8 及更早版本的计时器精度为 15.625 毫秒;
  • IE9 及更晚版本的计时器精度为 4 毫秒;
  • Firefox 和 Safari 的计时器精度为约 10 毫秒;
  • Chrome 的计时器精度为 4 毫秒。

requestAnimationFrame

requestAnimationFrame()方法接收一个参数,此参数是一个要在重绘屏幕前调用的函数。这个函数就是修改 DOM 样式以反映下一次重绘有什么变化的地方。为了实现动画循环,可以把多个
requestAnimationFrame()调用串联起来,就像以前使用setTimeout()时一样:

function updateProgress() { 
 var div = document.getElementById("status"); 
 div.style.width = (parseInt(div.style.width, 10) + 5) + "%"; 
 if (div.style.left != "100%") { 
 requestAnimationFrame(updateProgress); 
 } 
} 
requestAnimationFrame(updateProgress);

传给 requestAnimationFrame()的函数实际上可以接收一个参数,此参数是一个 DOMHighResTimeStamp 的实例(比如 performance.now()返回的值),表示下次重绘的时间。这一点非常重要:requestAnimationFrame()实际上把重绘任务安排在了未来一个已知的时间点上,而且通过这个参数告诉了开发者。

requestAnimationFrame()也返回一个请求 ID,可以用于通过另一个方法 cancelAnimationFrame()来取消重绘任务。

let requestID = window.requestAnimationFrame(() => { 
 console.log('Repaint!'); 
}); 
window.cancelAnimationFrame(requestID);

通过 requestAnimationFrame 节流

支持这个方法的浏览器会暴露出作为钩子的回调队列。所谓钩子(hook),就是浏览器在执行下一次重绘之前的一个点。这个回调队列是一个可修改的函数列表,包含应该在重绘之前调用的函数。每次调用requestAnimationFrame()都会在队列上推入一个回调函数,队列的长度没有限制。

通过 requestAnimationFrame()递归地向队列中加入回调函数,可以保证每次重绘最多只调用一次回调函数。

滚动事件监听器每次触发都会调用名为 expensiveOperation()(耗时操作)的函数。当向下滚动网页时,这个事件很快就会被触发并执行成百上千次:

function expensiveOperation() { 
 console.log('Invoked at', Date.now()); 
} 
window.addEventListener('scroll', () => { 
 expensiveOperation(); 
});

如果想把事件处理程序的调用限制在每次重绘前发生,那么可以像这样下面把它封装到requestAnimationFrame()调用中:

function expensiveOperation() { 
 console.log('Invoked at', Date.now()); 
} 
window.addEventListener('scroll', () => { 
 window.requestAnimationFrame(expensiveOperation); 
});

这样会把所有回调的执行集中在重绘钩子,但不会过滤掉每次重绘的多余调用。此时,定义一个标志变量,由回调设置其开关状态,就可以将多余的调用屏蔽:

let enqueued = false; 
function expensiveOperation() { 
 console.log('Invoked at', Date.now()); 
 enqueued = false; 
} 
window.addEventListener('scroll', () => { 
 if (!enqueued) { 
 enqueued = true; 
 window.requestAnimationFrame(expensiveOperation); 
 } 
});

基本的画布功能

创建<canvas>元素时至少要设置其 width 和 height 属性,这样才能告诉浏览器在多大面积上绘图。出现在开始和结束标签之间的内容是后备数据,会在浏览器不支持<canvas>元素时显示。

<canvas id="drawing" width="200" height="200">A drawing of something.</canvas>

要在画布上绘制图形,首先要取得绘图上下文。使用 getContext()方法可以获取对绘图上下文的引用。对于平面图形,需要给这个方法传入参数"2d",表示要获取 2D 上下文对象:

let drawing = document.getElementById("drawing"); 
// 确保浏览器支持<canvas> 
if (drawing.getContext) { 
 let context = drawing.getContext("2d"); 
 // 其他代码
}

可以使用 toDataURL()方法导出<canvas>元素上的图像。这个方法接收一个参数:要生成图像的 MIME 类型(与用来创建图形的上下文无关)。例如,要从画布上导出一张 PNG 格式的图片,可以这样做:

let drawing = document.getElementById("drawing"); 
// 确保浏览器支持<canvas> 
if (drawing.getContext) { 
 // 取得图像的数据 URI 
 let imgURI = drawing.toDataURL("image/png"); 
 // 显示图片
 let image = document.createElement("img"); 
 image.src = imgURI; 
 document.body.appendChild(image); 
}

2D 绘图上下文

2D 绘图上下文提供了绘制 2D 图形的方法,包括矩形、弧形和路径。2D 上下文的坐标原点(0, 0)在<canvas>元素的左上角。所有坐标值都相对于该点计算,因此 x 坐标向右增长,y 坐标向下增长。

填充和描边

2D 上下文有两个基本绘制操作:填充和描边。填充以指定样式(颜色、渐变或图像)自动填充形状,而描边只为图形边界着色。大多数 2D 上下文操作有填充和描边的变体,显示效果取决于两个属性:fillStyle 和 strokeStyle。

这两个属性可以是字符串、渐变对象或图案对象,默认值都为"#000000"。字符串表示颜色值,可以是 CSS 支持的任意格式:名称、十六进制代码、rgb、rgba、hsl 或 hsla。

let drawing = document.getElementById("drawing"); 
// 确保浏览器支持<canvas> 
if (drawing.getContext) { 
 let context = drawing.getContext("2d"); 
 context.strokeStyle = "red"; 
 context.fillStyle = "#0000ff"; 
}

绘制矩形

矩形是唯一一个可以直接在 2D 绘图上下文中绘制的形状。与绘制矩形相关的方法有 3 个:fillRect()、strokeRect()和 clearRect()。这些方法都接收 4 个参数:矩形 x 坐标、矩形 y 坐标、矩形宽度和矩形高度。这几个参数的单位都是像素。

fillRect()方法用于以指定颜色在画布上绘制并填充矩形。填充的颜色使用 fillStyle 属性指定。

let drawing = document.getElementById("drawing"); 
// 确保浏览器支持<canvas> 
if (drawing.getContext) { 
 let context = drawing.getContext("2d"); 
 /* 
 * 引自 MDN 文档
 */ 
 // 绘制红色矩形
 context.fillStyle = "#ff0000"; 
 context.fillRect(10, 10, 50, 50); 
 // 绘制半透明蓝色矩形
 context.fillStyle = "rgba(0,0,255,0.5)"; 
 context.fillRect(30, 30, 50, 50); 
}

strokeRect()方法使用通过 strokeStyle 属性指定的颜色绘制矩形轮廓。

let drawing = document.getElementById("drawing"); 
// 确保浏览器支持<canvas> 
if (drawing.getContext) { 
 let context = drawing.getContext("2d"); 
 /* 
 * 引自 MDN 文档
 */ 
 // 绘制红色轮廓的矩形
 context.strokeStyle = "#ff0000"; 
 context.strokeRect(10, 10, 50, 50); 
 // 绘制半透明蓝色轮廓的矩形
 context.strokeStyle = "rgba(0,0,255,0.5)"; 
 context.strokeRect(30, 30, 50, 50); 
}

使用 clearRect()方法可以擦除画布中某个区域。该方法用于把绘图上下文中的某个区域变透明。通过先绘制形状再擦除指定区域,可以创建出有趣的效果,比如从已有矩形中开个孔。

let drawing = document.getElementById("drawing"); 
// 确保浏览器支持<canvas> 
if (drawing.getContext) { 
 let context = drawing.getContext("2d"); 
 /* 
 * 引自 MDN 文档
 */ 
 // 绘制红色矩形
 context.fillStyle = "#ff0000"; 
 context.fillRect(10, 10, 50, 50); 
 // 绘制半透明蓝色矩形
 context.fillStyle = "rgba(0,0,255,0.5)"; 
 context.fillRect(30, 30, 50, 50); 
 // 在前两个矩形重叠的区域擦除一个矩形区域
 context.clearRect(40, 40, 10, 10); 
}

绘制路径

2D 绘图上下文支持很多在画布上绘制路径的方法。通过路径可以创建复杂的形状和线条。要绘制路径,必须首先调用 beginPath()方法以表示要开始绘制新路径。然后,再调用下列方法来绘制路径。

  • arc(x, y, radius, startAngle, endAngle,counterclockwise):以坐标(x, y)为圆心,以 radius 为半径绘制一条弧线,起始角度为 startAngle,结束角度为 endAngle(都是弧度)。最后一个参数 counterclockwise 表示是否逆时针计算起始角度和结束角度(默认为顺时针)。
  • arcTo(x1, y1, x2, y2, radius):以给定半径 radius,经由(x1, y1)绘制一条从上一点到(x2, y2)的弧线。
  • bezierCurveTo(c1x, c1y, c2x, c2y, x, y):以(c1x, c1y)和(c2x, c2y)为控制点,绘制一条从上一点到(x, y)的弧线(三次贝塞尔曲线)。
  • lineTo(x, y):绘制一条从上一点到(x, y)的直线。
  • moveTo(x, y):不绘制线条,只把绘制光标移动到(x, y)。
  • quadraticCurveTo(cx, cy, x, y):以(cx, cy)为控制点,绘制一条从上一点到(x, y)的弧线(二次贝塞尔曲线)。
  • rect(x, y, width, height):以给定宽度和高度在坐标点(x, y)绘制一个矩形。这个方法与 strokeRect()和 fillRect()的区别在于,它创建的是一条路径,而不是独立的图形。

创建路径之后,可以使用 closePath()方法绘制一条返回起点的线。如果路径已经完成,则既可以指定 fillStyle 属性并调用 fill()方法来填充路径,也可以指定 strokeStyle 属性并调用stroke()方法来描画路径,还可以调用 clip()方法基于已有路径创建一个新剪切区域。

let drawing = document.getElementById("drawing"); 
// 确保浏览器支持<canvas> 
if (drawing.getContext) { 
 let context = drawing.getContext("2d"); 
 // 创建路径
 context.beginPath(); 
 // 绘制外圆
 context.arc(100, 100, 99, 0, 2 * Math.PI, false); 
 // 绘制内圆
 context.moveTo(194, 100); 
 context.arc(100, 100, 94, 0, 2 * Math.PI, false); 
 // 绘制分针
 context.moveTo(100, 100); 
 context.lineTo(100, 15); 
 // 绘制时针
 context.moveTo(100, 100); 
 context.lineTo(35, 100); 
 // 描画路径
 context.stroke(); 
}

在这里插入图片描述
路径经常被使用,所以也有一个 isPointInPath()方法,接收 x 轴和 y 轴坐标作为参数。这个方法用于确定指定的点是否在路径上,可以在关闭路径前随时调用。

if (context.isPointInPath(100, 100)) { 
 alert("Point (100, 100) is in the path."); 
}

绘制文本

fillText()和 strokeText()。这两个方法都接收 4 个参数:要绘制的字符串、x 坐标、y 坐标和可选的最大像素宽度。这两个方法最终绘制的结果都取决于以下 3 个属性。

  • font:以 CSS 语法指定的字体样式、大小、字体族等,比如"10px Arial"。
  • textAlign:指定文本的对齐方式,可能的值包括"start"、“end”、“left”、“right"和"center”。推荐用"start"和"end",不使用"left"和"right",因为前者无论在从左到右书写的语言还是从右到左书写的语言中含义都更明确。
  • textBaseLine :指定文本的基线,可能的值包括 “top” 、 “hanging” 、 “middle”、“alphabetic”、“ideographic"和"bottom”。

fillText()方法是使用最多的,因为它模拟了在网页中渲染文本。

context.font = "bold 14px Arial"; 
context.textAlign = "center"; 
context.textBaseline = "middle"; 
context.fillText("12", 100, 20);

在这里插入图片描述

// 正常
context.font = "bold 14px Arial"; 
context.textAlign = "center"; 
context.textBaseline = "middle"; 
context.fillText("12", 100, 20); 
// 与开头对齐
context.textAlign = "start"; 
context.fillText("12", 100, 40); 
// 与末尾对齐
context.textAlign = "end"; 
context.fillText("12", 100, 60);

在这里插入图片描述
2D 上下文提供了用于辅助确定文本大小的 measureText()方法。这个方法接收一个参数,即要绘制的文本,然后返回一个TextMetrics 对象。

measureText()方法使用 font、textAlign 和 textBaseline 属性当前的值计算绘制指定文本后的大小。例如,假设要把文本"Hello world!"放到一个 140 像素宽的矩形中,可以使用以下代码,从 100 像素的字体大小开始计算,不断递减,直到文本大小合适

let fontSize = 100; 
context.font = fontSize + "px Arial"; 
while(context.measureText("Hello world!").width > 140) { 
 fontSize--; 
 context.font = fontSize + "px Arial"; 
} 
context.fillText("Hello world!", 10, 10); 
context.fillText("Font size is " + fontSize + "px", 10, 50);

fillText()和 strokeText()方法还有第四个参数,即文本的最大宽度。这个参数是可选的,如果调用 fillText()和 strokeText()时提供了此参数,但要绘制的字符串超出了最大宽度限制,则文本会以正确的字符高度绘制,这时字符会被水平压缩,以达到限定宽度。

变换

变换可以操作绘制在画布上的图像。

  • rotate(angle):围绕原点把图像旋转 angle 弧度。
  • scale(scaleX, scaleY):通过在 x 轴乘以 scaleX、在 y 轴乘以 scaleY 来缩放图像。scaleX和 scaleY 的默认值都是 1.0。
  • translate(x, y):把原点移动到(x, y)。执行这个操作后,坐标(0, 0)就会变成(x, y)。
  • transform(m1_1, m1_2, m2_1, m2_2, dx, dy):像下面这样通过矩阵乘法直接修改矩阵。
    m1_1 m1_2 dx
    m2_1 m2_2 dy
    0 0 1
  • setTransform(m1_1, m1_2, m2_1, m2_2, dx, dy):把矩阵重置为默认值,再以传入的参数调用 transform()。

在前面绘制表盘的例子中,如果把坐标原点移动到表盘中心,那再绘制表针就非常简单了:

let drawing = document.getElementById("drawing"); 
// 确保浏览器支持<canvas> 
if (drawing.getContext) { 
 let context = drawing.getContext("2d"); 
 // 创建路径
 context.beginPath(); 
 // 绘制外圆
 context.arc(100, 100, 99, 0, 2 * Math.PI, false); 
 // 绘制内圆
 context.moveTo(194, 100); 
 context.arc(100, 100, 94, 0, 2 * Math.PI, false); 
 // 移动原点到表盘中心
 context.translate(100, 100); 
 // 绘制分针
 context.moveTo(0, 0); 
 context.lineTo(0, -85); 
 // 绘制时针
 context.moveTo(0, 0); 
 context.lineTo(-65, 0); 
 // 描画路径
 context.stroke(); 
}

所有这些变换,包括 fillStyle 和 strokeStyle 属性,会一直保留在上下文中,直到再次修改它们。有两个方法可以帮我们跟踪变化。如果想着什么时候再回到当前的属性和变换状态,可以调用 save()方法。调用这个方法后,所有这一时刻的设置会被放到一个暂存栈中。保存之后,可以继续修改上下文。而在需要恢复之前的上下文时,可以调用restore()方法。这个方法会从暂存栈中取出并恢复之前保存的设置。多次调用 save()方法可以在暂存栈中存储多套设置,然后通过 restore()可以系统地恢复。

context.fillStyle = "#ff0000"; 
context.save(); 
context.fillStyle = "#00ff00"; 
context.translate(100, 100); 
context.save(); 
context.fillStyle = "#0000ff"; 
context.fillRect(0, 0, 100, 200); // 在(100, 100)绘制蓝色矩形
context.restore(); 
context.fillRect(10, 10, 100, 200); // 在(100, 100)绘制绿色矩形
context.restore(); 
context.fillRect(0, 0, 100, 200); // 在(0, 0)绘制红色矩形

绘制图像

2D 绘图上下文内置支持操作图像。如果想把现有图像绘制到画布上,可以使用 drawImage()方法。这个方法可以接收 3 组不同的参数,并产生不同的结果。最简单的调用是传入一个 HTML 的<img>元素,以及表示绘制目标的 x 和 y 坐标,结果是把图像绘制到指定位置。

let image = document.images[0]; 
context.drawImage(image, 10, 10);

如果想改变所绘制图像的大小,可以再传入另外两个参数:目标宽度和目标高度。这里的缩放只影响绘制的图像,不影响上下文的变换矩阵。

context.drawImage(image, 50, 10, 20, 30);

执行之后,图像会缩放到 20 像素宽、30 像素高。

还可以只把图像绘制到上下文中的一个区域。此时,需要给 drawImage()提供 9 个参数:要绘制的图像、源图像 x 坐标、源图像 y 坐标、源图像宽度、源图像高度、目标区域 x 坐标、目标区域 y 坐标、目标区域宽度和目标区域高度。

第一个参数除了可以是 HTML 的<img>元素,还可以是另一个<canvas>元素,这样就会把另一个画布的内容绘制到当前画布上。

阴影

2D 上下文可以根据以下属性的值自动为已有形状或路径生成阴影。

  • shadowColor:CSS 颜色值,表示要绘制的阴影颜色,默认为黑色。
  • shadowOffsetX:阴影相对于形状或路径的 x 坐标的偏移量,默认为 0。
  • shadowOffsetY:阴影相对于形状或路径的 y 坐标的偏移量,默认为 0。
  • shadowBlur:像素,表示阴影的模糊量。默认值为 0,表示不模糊。

这些属性都可以通过 context 对象读写。只要在绘制图形或路径前给这些属性设置好适当的值,阴影就会自动生成。

let context = drawing.getContext("2d"); 
// 设置阴影
context.shadowOffsetX = 5; 
context.shadowOffsetY = 5; 
context.shadowBlur = 4; 
context.shadowColor = "rgba(0, 0, 0, 0.5)"; 
// 绘制红色矩形
context.fillStyle = "#ff0000"; 
context.fillRect(10, 10, 50, 50); 
// 绘制蓝色矩形
context.fillStyle = "rgba(0,0,255,1)"; 
context.fillRect(30, 30, 50, 50);

得到结果
在这里插入图片描述

渐变

渐变通过 CanvasGradient 的实例表示。要创建一个新的线性渐变,可以调用上下文的 createLinearGradient()方法。这个方法接收 4 个参数:起点 x 坐标、起点 y 坐标、终点 x 坐标和终点 y 坐标。调用之后,该方法会以指定大小创建一个新的 CanvasGradient对象并返回实例。

接下来要使用 addColorStop()方法为渐变指定色标。这个方法接收两个参数:色标位置和 CSS 颜色字符串。色标位置通过 0~1 范围内的值表示,0 是第一种颜色,1 是最后一种颜色。

let gradient = context.createLinearGradient(30, 30, 70, 70); 
gradient.addColorStop(0, "white"); 
gradient.addColorStop(1, "black");
// 绘制红色矩形
context.fillStyle = "#ff0000"; 
context.fillRect(10, 10, 50, 50); 
// 绘制渐变矩形
context.fillStyle = gradient; 
context.fillRect(30, 30, 50, 50);

在这里插入图片描述
如果矩形没有绘制到渐变的范围内,则只会显示部分渐变。

context.fillStyle = gradient; 
context.fillRect(50, 50, 50, 50);

需要写个函数计算相应的坐标。

function createRectLinearGradient(context, x, y, width, height) { 
 return context.createLinearGradient(x, y, x+width, y+height); 
}
let gradient = createRectLinearGradient(context, 30, 30, 50, 50); 
gradient.addColorStop(0, "white"); 
gradient.addColorStop(1, "black"); 
// 绘制渐变矩形
context.fillStyle = gradient; 
context.fillRect(30, 30, 50, 50);

径向渐变(或放射性渐变)要使用 createRadialGradient()方法来创建。这个方法接收 6 个参数,分别对应两个圆形圆心的坐标和半径。前 3 个参数指定起点圆形中心的 x、y 坐标和半径,后 3 个参数指定终点圆形中心的 x、y 坐标和半径。

图案

图案是用于填充和描画图形的重复图像。要创建新图案,可以调用 createPattern()方法并传入两个参数:一个 HTML<img>元素和一个表示该如何重复图像的字符串。第二个参数的值与 CSS 的background-repeat 属性是一样的,包括"repeat"、“repeat-x”、“repeat-y"和"no-repeat”。

let image = document.images[0], 
 pattern = context.createPattern(image, "repeat"); 
// 绘制矩形
context.fillStyle = pattern; 
context.fillRect(10, 10, 150, 150);

图像数据

2D 上下文中比较强大的一种能力是可以使用 getImageData()方法获取原始图像数据。这个方法接收 4 个参数:要取得数据中第一个像素的左上角坐标和要取得的像素宽度及高度。

返回的对象是一个 ImageData 的实例。每个 ImageData 对象都包含 3 个属性:width、height和 data,其中,data 属性是包含图像的原始像素信息的数组。每个像素在 data 数组中都由 4 个值表示,分别代表红、绿、蓝和透明度值。

合成

2D上下文中绘制的所有内容都会应用两个属性:globalAlpha 和 globalComposition Operation,其中,globalAlpha 属性是一个范围在 0~1 的值(包括 0 和 1),用于指定所有绘制内容的透明度,默认值为 0。

// 绘制红色矩形
context.fillStyle = "#ff0000"; 
context.fillRect(10, 10, 50, 50); 
// 修改全局透明度
context.globalAlpha = 0.5; 
// 绘制蓝色矩形
context.fillStyle = "rgba(0,0,255,1)"; 
context.fillRect(30, 30, 50, 50); 
// 重置
context.globalAlpha = 0;

在这个例子中,蓝色矩形是绘制在红色矩形上面的。因为在绘制蓝色矩形前 globalAlpha 被设置成了 0.5,所以蓝色矩形就变成半透明了,从而可以透过它看到下面的红色矩形。

globalCompositionOperation 属性表示新绘制的形状如何与上下文中已有的形状融合。这个属性是一个字符串,可以取下列值。

  • source-over:默认值,新图形绘制在原有图形上面。
  • source-in:新图形只绘制出与原有图形重叠的部分,画布上其余部分全部透明。
  • source-out:新图形只绘制出不与原有图形重叠的部分,画布上其余部分全部透明。
  • source-atop:新图形只绘制出与原有图形重叠的部分,原有图形不受影响。
  • destination-over:新图形绘制在原有图形下面,重叠部分只有原图形透明像素下的部分可见。
  • destination-in:新图形绘制在原有图形下面,画布上只剩下二者重叠的部分,其余部分完全透明。
  • destination-out:新图形与原有图形重叠的部分完全透明,原图形其余部分不受影响。
  • destination-atop:新图形绘制在原有图形下面,原有图形与新图形不重叠的部分完全透明。
  • lighter:新图形与原有图形重叠部分的像素值相加,使该部分变亮。
  • copy:新图形将擦除并完全取代原有图形。
  • xor:新图形与原有图形重叠部分的像素执行“异或”计算。
// 绘制红色矩形
context.fillStyle = "#ff0000"; 
context.fillRect(10, 10, 50, 50); 
// 设置合成方式
context.globalCompositeOperation = "destination-over"; 
// 绘制蓝色矩形
context.fillStyle = "rgba(0,0,255,1)"; 
context.fillRect(30, 30, 50, 50);

将 globalCompositeOperation 属性的值修改为"destination-over"意味着红色矩形会出现在蓝色矩形上面。

WebGL

WebGL 上下文

在使用上下文之前,应该先检测返回值是否存在

let drawing = document.getElementById("drawing"); 
// 确保浏览器支持<canvas> 
if (drawing.getContext) { 
 let gl = drawing.getContext("webgl"); 
 if (gl){ 
 // 使用 WebGL 
 } 
}

WebGL 基础

可以在调用 getContext()取得 WebGL 上下文时指定一些选项。这些选项通过一个参数对象传入,选项就是参数对象的一个或多个属性。

  • alpha:布尔值,表示是否为上下文创建透明通道缓冲区,默认为 true。
  • depth:布尔值,表示是否使用 16 位深缓冲区,默认为 true。
  • stencil:布尔值,表示是否使用 8 位模板缓冲区,默认为 false。
  • antialias:布尔值,表示是否使用默认机制执行抗锯齿操作,默认为 true。
  • premultipliedAlpha:布尔值,表示绘图缓冲区是否预乘透明度值,默认为 true。
  • preserveDrawingBuffer:布尔值,表示绘图完成后是否保留绘图缓冲区,默认为 false。
let drawing = document.getElementById("drawing"); 
// 确保浏览器支持<canvas> 
if (drawing.getContext) { 
 let gl = drawing.getContext("webgl", { alpha: false }); 
 if (gl) { 
 // 使用 WebGL 
 } 
}

1. 常量

context 对象上的常量则不包含 GL_前缀。例如,GL_COLOR_BUFFER_BIT 常量在WebGL 中要这样访问 gl.COLOR_BUFFER_BIT。WebGL 以这种方式支持大部分 OpenGL 常量(少数常量不支持)。

2. 方法命名

OpenGL(同时也是 WebGL)中的很多方法会包含相关的数据类型信息。接收不同类型和不同数量参数的方法,会通过方法名的后缀体现这些信息。表示参数数量的数字(1~4)在先,表示数据类型的字符串(“f”表示浮点数,“i”表示整数)在后。比如,gl.uniform4f()的意思是需要 4 个浮点数值参数,而 gl.uniform3i()表示需要 3 个整数值参数。还有很多方法接收数组,这类方法用字母“v”(vector)来表示。因此,gl.uniform3iv()就是要
接收一个包含 3 个值的数组参数。

3. 准备绘图

准备使用 WebGL 上下文之前,通常需要先指定一种实心颜色清除<canvas>。为此,要调用clearColor()方法并传入 4 个参数,分别表示红、绿、蓝和透明度值。每个参数必须是 0~1 范围内的值,表示各个组件在最终颜色的强度。

gl.clearColor(0, 0, 0, 1); // 黑色
gl.clear(gl.COLOR_BUFFER_BIT);

4. 视口与坐标

绘图前还要定义 WebGL 视口。默认情况下,视口使用整个<canvas>区域。要改变视口,可以调用viewport()方法并传入视口相对于元素的 x、y 坐标及宽度和高度。视口的 x 和 y 坐标起点(0, 0)表示<canvas>元素的左下角,向上、向右增长可以用点(width–1, height–1)定义。

// 视口是<canvas> 左下角四分之一区域
gl.viewport(0, 0, drawing.width/2, drawing.height/2); 
// 视口是<canvas> 左上角四分之一区域
gl.viewport(0, drawing.height/2, drawing.width/2, drawing.height/2); 
// 视口是<canvas> 右下角四分之一区域
gl.viewport(drawing.width/2, 0, drawing.width/2, drawing.height/2);

5. 缓冲区

在 JavaScript 中,顶点信息保存在定型数组中。要使用这些信息,必须先把它们转换为 WebGL 缓冲区。创建缓冲区要调用 gl.createBuffer()方法,并使用 gl.bindBuffer()方法将缓冲区绑定到WebGL 上下文。绑定之后,就可以用数据填充缓冲区了。

let buffer = gl.createBuffer(); 
gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0.5, 1]), gl.STATIC_DRAW);

调用 gl.bindBuffer()将 buffer 设置为上下文的当前缓冲区。然后,所有缓冲区操作都在buffer 上直接执行。因此,调用 gl.bufferData()虽然没有包含对 buffer 的直接引用,但仍然是在它上面执行的。上面最后一行代码使用一个 Float32Array(通常把所有顶点信息保存在Float32Array 中)初始化了 buffer。如果想输出缓冲区内容,那么可以调用 drawElements()方法并传入 gl.ELEMENT_ARRAY_BUFFER。

gl.bufferData()方法的最后一个参数表示如何使用缓冲区。这个参数可以是以下常量值。

  • gl.STATIC_DRAW:数据加载一次,可以在多次绘制中使用。
  • gl.STREAM_DRAW:数据加载一次,只能在几次绘制中使用。
  • gl.DYNAMIC_DRAW:数据可以重复修改,在多次绘制中使用。

如果不再需要缓冲区,那么最好调用 gl.deleteBuffer()方法释放其占用的内存。

6. 错误

WebGL 操作中通常不会抛出错误。必须在调用可能失败的方法后,调用 gl.getError()方法。这个方法返回一个常量,表示发生的错误类型。

  • gl.NO_ERROR:上一次操作没有发生错误(0 值)。
  • gl.INVALID_ENUM:上一次操作没有传入 WebGL 预定义的常量。
  • gl.INVALID_VALUE:上一次操作需要无符号数值,但是传入了负数。
  • gl.INVALID_OPERATION:上一次操作在当前状态下无法完成。
  • gl.OUT_OF_MEMORY:上一次操作因内存不足而无法完成。
  • gl.CONTEXT_LOST_WEBGL:上一次操作因外部事件(如设备掉电)而丢失了 WebGL 上下文。

7. 着色器

WebGL 中有两种着色器:顶点着色器和片段(或像素)着色器。顶点着色器用于把 3D 顶点转换为可以渲染的 2D 点。片段着色器用于计算绘制一个像素的正确颜色。

编写着色器

GLSL 是一种类似于 C 的语言,专门用于编写 OpenGL 着色器。

每个着色器都有一个 main()方法,在绘制期间会重复执行。给着色器传递数据的方式有两种:attribute 和 uniform。attribute 用于将顶点传入顶点着色器,而 uniform 用于将常量值传入任何着色器。attribute 和 uniform 是在 main()函数外部定义的。在值类型关键字之后是数据类型,然后是变量名。

// OpenGL 着色器语言
// 着色器,摘自 Bartek Drozdz 的文章“Get started with WebGL—draw a square”
attribute vec2 aVertexPosition; 
void main() { 
 gl_Position = vec4(aVertexPosition, 0.0, 1.0); 
}

这个顶点着色器定义了一个名为 aVertexPosition 的 attribute。这个 attribute 是一个包含两项的数组(数据类型为 vec2),代表 x 和 y 坐标。即使只传入了两个坐标,顶点着色器返回的值也会包含 4 个元素,保存在变量 gl_Position 中。这个着色器创建了一个新的包含 4 项的数组(vec4),缺少的坐标会补充上,实际上是把 2D 坐标转换为了 3D 坐标。

片段着色器与顶点着色器类似,只不过是通过 uniform 传入数据。

// OpenGL 着色器语言
// 着色器,摘自 Bartek Drozdz 的文章“Get started with WebGL—draw a square”
uniform vec4 uColor; 
void main() { 
 gl_FragColor = uColor; 
}

片段着色器必须返回一个值,保存到变量 gl_FragColor 中,这个值表示绘制时使用的颜色。这个着色器定义了一个 uniform,包含颜色的 4 个组件(vec4),保存在 uColor 中。

创建着色器程序

通常使用带有自定义 type 属性的<script>元素把着色器代码包含在网页中。如果 type 属性无效,则浏览器不会解析

<script type="x-webgl/x-vertex-shader" id="vertexShader"> 
attribute vec2 aVertexPosition; 
void main() { 
 gl_Position = vec4(aVertexPosition, 0.0, 1.0); 
} 
</script> 
<script type="x-webgl/x-fragment-shader" id="fragmentShader"> 
uniform vec4 uColor; 
void main() { 
 gl_FragColor = uColor; 
} 
</script>

然后可以使用 text 属性提取<script>元素的内容:

let vertexGlsl = document.getElementById("vertexShader").text, 
 fragmentGlsl = document.getElementById("fragmentShader").text;

下一步是创建 shader 对象。为此,需要调用 gl.createShader()方法,并传入想要创建的着色器类型( gl.VERTEX_SHADER 或 gl.FRAGMENT_SHADER )。然后,调用gl.shaderSource()方法把 GLSL 代码应用到着色器,再调用 gl.compileShader()编译着色器。

let vertexShader = gl.createShader(gl.VERTEX_SHADER); 
gl.shaderSource(vertexShader, vertexGlsl); 
gl.compileShader(vertexShader); 
let fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 
gl.shaderSource(fragmentShader, fragmentGlsl); 
gl.compileShader(fragmentShader);

然后,可以通过以下代码把这两个对象链接到着色器程序:

let program = gl.createProgram(); 
gl.attachShader(program, vertexShader); 
gl.attachShader(program, fragmentShader); 
gl.linkProgram(program);

链接到程序之后,就可以通过 gl.useProgram()方法让
WebGL 上下文使用这个程序。

gl.useProgram(program);
给着色器传值

要给着色器传值,必须先找到要接收值的变量。对于 uniform 变量,可以调用 gl.getUniformLocation()方法。这个方法返回一个对象,表示该 uniform 变量在内存中的位置。然后,可以使用这个位置来完成赋值。

let uColor = gl.getUniformLocation(program, "uColor"); 
gl.uniform4fv(uColor, [0, 0, 0, 1]);
调试着色器和程序

着色器操作也可能失败,而且是静默失败。
对于着色器,可以调用 gl.getShaderParameter()方法取得编译之后的编译状态:

if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { 
 alert(gl.getShaderInfoLog(vertexShader)); 
}

着色器程序也可能失败,因此也有类似的方法。gl.getProgramParameter()用于检测状态。

8. 绘图

WebGL 只能绘制三种形状:点、线和三角形。WebGL 绘图要使用 drawArrays()和 drawElements()方法,前者使用数组缓冲区,后者则操作元素数组缓冲区。

drawArrays()和 drawElements()的第一个参数都表示要绘制形状的常量。

  • gl.POINTS:将每个顶点当成一个点来绘制。
  • gl.LINES:将数组作为一系列顶点,在这些顶点间绘制直线。每个顶点既是起点也是终点,因此数组中的顶点必须是偶数个才能开始绘制。
  • gl.LINE_LOOP:将数组作为一系列顶点,在这些顶点间绘制直线。从第一个顶点到第二个顶点绘制一条直线,再从第二个顶点到第三个顶点绘制一条直线,以此类推,直到绘制到最后一个顶点。此时再从最后一个顶点到第一个顶点绘制一条直线。这样就可以绘制出形状的轮廓。
  • gl.LINE_STRIP:类似于 gl.LINE_LOOP,区别在于不会从最后一个顶点到第一个顶点绘制直线。
  • gl.TRIANGLES:将数组作为一系列顶点,在这些顶点间绘制三角形。如不特殊指定,每个三角形都分开绘制,不共享顶点。
  • gl.TRIANGLES_STRIP:类似于 gl.TRIANGLES,区别在于前 3 个顶点之后的顶点会作为第三个顶点与其前面的两个顶点构成三角形。例如,如果数组中包含顶点 A、B、C、D,那么第一个三角形使用 ABC,第二个三角形使用 BCD。
  • gl.TRIANGLES_FAN:类似于 gl.TRIANGLES,区别在于前 3 个顶点之后的顶点会作为第三个顶点与其前面的顶点和第一个顶点构成三角形。例如,如果数组中包含顶点 A、B、C、D,那么第一个三角形使用 ABC,第二个三角形使用 ACD。

以上常量可以作为 gl.drawArrays()方法的第一个参数,第二个参数是数组缓冲区的起点索引,第三个参数是数组缓冲区包含的顶点集合的数量。

9. 纹理

WebGL 纹理可以使用 DOM 中的图片。可以使用 gl.createTexture()方法创建新的纹理,然后再将图片绑定到这个纹理。如果图片还没有加载,则可以创建一个 Image 对象来动态加载。图片加载完成后才能初始化纹理,因此在图片的 load 事件之后才能使用纹理。

let image = new Image(), 
 texture; 
image.src = "smile.gif"; 
image.onload = function() { 
 texture = gl.createTexture(); 
 gl.bindTexture(gl.TEXTURE_2D, texture); 
 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); 
 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); 
 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 
 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 
 // 除当前纹理
 gl.bindTexture(gl.TEXTURE_2D, null); 
}

10. 读取像素

读取像素的 readPixels()方法与OpenGL 中的方法有同样的参数,只不过最后一个参数必须是定型数组。像素信息是从帧缓冲区读出来并放到这个定型数组中的。readPixels()方法的参数包括 x 和 y 坐标、宽度、高度、图像格式、类型和定型数组。前 4 个参数用于指定要读取像素的位置。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值