Skip to content

Commit 8385872

Browse files
author
程浚哲
committed
feat: canvas
1 parent d59c3e6 commit 8385872

File tree

3 files changed

+107
-6
lines changed

3 files changed

+107
-6
lines changed

.vscode/settings.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"liveServer.settings.port": 5501
3+
}

docs/canvas.md docs/canvas.mdx

+100-5
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,27 @@ canvas.toBlob(blob => {
2020
const dataUrl = canvas.toDataURL("image/jpeg", 1); // quality
2121
```
2222

23+
### captureStream
24+
获取Canvas的媒体流,从而实现Video预览或者媒体录制的能力。
25+
``` js
26+
const stream = canvas.captureStream();
27+
28+
// 预览能力
29+
video.srcObject = stream;
30+
31+
// 录制能力
32+
const recorder = new MediaRecorder(stream);
33+
```
34+
35+
2336
### drawImage
2437

25-
Canvas提供了`drawImage`方法将不同的图像源绘制到我们的目标Canvas上,包括Image、Video甚至另一个Canvas对象。
38+
Canvas提供了`drawImage`方法将不同的图像源绘制到我们的目标Canvas上,包括Image、Video甚至另一个Canvas对象,以及后文会介绍的ImageBitMap
2639

2740
``` js
28-
canvas.drawImage(image, 0, 0)
29-
canvas.drawImage(video, 0, 0)
30-
canvas.drawImage(canvas2, 0, 0)
41+
ctx.drawImage(image, 0, 0)
42+
ctx.drawImage(video, 0, 0)
43+
ctx.drawImage(canvas2, 0, 0)
3144
```
3245

3346
`drawImage`函数是个重载函数,有几种不同的用法。(注:下文中的`d`表示目标Canvas Destination,`s`表示图像源头Source)
@@ -42,6 +55,23 @@ canvas.drawImage(canvas2, 0, 0)
4255

4356

4457

58+
:::info
59+
`drawImage`使用不同的图像源时的行为不同,性能上也略有差异,笔者在M2 Macbook Pro下,将CPU降速6倍下用15000*15000的图片进行性能测试后得出以下结论(例子中图像源已经完全ready)。
60+
1. `drawImage(Image)`,主线程API调用很快,渲染线程绘制上屏较慢(将近3秒)。
61+
2. `drawImage(Canvas)`,主线程API调用很快,渲染线程绘制上屏也很快。**推荐**
62+
3. `drawImage(OffscreenCanvas)`,主线程API调用很慢(将近3秒),渲染线程绘制上屏快。不知道为什么和用例2存在一定的差距,但总的来说需要在主线程缓存虚拟节点的内容的时候,推荐用Canvas而不是OffscreenCanvas。
63+
4. `drawImage(ImageBitmap)`。主线程API调用很快,渲染线程绘制上屏也很快,但本身ImageBitMap的创建也会花费时间(将近3秒)。
64+
65+
在例子2和例子3中,我们需要先通过drawImage把图片绘制到用来缓存的Canvas/OffscreenCanvas上,但我没有立刻同步地把缓存的Canvas绘制到我们的目标Canvas上,而是使用了一个定时器来确保先执行渲染线程,从而保证我们的Canvas图像源本身已经绘制完毕ready了。
66+
此时整体执行顺序如下:`1. 主线程 drawImage(image) -> 2. 渲染线程 把图片绘制到Canvas上 -> 3. 主线程 drawImage(canvas) -> 4. 渲染线程 绘制Canvas到Canvas`
67+
因此我实际上测量的是3和4的总时长,这也是离屏渲染的常见场景————我已经提前缓存好了Canvas,现在关心的是调用drawImage(canvas)时上屏所需要的时间。
68+
69+
在其他的一些场景下,我们会在主线程调用了`drawImage(image)`后立刻调用`drawImage(canvas)`,相当于我们例子中把定时器去掉的效果。
70+
此时整体的执行顺序如下:`1. 主线程 drawImage(image) -> 2. 主线程 drawImage(canvas) -> 3. 渲染线程`
71+
那么此时`drawImage(Canvas)``drawImage(OffscreenCanvas)`都耗时将近3秒,渲染线程上屏则迅速完成,应该是浏览器内部存在相应的优化。
72+
73+
:::
74+
4575
### getImageData/putImageData
4676

4777
通过`getImageData`我们能够拿到Canvas指定区域对应的像素数据。可以通过指定的数学转换实现不同的效果,比如Konva的高斯模糊等滤镜就是通过纯CPU计算实现的。
@@ -58,11 +88,74 @@ for (let i = 0; i < imageData.data.length; i += 4) {
5888
ctx.putImageData(imageData, 0, 0)
5989
```
6090

91+
:::caution
92+
需要特别注意的是,`getImageData``putImageData`都是非常耗CPU的操作,容易造成长任务,尽量使用`drawImage`等方法绘制。
93+
:::
94+
95+
:::info GPU/CPU Canvas
96+
默认情况下Canvas的创建和绘制都是在GPU上的(硬件加速),当我们调用`getImageData`或者`putImageData`时本质都是GPU显存和CPU内存的读写数据,这是个比较耗费性能的操作。如果我们的Canvas存在很频繁的这类读写操作,可以考虑使用`willReadFrequently`标识,这样Canvas的绘制数据都会被存储在CPU内存中,减少读写操作的延时,但同时也会失去GPU硬件加速的能力。
6197

98+
``` js
99+
const ctx = canvas.getContext('2d', {
100+
willReadFrequently: true
101+
})
102+
```
62103

104+
> **willReadFrequently**
105+
>
106+
> A boolean value that indicates whether or not a lot of read-back operations are planned. This will force the use of a software (instead of hardware accelerated) 2D canvas and can save memory when calling getImageData() frequently.
107+
:::
63108

64109

65110
### OffscreenCanvas
111+
为了缓解主线程的压力,我们可以将主线程中的部分计算和绘制放到Web Worker中。但Web Worker环境下无法访问DOM,因此浏览器提供了一个和DOM接耦的Canvas————OffscreenCanvas让我们在Web Worker中使用。它本身的绘制能力和Canvas完全一致,如果我们单纯在主线程中使用它其实和使用普通的Canvas没有任何性能的差别。
112+
113+
114+
#### transferControlToOffscreen
115+
在主线程调用Canvas的`transferControlToOffscreen`方法可以生成一个OffscreenCanvas实例,同时会把自身上下文的所有权转移给该实例。
116+
117+
这意味着我们将无法直接访问Canvas的上下文,但通过OffscreenCanvas却可以拿到Canvas的上下文。而我们可以把OffscreenCanvas传递给Web Worker,即可通过在Web Woker中调用OffscreenCanvas的能力来间接绘制主线程的Canvas。
118+
119+
``` js title="main.js"
120+
const canvas = document.createElement('canvas')
121+
canvas.width = 5000
122+
canvas.height = 5000
123+
document.body.appendChild(canvas)
124+
125+
const offscreenCanvas = canvas.transferControlToOffscreen()
126+
const worker = new Worker('./worker.js')
127+
worker.postMessage({ canvas: offscreenCanvas }, [offscreenCanvas])
128+
```
129+
130+
131+
``` js title="worker.js"
132+
let canvas = null;
133+
self.onmessage = function(evt) {
134+
if (evt.data.canvas) {
135+
canvas = evt.data.canvas;
136+
draw()
137+
}
138+
}
139+
140+
function draw() {
141+
if (canvas) {
142+
const ctx = canvas.getContext('2d');
143+
ctx.fillStyle = 'pink'
144+
ctx.fillRect(0, 0, canvas.width, canvas.height);
145+
requestAnimationFrame(draw)
146+
}
147+
}
148+
```
149+
150+
#### transferToImageBitmap
151+
除了上述的方法外,我们也可以直接在Web Worker中初始化OffscreenCanvas实例并进行绘制操作。而为了将绘制内容同步到主线程的Canvas上,我们首先可能会想到`getImageData`但很明显这太耗性能了,又或者把OffscreenCanvas传递到主线程但我Worker线程后面还要用到所以也不行。因此浏览器给OffscreenCanvas提供了`transferToImageBitmap`的能力来解决这个问题。
152+
153+
在前述章节中我们介绍过,`getImageData`的本质是把GPU显存中的数据写入到CPU内存中,存在不小的性能开销。而`transferToImageBitmap`可以简单理解成GPU显存到GPU显存的传递,即把OffscreenCanvas当前绘制的内容转移到另一块GPU显存空间中,性能是比较好的,并且此时如果再尝试通过`getImageData`读取OffscreenCanvas的数据会发现都已经被重置了。
154+
155+
156+
### ImageBitmap
157+
158+
66159

67160

68161

@@ -88,7 +181,9 @@ function batchDraw() {
88181

89182
#### 离屏渲染
90183

91-
当一个虚拟节点渲染的内容不变时,我们可以将它的内容渲染到一个额外的Canvas(不一定需要是OffscreenCanvas)上,后续借助这个离屏的Canvas进行绘制。
184+
当我们谈论到离屏渲染的技术,总是容易和OffscreenCanvas搞混淆。事实上,OffscreenCanvas的作用就是为了让我们在Web Worker这类的环境下使用Canvas的能力,如果我们单纯在主线程使用OffscreenCanvas,这和直接使用Canvas基本没有区别。
185+
186+
而所谓的离屏渲染,一般指的是我们除了在文档中用于绘制内容的Canvas外,额外创建新的Canvas来缓存绘制的内容。比如当某个虚拟节点要绘制的内容始终不变时,我们可以直接使用DrawImage来把离屏Canvas的内容进行绘制,从而省去了调用Canvas API来重复绘制相同内容的情况,实现性能的优化。
92187

93188

94189

docs/音视频学习.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ const stream3 = audio.captureMedia()
100100
### MediaRecorder
101101

102102

103-
## 媒体扩展(Media Source Extension)
103+
## AudioContext
104+
> TODO
105+
106+
## Media Source Extension
104107

105108
在HTML5中最简单的音视频播放方式是直接将资源链接传入*video*标签的*src*属性。这种加载方式也被称为流式加载,即并不需要等待资源完全下载好后才开始音视频的播放,而是一边加载数据一边进行播放。通过观察控制台可以发现,在加载一个大体积视频的时候,通常会发送多个请求来分段请求该资源,每个请求都会带着`Range: byteds=<start>-`的请求头,响应的状态码是206,响应头包括`Content-Length: <length>; Content-Range: byted <start>-<end>/<length>`
106109

0 commit comments

Comments
 (0)