背景
最近做项目,遇到这样一个场景:需要将视频旋转方位,随时可以切换,且一个页面要展示多个同样的视频源,并且大小和方位不一致。由于该项目是实时通话的场景,底层依赖于外部,不支持多个id播放。因此,复制播放的video就成了解决方案。
视频就是有多张连续的图片在时间上的不断堆叠和切换,切换频率大于我们的视觉反应速度(每秒帧数),于是就有了动态效果,让人感觉不到其实看到的是一张张图片。最传统的电影放映就可以直观地感受到其中的原理,苏乞儿在悟透亢龙有悔也是基于此原理(手动狗头)。最终,我选择了定时从视频源截取图片,使用canvas绘图实现了该功能。
原理
本文将围绕如何实现如上图的旋转进行讲解,不讲canvas绘图入门,主要讲canvas如何实现图像的平移、旋转、伸缩操作。canvas的转换方法有如下几种:
| 方法 | 描述 |
|---|---|
| scale() | 缩放当前绘图至更大或更小 |
| rotate() | 旋转当前绘图 |
| translate() | 重新映射画布上的 (0,0) 位置 |
| transform() | 替换绘图的当前变换矩阵 |
| setTransform() | 将当前转换重置为单位矩阵。然后运行 transform() |
转换方法可以理解为:将原来坐标转换为新坐标的一种运算。scale实现了x轴和y轴的拉伸或者压缩;rotate指的是绕原点旋转多少角度;translate指的是进行平移操作;transform则可以一次性进行缩放、旋转、平移;setTransform和transform一样,区别在于transform是在已有的变换矩阵(稍后讲解)的基础上,继续transform,而setTransform则先把原有转换矩阵置位单位矩阵,再进行transform运算。说人话:transform是相对与目前的状态继续操作;setTransform则是在最初始状态的操作。
变换矩阵是数学线性代数中的一个概念。在线性代数中,线性变换能够用矩阵来表示。
对于二维空间的坐标变换可以用以下公式表示:
canvas 的transform对应的调用方法为: transform(a,b,c,d,e,f)。
如何计算所需要的transform
当我们知道变换前和变换后的位置,就可以求解变换矩阵T。旋转矩阵有6个变量,我们只需要3个点即可(3点决定一个面;六元一次方程需要六个等式,一个点两个变量,所以需要3个点)。
在canvas坐标系中(应该说屏幕坐标系):左上角为原点(0,0),向右为正x轴,向下为正y轴,右下角的坐标为(w,h)。图1变换前后的过程如下所示:
w和h分别为原图片的宽和高,我们选取1、2、3点进行方程的求解。虽然任意选择不共线的3个点都可以求解出来,但是由于1、2、3的0比较多,方便求解,可以得到以下方程组:
可以轻松求解,得出变换矩阵:
js实现代码如下:
<body>
<img id="src" src="./老泉.jpg" />
<canvas id="dist"></canvas>
<script>
const img = document.getElementById("src");
img.onload = () => {
const canvas = document.getElementById("dist");
const ctx = canvas.getContext("2d");
const newWidth = img.width;
const newHeight = img.height;
canvas.width = newHeight; //newHeight
canvas.height = newWidth; // newWidth
ctx.transform(0, 1, 1, 0, 0, 0);
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, newWidth, newHeight);
}
</script>
</body>
其他变换方向或者大小变换可以手工推导试试。如果需要进行缩放验证,需要注意drawImage函数的最后两个参数,最后两个参数表示新图片的宽和高,如有缩放,也需同步变化。可以参考一代码:
<body>
<img id="src" src="./老泉.jpg" />
<canvas id="dist"></canvas>
<script>
const img = document.getElementById("src");
img.onload = () => {
const canvas = document.getElementById("dist");
const ctx = canvas.getContext("2d");
const radio = 0.5;
const newWidth = radio * img.width;
const newHeight = radio * img.height;
canvas.width = newHeight; //newHeight
canvas.height = newWidth; // newWidth
ctx.transform(0, 1, 1, 0, 0, 0);
// ctx.transform(0, 1, -1, 0, newHeight, 0);
// ctx.transform(0, -1, 1, 0, 0, newWidth);
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, newWidth, newHeight);
}
</script>
</body>
其中,注释的行是其他几种旋转的矩阵,有兴趣的读者可以验证一下。