# 使用 requestAnimationFrame

# requestAnimationFrame

用以通知浏览器某些 JavaScript 代码要执行动画了。这样浏览器就可以在运行某些代码后进行适当的优化。

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

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

因为 requestAnimationFrame() 只会调用一次传入的函数,所以每次更新用户界面时需要再手动调用它一次。同样,也需要控制动画何时停止。结果就会得到非常平滑的动画。

传给 requestAnimationFrame() 的函数实际上可以接收一个参数,此参数是一个 DOMHighRes-TimeStamp 的实例(比如 performance.now() 返回的值),表示下次重绘的时间。这一点非常重要:requestAnimationFrame() 实际上把重绘任务安排在了未来一个已知的时间点上,而且通过这个参数告诉了开发者。基于这个参数,就可以更好地决定如何调优动画了。

# cancelAnimationFrame

requestAnimationFrame() 返回一个请求 ID,可以用于通过方法 cancelAnimationFrame() 来取消重绘任务。下面的例子展示了刚把一个任务加入队列又立即将其取消:

let requestId = widow.requestAnimationFrame(() => {
  console.log('Repaint!')
})

window.cancelAnimationFrame(requestId)

# 通过 requestAnimationFrame 节流

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

这个回调队列的行为不一定跟动画有关。不过,通过 requestAnimationFrame() 递归地向队列中加入回调函数,可以保证每次重绘最多只调用一次回调函数。这是一个非常好的节流工具。在频繁执行影响页面外观的代码时(比如滚动事件监听器),可以利用这个回调队列进行节流。

比如下面的例子:

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);
  }
});

因为重绘是非常频繁的操作,所以这还算不上真正的节流。更好的办法是配合使用一个计时器来限制操作执行的频率。这样,计时器可以限制实际的操作执行间隔,而 requestAnimationFrame 控制在浏览器的哪个渲染周期中执行。下面的例子可以将回调限制为不超过 50 毫秒执行一次:

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

# 基本的画布功能

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

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

与其他元素一样,width 和 height 属性也可以在 DOM 节点上设置,因此可以随时修改。整个元素还可以通过 CSS 添加样式,并且元素在添加样式或实际绘制内容前是不可见的;要在画布上绘制图形,首先要取得绘图上下文。使用 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('draeing')

// 确保浏览器支持 canvas
if (drawing.getContext) {
  // 获取图像的数据 URI
  let imgURI = drawing.toDataURL('image/png')

  // 显示图片
  let image = document.createElement('img')
  image.src = imgURI
  document.body.appendChild(image)
}

注意 如果画布中的图像是其他域绘制过来的,toDataURL() 方法就会抛出错误。

# 2D 绘图上下文

2D 上下文的坐标原点(0, 0)在 canvas 元素的左上角。所有坐标值都相对于该点计算,因此 x 坐标向右增长,y 坐标向下增长。默认情况下, width 和 height 表示两个方向上像素的最大值。

# 填充和描边

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')
  // 绘制红色矩形
  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')
  // 绘制红色轮廓的矩形
  context.strokeStyle = '#ff0000'
  context.strokeRect(10, 10, 50, 50)

  // 绘制半透明蓝色轮廓的矩形
  context.strokeStyle = 'rgba(0, 0, 255, 0.5)'
  context.strokeRect(30, 30, 50, 50)
}

注意 描边宽度由 lineWidth 属性控制,它可以是任意整数值。类似地, lineCap 属性控制线条端点的形状[ "butt" (平头)、 "round" (出圆头)或 "square" (出方头)],而 lineJoin 属性控制线条交点的形状[ "round" (圆转)、 "bevel" (取平)或 "miter" (出尖)]。

let drawing = document.getElementById('drawing')
// 确保浏览器支持 canvas
if (drawing.getContext) {
  let context = drawing.getContext('2d')
  // 绘制红色轮廓的矩形
  context.lineWidth = 30
  context.lineCap = 'round'
  context.strokeStyle = '#ff0000'
  context.strokeRect(10, 10, 50, 50)
  // 绘制半透明蓝色轮廓的矩形
  context.lineWidth = 10
  context.lineCap = 'miter'
  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')
  // 绘制红色矩形
  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')

// 确保浏览器可用
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)) {
  console.log('Point (10, 100) is in the path.')
}

# 绘制文本

2D 绘图上下文提供了绘制文本的方法,即 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() 方法使用 fillStyle 属性绘制文本,而 strokeText() 方法使用 strokeStyle 属性。

// 正常
context.font = 'blod 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 对象。这个返回的对象目前只有一个属性 width ,不过将来应该会增加更多度量指标。

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 Wodld!', 10, 10)
context.fillText('Font size is ' + fontSize + 'px', 10, 50)

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

# 变换

上下文变换可以操作绘制在画布上的图像。2D 绘图上下文支持所有常见的绘制变换。在创建绘制上下文时,会以默认值初始化变换矩阵,从而让绘制操作如实应用到绘制结果上。对绘制上下文应用变换,可以导致以不同的变换矩阵应用绘制操作,从而产生不同的结果。

以下方法可用于改变绘制上下文的变换矩阵:

  • 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')

// 确保浏览器可用
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()

  if (context.isPointInPath(100, 100)) {
    console.log('Point (10, 100) is in the path.')
  }
  context.font = 'blod 14px Arial'
  context.textAlign = 'center'
  context.textBaseLine = 'middle'
  context.fillText('12', 100, 20)
}

把原点移动到(100, 100),也就是表盘的中心后,要绘制表针只需简单的数学计算即可。这是因为所有计算都是基于(0, 0),而不是(100, 100)了。当然,也可以使用 rotate() 方法来转动表针:

let drawing = document.getElementById('drawing')

// 确保浏览器可用
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.rotate(1) 

  // 绘制分针
  context.moveTo(0, 0)
  context.lineTo(0, -85)

  // 绘制分针
  context.moveTo(0, 0)
  context.lineTo(-65, 0)

  // 描画路径
  context.stroke()

  if (context.isPointInPath(100, 100)) {
    console.log('Point (10, 100) is in the path.')
  }
  context.font = 'blod 14px Arial'
  context.textAlign = 'center'
  context.textBaseLine = 'middle'
  context.fillText('12', 100, 20)
}

所有这些变换,包括 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) 绘制红色矩形

注意, save() 方法只保存应用到绘图上下文的设置和变换,不保存绘图上下文的内容。

# 绘制对象

可以使用 drawImage() 方法将图像绘制到画布上。这个方法可以接收 3 组不同的参数,并产生不同的结果。

  1. 传入 HTML 的 img 元素以及绘制目标的 x 和 y 坐标,将结果绘制到指定的位置。
let image = document.images[0]
context.drawImage(image, 10, 10)

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

context.drawImage(image, 10, 10, 20, 30)
  1. 把图像绘制到上下文中的一个区域

需要给 drawImage() 提供 9 个参数:要绘制的图像、源图像 x 坐标、源图像 y 坐标、源图像宽度、源图像高度、目标区域 x 坐标、目标区域 y 坐标、目标区域宽度和目标区域高度。这个重载后的 drawImage() 方法可以实现最大限度的控制,比如:

context.drawImage(image, 0, 10, 50, 50, 0, 100, 40, 60)

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

# 阴影

绘制阴影需要根据下面的几个属性来生成:

  • 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)

avatar

# 渐变

渐变通过 CanvasGradient 的实例表示,要创建一个新的线性渐变,可以调用上下文的 createLinearGradient() 方法。这个方法接收 4 个参数:起点 x 坐标、起点 y 坐标、终点 x 坐标和终点 y 坐标。调用之后,该方法会以指定大小创建一个新的 CanvasGradient 对象并返回实例。有了 gradient 对象后,接下来要使用 addColorStop() 方法为渐变指定色标。这个方法接收两个参数:色标位置和 CSS 颜色字符串。色标位置通过 0~1 范围内的值表示,0 是第一种颜色,1 是最后一种颜色。如下代码:

let gradient = context.createLinearGradient(30, 30, 70, 70)
gradient.addColorStop(0, 'white')
gradient.addColorStop(1, 'black')

创建的渐变对象需要赋值给 fillStyle 或者 strokeStyle 属性,来绘制图形:

// 绘制红色矩形
context.fillStyle = '#ff0000'
context.fillRect(10, 10, 50, 50)
// 绘制渐变矩形
context.filStyle = gradient
context.fillRect(30, 30, 50, 50)

avatar

如果矩形没有绘制到渐变的范围内,则只会显示部分渐变。比如:

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

avatar

保持渐变与形状的一致非常重要,有时候可能需要写个函数计算相应的坐标。比如:

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 坐标和半径。在创建径向渐变时,可以把两个圆形想象成一个圆柱体的两个圆形表面。把一个表面定义得小一点,另一个定义得大一点,就会得到一个圆锥体。然后,通过移动两个圆形的圆心,就可以旋转这个圆锥体。

要创建起点圆心在形状中心并向外扩散的径向渐变,需要将两个圆形设置为同心圆。比如,要在前面例子中矩形的中心创建径向渐变,则渐变的两个圆形的圆心都必须设置为(55, 55)。这是因为矩形的起点是(30, 30),终点是(80, 80)。代码如下:

let gradient = context.createRadialGradient(55, 55, 10, 55, 55, 30)

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)

avatar

# 图案

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

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

记住,跟渐变一样,图案的起点实际上是画布的原点(0, 0)。将填充样式设置为图案,表示在指定位置而不是开始绘制的位置显示图案。传给 createPattern() 方法的第一个参数也可以是 video 元素或者另一个 canvas 元素。

# 图像数据

可以使用 getImageData() 方法获取原始图像数据。这个方法接收 4 个参数:要取得数据中第一个像素的左上角坐标和要取得的像素宽度及高度。例如,要从(10, 5) 开始取得 50 像素宽、50 像素高的区域对应的数据,可以这样写:

let imageData = context.getImageData(10, 5, 50, 50)

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

let data = imageData.data
const red = data[0]
const green = data[1]
const blue = data[2]
const alpha = data[3]

这个数组中的每个值都在 0~255 范围内(包括 0 和 255)。对原始图像数据进行访问可以更灵活地操作图像。例如,通过更改图像数据可以创建一个简单的灰阶过滤器:

let drawing = document.getElementById("drawing");
// 确保浏览器支持 canvas
if (drawing.getContext) {
  let context = drawing.getContext("2d"),
  cosnt image = document.images[0];
  // 绘制图像
  context.drawImage(image, 0, 0);
  // 取得图像数据
  const imageData = context.getImageData(0, 0, image.width, image.height);
  const data = imageData.data;
  for (i=0, let len = data.length; i < len; i+=4) {
    const red = data[i];
    const green = data[i+1];
    const blue = data[i+2];
    const alpha = data[i+3];
    // 取得 RGB 平均值
    const average = Math.floor((red + green + blue) / 3);
    // 设置颜色,不管透明度
    data[i] = average;
    data[i+1] = average;
    data[i+2] = average;
  }
  // 将修改后的数据写回 ImageData 并应用到画布上显示出来
  imageData.data = data;
  context.putImageData(imageData, 0, 0);
}

每次循环中取得红、绿、蓝的颜色值,计算出它们的平均值。然后再把原来的值修改为这个平均值,实际上相当于过滤掉了颜色信息,只留下类似亮度的灰度信息。之后将 data 数组重写回 imageData 对象。最后调用 putImageData() 方法,把图像数据再绘制到画布上。结果就得到了原始图像的黑白版。

# 合成

2D 上下文中绘制的所有内容都会应用两个属性:globalAlpha 和 globalCompositionOperation,其中,globalAlpha 属性是一个范围在 0~1 的值(包括 0 和 1),用于指定所有绘制内容的透明度,默认值为 0。如果所有后来的绘制都需要使用同样的透明度,那么可以将 globalAlpha 设置为适当的值,执行绘制,然后再把 globalAlpha 设置为 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;

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)

avatar

使用 globalCompositeOperation 属性时,一定记得要在不同浏览器上进行测试。不同浏览器在实现这些选项时可能存在差异。

# 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
  }
}

如果调用 getContext() 不能创建 WebGL 上下文,某些浏览器就会抛出错误。为此,最好把这个方法调用包装在 try / catch 块中:

let drawing = document.getElementById("drawing");
// 确保浏览器支持<canvas>
if (drawing.getContext) {
  try {
    gl = drawing.getContext("webgl");
  } catch (ex) {
    // 什么也不做
  }
  if (gl) {
    // 使用 WebGL
  } else {
    alert("WebGL context could not be created.");
  }
}
  1. 准备绘图

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

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

以上代码把清理颜色缓冲区的值设置为黑色,然后调用 clear() 方法,这个方法相当于 OpenGL 中的 glClear() 方法。参数 gl.COLOR_BUFFER_BIT 告诉 WebGL 使用之前定义的颜色填充画布。通常,所有绘图操作之前都需要先清除绘制区域。

  1. 视口

绘图前还要定义 WebGL 视口。默认情况下,视口使用整个 canvas 区域。要改变视口,可以调用 viewport() 方法并传入视口相对于 canvas 元素的 x、y 坐标及宽度和高度。例如,以下代码表示要使用整个 canvas 元素:

gl.viewport(0, 0, drawing.width, drawing.height)

这个视口的坐标系统与网页中通常的坐标系统不一样。视口的 x 和 y 坐标起点(0, 0) 表示 canvas 元素的左下角,向上、向右增长可以用点(width–1, height–1)定义

avatar

知道如何定义视口就可以只使用 canvas 元素的一部分来绘图。比如下面的例子:

// 视口是<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);

定义视口的坐标系统与视口中的坐标系统不一样。在视口中,坐标原点(0, 0)是视口的中心点。左下角是(–1, –1),右上角是(1, 1):

avatar

如果绘图时使用了视口外部的坐标,则绘制结果会被视口剪切。

评 论:

更新: 12/27/2020, 4:59:16 PM