js視訊轉字元畫 —— 寫一個屬於自己的字元轉換器

Jsonz發表於2018-07-30

部落格地址jsonz1993.github.io/2018/07/vid…

github地址 歡迎start follow ( *・ω・)✄╰ひ╯

先看一下效果,原視訊是這樣的。我們要實現的效果是這樣子的。之所以找這個視訊是因為...這個視訊和背景的對比度比較高做出來比較有辨識度,沒有其他的意思 ( *・ω・)✄╰ひ╯

起因

js視訊轉字元畫 —— 寫一個屬於自己的字元轉換器

某天一個基友在群裡問我,在抖音看到一種視訊,問我是不是能實現。我說可以的~ 於是當天晚上花了一個多小時折騰了一個粗糙版本...

先把視訊丟到部門技術群問有沒有關鍵字,給了一個keyword 圖片轉字串 於是照著這個思路去gayhub找資源拼樂高!

整體思路

  • 第一步我們通過 input[type="file"] 獲取檔案
  • 拿到檔案之後,用 URL.createObjectURL來獲取視訊的路徑
  • 通過 ctx.drawImage 我們可以把某個 video 當前的影像渲染到 canvas裡面
  • ctx.getImageData 可以獲取當前canvas 裡面圖片的色值,利用公式計算出灰度
  • 根據灰度的深淺去匹配要顯示的字元,深色匹配比較密集的字元,最淺的直接用空格代替,然後用 ctx.fillText重繪進去
  • 設定 video.currentTime 來獲得視訊的某一時刻影像,重複上述重繪過程

既然大概的思路已經理清,接下來就是具體的編碼,把想法寫出來的過程

具體編碼實現

獲取視訊檔案

首先我們先確定下html需要哪些元素

  1. 一個 input[type="file"] 用來給使用者上傳的
  2. 一個 video 用來承載上傳的video
  3. 一個 canvas 用來展示最終的結果

大概是長這樣:

<input type="file" id="inputFile" accept=".mp4" />
<canvas id="canvasShow"></canvas>
<video id="video"></video>
複製程式碼

接下來js檔案,我們要先對 input 繫結個監聽事件,拿到檔案url之後設定給video

這裡要注意兩點,一個是 url用完不用的話,用 URL.revokeObjectURL 釋放資源; 一個是我們這裡用了 await 在domVide.onCanplay之前不做任何操作,防止視訊沒有載入完就操作,有黑屏風險。

如果對 es6、es7、es8不熟悉的小夥伴要去補一下了~ 現在基本不會這些基本語法都看不懂demo= = 附上阮一峰老師的ES6教程,又想起面試被問ES7有什麼新特性 簡直是*了狗

domInput.addEventListener('change', async({target: {files }})=> {
  const file = files[0];
  const url = URL.createObjectURL(file);
  domVideo.src = urlrl;
  await new Promise(res=> domVideo.addEventListener('canplay', res));
  // next ====>  handleVideoInit()
});
複製程式碼

將視訊渲染到canvas

拿到視訊之後,我們要把當前這一個時刻的影像渲染到canvas裡面 先用ctx.drawImage(video, 0, 0, width, height) 把video dom當前屏渲染進canvas

再用ctx.getImageData(0, 0, width, height) 獲取圖片的色值來做處理

可以通過調整 img2Text 來選擇渲染出來的圖片是想要怎樣的(由哪些字元組成等等)

比如把 textList改為 ['Aa', 'Bv', 'Cc', 'Dd', '#', '&', '@', '$', '*', '?', ';', '^', '·', '·', '·', '·'],辨識度會高一點

/*
domVide => video元素
size => 存放video等元素的長寬
canvasVideo => 存放video當前的影像的canvas
canvasShow => 存放最後展示效果的canvas
*/
const size = {w: 0, h: 0};
const canvasVideo = document.createElement('canvas');
function handleVideoInit() {
  domVideo.currentTime = 0;
  size.w = domVideo.width = canvasVideo.width = canvasShow.width = domVideo.videoWidth * .5;
  size.h = domVideo.height = canvasVideo.height = canvasShow.height = domVideo.videoHeight * .5;
  video2Img();
}

function video2Img() {
  const { w, h } = size;
  ctxVideo.drawImage(domVideo, 0, 0, w, h);
  const { data } = ctxVideo.getImageData(0, 0, w, h);
  ctxShow.clearRect(0, 0, w, h);
  for (let _h= 0; _h< h; _h+= 8) {
    for (let _w= 0; _w< w; _w+= 8) {
      const index = (_w + w * _h) * 4;
      const r = data[index + 0];
      const g = data[index + 1];
      const b = data[index + 2];
      const gray = .299 * r + .587 * g + .114 * b;
      ctxShow.fillText(img2Text(gray), _w, _h + 8);
    }
  }
}

function img2Text(g) {
  const i = g % 16 === 0 ? parseInt(g / 16) - 1 : parseInt(g/ 16);
    return ['#', '&', '@', '%', '$', 'w', '*', '+', 'o', '?', '!', ';', '^', ',', '.', ' '][i];
}
複製程式碼

到這一步,其實已經實現了把一張圖片變為字元填充圖了,剩下的工作無非就是把視訊變成一張張的圖片,然後重複執行這些邏輯

持續呼叫渲染字元視訊

我們改一下 video2Img 函式,將其實現為能持續呼叫的形式, 再新增一個函式 clear 用來清理垃圾

這裡用到的是 window.requestAnimationFrame 去持續呼叫

function video2Img({
  timePoint= 0,
  curT= Date.now(),
  prevT= Date.now(),
  prevInterval,
}) {
  const { w, h } = size;
  ctxVideo.drawImage(domVideo, 0, 0, w, h);
  drawOnce();

  let _interval = Math.max((curT - prevT), 16) / 1000;
  if (curT - prevT !== 0) _interval -= prevInterval;
  await new Promise(res=> setTimeout(res, _interval*1000));
  const nextTimePoint = _interval + timePoint;
  if (nextTimePoint > domVideo.duration) return clear();

  tId = window.requestAnimationFrame(()=> video2Img({
    timePoint: nextTimePoint,
    prevT: curT,
    curT: Date.now(),
    prevInterval: _interval,
  }));  
}

function drawOnce() {
  const { data } = ctxVideo.getImageData(0, 0, w, h);
  ctxShow.clearRect(0, 0, w, h);
  for (let _h= 0; _h< h; _h+= 8) {
    for (let _w= 0; _w< w; _w+= 8) {
      const index = (_w + w * _h) * 4;
      const r = data[index + 0];
      const g = data[index + 1];
      const b = data[index + 2];
      const gray = .299 * r + .587 * g + .114 * b;
      ctxShow.fillText(img2Text(gray), _w, _h + 8);
    }
  }
}

function cleart() {
  const {w, h} = size;
  lastUrl && URL.revokeObjectURL(lastUrl);
  tId && window.cancelAnimationFrame(tId);
  ctxShow.clearRect(0, 0, w, h);
  ctxVideo.clearRect(0, 0, w, h);
}
複製程式碼

原始碼與demo

至此,功能基本都實現了,下面提供線上的呆毛和github倉庫地址~

pc端線上呆毛

github 原始碼

video轉圖片忘了是在github看哪個專案的,ctx.drawImage(video, 0, 0, width, height)這個是看完才知道的。

圖片轉字元基本是看這個大哥的github

在找方案的時候看到的一個畫素圖實現,挺有趣的,以前實現馬賽克是拿周圍畫素值取平均去做,這個哥們是直接放大截圖 更簡單粗暴傳送門

相關文章