背景
因为最近看到很多canvas的骚操作,有画龙的、有做动画的,在看他们代码的时候,这些图形处理的方式让我突然想到在大二用python做的一个隐形斗篷的骚操作,于是二话不说就打开github创建了项目,一顿骚操作下,效果终于出来了!!!
动画演示
实现过程
1、先看html里面有什么
<video id="video-red" controls width="350"></video>
<canvas id="canvas-perspective"></canvas>
video用来播放原视频
canvas用来快速绘画(视觉效果约等于播放)原视频画面
2、再看js中的核心代码
import { rgbaConvertToHex } from "./utils/index";
class CanvasPerspective {
constructor(canvasEle, videoEle, colorScope) {
this.videoEle = videoEle;
this.canvasEle = canvasEle;
this.colorScope = colorScope || "red";
this.context = this.canvasEle.getContext("2d");
this.playing = false;
this.gap = 2;
this.backgroundImageData = null;
// 监听video的状态
this.videoEle.addEventListener("play", () => {
console.log("play");
this.playing = true;
// 当video处于播放时去实时绘制当前视频画面
this.drawVideo();
});
this.videoEle.addEventListener("pause", () => {
console.log("pause");
this.playing = false;
});
}
changeColor(color) {
this.colorScope = color;
console.log(color);
}
destroy() {
this.playing = false;
this.backgroundImageData = null;
this.context.clearRect(0, 0, this.canvasEle.width, this.canvasEle.width);
}
drawVideo() {
this.context.drawImage(
this.videoEle,
0,
0,
this.canvasEle.width,
this.canvasEle.height
);
const imageData = this.context.getImageData(
0,
0,
this.canvasEle.width,
this.canvasEle.height
).data;
if (!this.backgroundImageData && imageData[3] !== 0) {
this.backgroundImageData = imageData;
}
for (let i = 0; i < this.canvasEle.height; i += this.gap) {
for (let j = 0; j < this.canvasEle.width; j += this.gap) {
const pos = (i * this.canvasEle.width + j) * 4;
const red = imageData[pos];
const green = imageData[pos + 1];
const blue = imageData[pos + 2];
let flag = false;
if (this.colorScope === "red") {
flag = red > 70 && green < 60 && blue < 60;
} else if (this.colorScope === "green") {
flag = green > 100 && green > red + blue;
} else if (this.colorScope === "blue") {
flag = blue > 100 && blue > red + green;
}
if (flag) {
const [r, g, b, a] = this.backgroundImageData.slice(pos, pos + 4);
this.context.fillStyle = rgbaConvertToHex(r, g, b, a);
this.context.fillRect(j, i, this.gap, this.gap);
this.context.beginPath();
this.context.arc(j, i, this.gap, 0, 2 * Math.PI);
this.context.closePath();
this.context.fill();
}
}
}
this.playing && window.requestAnimationFrame(this.drawVideo.bind(this));
}
}
CanvasPerspective类
- videoEle (保存video对象)
- canvasEle(保存canvas对象)
- colorScope(目标区域的颜色,默认是红色)
- context(canvas的绘画话柄)
- playing(video的播放状态)
- gap(扫描像素点的step)
- backgroundImageData(
原视频第一帧的背景图数据)
drawVideo方法
-
将video当前画面绘画到canvas上
this.context.drawImage( this.videoEle, 0, 0, this.canvasEle.width, this.canvasEle.height ); -
获取canvas图像数据
const imageData = this.context.getImageData( 0, 0, this.canvasEle.width, this.canvasEle.height ).data; -
保存第一帧(背景图)的图像数据
if (!this.backgroundImageData && imageData[3] !== 0) { // 保存第一帧(背景图)的像素数据 this.backgroundImageData = imageData; }- imageData[ 3 ] !== 0 是在使用iphone safari时发现了一个bug,backgroundImageData的数据为[0, 0, 0, 0, .....],即一个
黑色全透明的背景。 - 所以这里判断一下,当第一个像素点的透明度不为0时才认为是原视频的背景(关于imageData数据的解释和运用在后面)
- imageData[ 3 ] !== 0 是在使用iphone safari时发现了一个bug,backgroundImageData的数据为[0, 0, 0, 0, .....],即一个
-
扫描canvas的像素点,拿到扫描到像素点的颜色值([red, green, blue, alpha]),判断当前像素点是否是目标颜色,如果是目标颜色就拿
之前保存的背景图中这个像素点的颜色值去覆盖掉自己。// 从左向右,从上到下去扫描canvas图像 for (let i = 0; i < this.canvasEle.height; i += this.gap) { for (let j = 0; j < this.canvasEle.width; j += this.gap) { /* (i * this.canvasEle.width + j)表示当前遍历到的像素点的index,由于一个像素点在imageData中是以4位数字来表示的(red、green、blue、alpha),所以需要乘以四来得到这个像素点在imageData中的起始位置 */ /* 例:第三个像素点,i=0,j=2,所以在imageData中的位置是(0*width+2) * 4 = 8 红:imageData[8] 绿:imageData[9] 蓝:imageData[10] 透明度:imageData[11] */ const pos = (i * this.canvasEle.width + j) * 4; /* R - 红色(从0到255) G - 绿色(从0到255) B - 蓝色(从0到255) A - Alpha通道(从0到255; 0是透明的,255是完全可见) */ const red = imageData[pos]; const green = imageData[pos + 1]; const blue = imageData[pos + 2]; let flag = false; if (this.colorScope === "red") { flag = red > 70 && green < 60 && blue < 60; } else if (this.colorScope === "green") { flag = green > 100 && green > red + blue; } else if (this.colorScope === "blue") { flag = blue > 100 && blue > red + green; } if (flag) { const [r, g, b, a] = this.backgroundImageData.slice(pos, pos + 4); // 目标区域填充为背景色 this.context.fillStyle = rgbaConvertToHex(r, g, b, a); this.context.fillRect(j, i, this.gap, this.gap); // 图像边缘钝化 this.context.beginPath(); this.context.arc(j, i, this.gap, 0, 2 * Math.PI); this.context.closePath(); this.context.fill(); } } } -
利用window.requestAnimationFrame来执行动画
this.playing && window.requestAnimationFrame(this.drawVideo.bind(this));
不足
1.由于需要扫描整个图像的数据,一般都是几万个数据的数组,所以性能还有待提升。
2.对于红绿蓝颜色区域的判断不是非常准确,因为实际拍摄的画面有很多影响因素,导致并不能达到标准rgba值。
3.对视频内容有严格要求(当然这是这种设计方案必然会产生的问题)
- 必须要保持背景不动
- 在视频前面几帧,只能展示背景,不能有其他干扰项
完整代码
⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
你的选择是()
a. 太酷了,点个star!
b. 正是我想要的,给你个star!
c. 下次一定!
d. 下次丕定
e. 钝角