1. 程式人生 > 實用技巧 >使用JS生成字元視訊/畫

使用JS生成字元視訊/畫

前言

  參考文章:https://blog.csdn.net/weixin_34224941/article/details/88039482

原理

  字元畫是計算每個畫素上的灰度值並與預定好的字元列表進行替換。

    1:灰度值能夠明確知道每個畫素的明暗度;

    2:計算出圖片上每個畫素上的灰度值(值範圍在 0 ~ 255);

    3:有一個字元列表,灰度值雖然最大為 255,但並字元列表的元素個數不一定要256個;

效果

  

整理

  既然使用JS實現,那麼就需要考慮JS的限制,比如,JS不能訪問本地檔案等等。

  從本地中選擇一個視訊檔案使用JS實現計算視訊檔案的每一幀畫面的每一畫素的灰度值與定義好的字元列表進行替換,到輸出到頁面上。可以具體拆分為:

  1:輸入,JS因為安全限制,所以不能直接讀取本地檔案,所以需要一個 type 為 file 的 input,來選擇讀取一個視訊檔案;

  2:處理視訊,讀取到視訊檔案後如何能夠處理視訊檔案,而處理視訊檔案就需要得到視訊的每一幀畫面。

    ①:首先得到視訊的每一幀畫面,使用 canvas 的 drawImage 方法( drawImage 方法的第一個引數可以是video或者img,而 video 是輸出當前播放的畫面,可以利用這個);

    ②:得到了視訊的每一幀畫面後,使用 canvas 的getImageData 方法,來得到畫面的畫素資訊。

  3:輸出,將得到的畫素資訊進行計算得到灰度值在字元列表的對映字元,並輸出到canvas,使用 canvas 的 fillText 方法。

  

  以上需要 input、canvas、video 標籤。

  在drawImage 時有個致命問題,這個雖然能得到當前播放的畫面,但不能實時的得到視訊的每一幀。這裡我不斷的去 drawImage 就可以解決。程式碼就為:

1 setInterval(function(){
2   ctx.drawImage(videoDom, 0, 0, width, height);
3 }, 10)     

程式碼實現

  先看看 HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>字元畫</title>
</head>
<body>
    <input id="file" type="file" />
    <canvas id="show" ></canvas>
    <video controls id="video"></video>
</body> </html>

  input 作用為選擇一個視訊;

  video 作用為播放視訊;

  canvas 作用為處理視訊並輸出字元視訊。

  JS程式碼實現

 1 <script>
 2 
 3 
 4     const fileDom = document.getElementById('file');
 5     const videoDom = document.getElementById('video');
 6     const canvasDom = document.getElementById('show');
 7     const ctx = canvasDom.getContext("2d");
 8 
 9     let t;
10     let asciiList = ['#', '&', '@', '%', '$', 'w', '*', '+', 'o', '?', '!', ';', '^', ',', '.', ' ']  // 預定義好的字元列表
11 
12 
13     // 監聽 input 的change事件,將得到的 視訊檔案能夠在 video 中播放
14     fileDom.addEventListener('change', function(e){
15 
16         let file = e.target.files[0]  // 獲取選擇的視訊的檔案物件
17         videoDom.src = URL.createObjectURL(file)  // 使用 URL.createObjectURL() 建立檔案物件的路徑並設定給video
18     
19     })
20 
21     //監聽 canplay 事件,確保視訊能夠播放
22     videoDom.addEventListener('canplay', function() {
23 
24 
25         videoDom.addEventListener('play', function(){
26 
27             // 設定 canvas 的大小 跟 視訊大小一樣
28             canvasDom.width = videoDom.videoWidth
29             canvasDom.height = videoDom.videoHeight
30 
31 
32             t = setInterval(function(){
33 
34                 // drawImage 得到當前 video 播放的畫面, 大小為 canvas 的大小, 配合 setInterval,就能夠實時的獲取,沒有setInterval 就只能得到一幀的畫面
35                 ctx.drawImage(videoDom, 0, 0, canvasDom.width, canvasDom.height);
36 
37                 // getImageData 得到當前 canvas 上的畫面的畫素資訊, imgData的data 為 一維陣列,每 4 個為一個畫素點資訊
38                 let imgData = ctx.getImageData(0, 0, canvasDom.width, canvasDom.height);
39                 
40                 let width = imgData.width;
41                 let height = imgData.height;
42                 let data = imgData.data;
43 
44                 // 在繪製前清空畫布,避免重複繪製
45                 ctx.clearRect(0, 0, width, height);
46 
47                 // 迴圈 每個畫素點,區間為 canvas 大小
48                 for(let h = 0; h<height; h+=6){  // +=6 為 橫向密度,也可以 ++,但是會卡
49                     for(let w = 0; w<width; w+=6){  // +=6 為 豎向密度,也可以 ++,但是會卡
50                         let rgba = (width * h + w) * 4  // *4 為 每4個元素為一個畫素點, rgba 為 每個畫素點資訊的第一個位置
51 
52                         // 根據 rgb 值進行計算灰度值並得到字元
53                         let ascii = getAscii(data[rgba], data[rgba + 1], data[rgba + 2], data[rgba + 3])
54 
55                         // 將字元畫到 canvas 上
56                         ctx.fillText(ascii, w, h)
57                     }
58                 }
59              
60             }, 10)     
61         })
62     })
63 
64     videoDom.addEventListener('pause', function () {
65         clearInterval(t)
66     })
67 
68     function getAscii(r, g, b, a) {
69         let gary = .299 * r + .587 * g + .114 * b;  // 計算灰度值
70         // 字元列表並非為 256個,所以需要進行計算對映,避免陣列越界
71         let i = gary % asciiList.length === 0 ? parseInt(gary / asciiList.length) - 1 : parseInt(gary/ asciiList.length);
72         return asciiList[i];
73     }
74 
75  
76     
77 
78  
79 
80     
81 
82 </script>

程式碼封裝

  程式碼:Img2ASCII.js

    地址:https://gitee.com/liaoblog/Img2ASCII

    具體引數說明可看註釋

 1 class Img2ASCII {
 2 
 3     /**
 4      * 
 5      * @param {String} ctxStr canvas 標籤的ID,必填
 6      * @param {Object} options 引數選項, 不是必填
 7      * 
 8      * options :
 9      *  mode:int 模式,模式不同顯示效果不同,有 1, 2,
10      *  rate:int 顯示速率,值越大,幀數越少,
11      *  asciiList:Array 字元列表
12      *  x:int 輸出橫向密度,值越小越精確,也越卡
13      *  y:int 輸出豎向密度,值越小越精確,也越卡
14      *  isFillColor:boolean 是否輸出顏色
15      */
16     constructor(ctxStr, options = {}){
17         this.ctxDom = document.getElementById(ctxStr)
18         this.ctx = document.getElementById(ctxStr).getContext("2d")
19         this.mode = options.mode?options.mode:1 // 模式,模式不同顯示效果不同
20         this.rate = options.rate?options.rate:10 // 顯示速率
21         this.asciiList = options.asciiList?options.asciiList:['#', '&', '@', '%', '$', 'w', '*', '+', 'o', '?', '!', ';', '^', ',', '.', ' ']
22         this.x = options.x?options.x:6
23         this.y = options.y?options.y:6
24         this.isFillColor = options.isFillColor?true:false
25 
26     }
27 
28     start(video, width = 300, height = 150) {
29         const that = this
30         this.stop()
31         this.ctxDom.width = width
32         this.ctxDom.height = height
33         this.flag = setInterval(function(){
34             that.ctx.drawImage(video, 0, 0, width, height);
35             let data = that.ctx.getImageData(0, 0, width, height);
36             that.draw(data)   
37         }, this.rate)          
38         
39     }
40 
41     stop() {
42         clearInterval(this.flag)
43     }
44 
45     draw(imgData) {
46 
47         let width = imgData.width;
48         let height = imgData.height;
49         let data = imgData.data;
50         this.ctx.clearRect(0, 0, width, height);
51 
52         for(let h = 0; h<height; h+=this.y){
53             for(let w = 0; w<width; w+=this.x){
54                 let rgba = (width * h + w) * 4
55                 let ascii = this.getAscii(data[rgba], data[rgba + 1], data[rgba + 2], data[rgba + 3])
56                 if(this.isFillColor) this.ctx.fillStyle = `rgba(${data[rgba]},${data[rgba + 1]},${data[rgba + 2]},${data[rgba + 3]})`
57                 this.ctx.fillText(ascii, w, h)
58             }
59         }
60 
61     }
62 
63     getAscii(r, g, b, a) {
64         let gary = .299 * r + .587 * g + .114 * b;
65         if(this.mode == 2) gary+=500
66         let i = gary % this.asciiList.length === 0 ? parseInt(gary / this.asciiList.length) - 1 : parseInt(gary/ this.asciiList.length);
67         return this.asciiList[i];
68     }
69 
70 
71 }
Img2ASCII.js

  程式碼使用

    1:引入 Img2ASCII.js

1  <script src="./Img2ASCII.js"></script>

    2:例項化

1 let img2ASCII = new Img2ASCII('show');

    3:生成

1 img2ASCII.start(videoDom, videoDom.videoWidth, videoDom.videoHeight)

  

  完整使用程式碼

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <meta name="viewport" content="width=device-width, initial-scale=1.0">
 6     <title>字元畫</title>
 7 </head>
 8 <body>
 9     <input id="file" type="file" />
10     <canvas id="show" ></canvas>
11     <video width="300px" controls id="video"></video>
12 </body>
13 </html>
14 <script src="./Img2ASCII.js"></script>
15 <script>
16 
17 
18     const fileDom = document.getElementById('file');
19     const videoDom = document.getElementById('video');
20 
21     let img2ASCII = new Img2ASCII('show');
22 
23     fileDom.addEventListener('change', function(e){
24 
25         let file = e.target.files[0]  // 獲取選擇的視訊的檔案物件
26         videoDom.src = URL.createObjectURL(file)  // 使用 URL.createObjectURL() 建立檔案物件的路徑並設定給video
27     })
28 
29     videoDom.addEventListener('canplay', function() {
30 
31         videoDom.addEventListener('play', function(){
32 
33             img2ASCII.start(videoDom, videoDom.videoWidth, videoDom.videoHeight)
34         })
35     })
36 
37     videoDom.addEventListener('pause', function () {
38         img2ASCII.stop()
39     })
40     
41 
42 </script>
完整使用