canvas之"隐形斗篷"大法

971 阅读3分钟

背景

因为最近看到很多canvas的骚操作,有画龙的、有做动画的,在看他们代码的时候,这些图形处理的方式让我突然想到在大二用python做的一个隐形斗篷的骚操作,于是二话不说就打开github创建了项目,一顿骚操作下,效果终于出来了!!!

动画演示

canvas.gif

实现过程

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方法

  1. 将video当前画面绘画到canvas上

    this.context.drawImage(
       this.videoEle,
       0,
       0,
       this.canvasEle.width,
       this.canvasEle.height
     );
    
  2. 获取canvas图像数据

     const imageData = this.context.getImageData(
       0,
       0,
       this.canvasEle.width,
       this.canvasEle.height
     ).data;
    
  3. 保存第一帧(背景图)的图像数据

    if (!this.backgroundImageData && imageData[3] !== 0) {
       // 保存第一帧(背景图)的像素数据
       this.backgroundImageData = imageData;
     }
    
    • imageData[ 3 ] !== 0 是在使用iphone safari时发现了一个bug,backgroundImageData的数据为[0, 0, 0, 0, .....],即一个黑色全透明的背景
    • 所以这里判断一下,当第一个像素点的透明度不为0时才认为是原视频的背景(关于imageData数据的解释和运用在后面)
  4. 扫描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();
         }
       }
     }
    
  5. 利用window.requestAnimationFrame来执行动画

     this.playing && window.requestAnimationFrame(this.drawVideo.bind(this));
    

不足

1.由于需要扫描整个图像的数据,一般都是几万个数据的数组,所以性能还有待提升。

2.对于红绿蓝颜色区域的判断不是非常准确,因为实际拍摄的画面有很多影响因素,导致并不能达到标准rgba值。

3.对视频内容有严格要求(当然这是这种设计方案必然会产生的问题)

  1. 必须要保持背景不动
  2. 在视频前面几帧,只能展示背景,不能有其他干扰项

完整代码

⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

github.com/Himangguo/c…

你的选择是()

a. 太酷了,点个star!

b. 正是我想要的,给你个star!

c. 下次一定!

d. 下次丕定

e. 钝角

参考资料

juejin.cn/post/696347…

developer.mozilla.org/zh-CN/docs/…