詳解使用JS如何制作簡單的ASCII圖與單極圖
ASCII圖
在終端執(zhí)行各種命令的時候經(jīng)常會看到一些終端里顯示出來的"圖片",遠(yuǎn)看仿佛一張圖,近看則是一個個的 ASCII碼,它們 大致長這樣子
而今天我們要做的則是用JS把一張給定的圖片轉(zhuǎn)換成這種用ASCII字符組成的“ASCII圖” 先看看最終效果,假設(shè)我們給定的圖片是這樣子的,
這是代碼處理后的結(jié)果,用了 I'mYasic
這8個字符來表示,還是可以分辨出大致的輪廓的。
單級圖
而另一種圖則是單極圖,也就是黑白圖片,還是剛剛那張圖片,輸出如下
基礎(chǔ)知識
這兩種圖都是比較簡單的,只需要以下知識即可
- HTML5中的Canvas
- 像素的RGB值
- JS中的Canvas相關(guān)API
制作ASCII圖
一般來說,在計(jì)算機(jī)當(dāng)中,我們看到的大多數(shù)圖片都是由一個個像素點(diǎn)構(gòu)成的,每一個像素點(diǎn)則由 RGBA 構(gòu)成,在 css 中我們時常用的 rgba(255, 255, 255, 255, 0)
就是一組RGBA值, 也即是RGB三原色和Alpha透明度。當(dāng)然一張圖片不是僅僅包含所有像素點(diǎn)數(shù)據(jù)的,還包括一些描述信息,也稱為圖片的 profile,這一部分小則幾KB,多則幾百KB,是圖片壓縮中經(jīng)常被處理的部分。
那么對于圖片中每一個像素點(diǎn)來說,只要我們改變了其相應(yīng)的RGBA的值,最終的圖片也就變了樣子。而修改哪些像素點(diǎn)、修改成什么樣的RGBA,則決定著最終的圖片風(fēng)格,這也是許多濾鏡采用的機(jī)制。
基于以上的理論知識,我們的ASCII圖制作思路也就有了。ASCII圖其實(shí)就是將一張圖中的一個像素點(diǎn),通過計(jì)算其RGBA的值,劃分成給定的幾個量化值,在這里由于我們用的 I'mYasic
這8個字符來表示,所以要分成8組值,每一組用一個ASCII字符來表示,最終就能組成一幅完整的ASCII圖片。
接下來就是具體的代碼實(shí)現(xiàn)。
獲取圖片的像素信息
通過 Canvas API 中的 getImageData()
方法我們可以獲得一個對象,這個對象的屬性里包含一個一維數(shù)組 data,這個一維數(shù)組每4個元素為一組,代表了一個 canvas 中指定范圍的全部像素信息,并且依次是 RED,GREEN,BLUE,ALPHA。因此我們可以先把圖片放進(jìn) canvas 中,再調(diào)用這個方法拿到像素。
不過我很疑惑為什么 data 是一個一維數(shù)組,通常處理的圖片都是二維圖片,如果用二維數(shù)組來表示像素信息,代碼讀取和處理會方便很多,也更容易理解。甚至可以用一個三維數(shù)組,專門用一個維度來放置RGBA信息。
獲取圖片像素信息的代碼如下所示
var canvasContext = canvas.getContext("2d"); canvasContext.drawImage(sourceImg, 0, 0); var imgData = canvasContext.getImageData(0 , 0, sourceImg.width, sourceImg.height); var imgDataArray = imgData.data;
那么對于某一個像素點(diǎn)的RGBA值就可以這樣獲取
var r = imgDataArray[lineIndex]; var g = imgDataArray[lineIndex + 1]; var b = imgDataArray[lineIndex + 2]; var a = imgDataArray[lineIndex + 3];
其中 lineIndex 是遍歷每一個像素點(diǎn)的基準(zhǔn)變量。
圖片灰度化
灰度化,也就是獲取像素點(diǎn)的灰度值。由于每一個像素點(diǎn)包含著RGBA四種信息,而我們需要將所有像素點(diǎn)的RGBA值分成8組,因此需要統(tǒng)一一下RGBA的值,最終得到一個值Y,而相應(yīng)的像素點(diǎn)的RGBA值滿足 Y = R = G = B ,在這里我們不考慮透明度 Alpha。由于RGB的值相等像素點(diǎn)顏色是介于白色與黑色之間的灰色,所以這一過程也稱為灰度化。
灰度化算法有很多種,我們在這里采取最簡單的方式,即
Y = (R + G + B) * 1/3
相應(yīng)代碼如下
function rgb2gray(r, g, b) { return r * 0.333 + g * 0.333 + b * 0.333; }
灰度圖量化
灰度化以后的圖片大致長這樣子,可以看到色彩已經(jīng)都變成灰色了。
那么接下來就是關(guān)鍵的“量化”過程。也就是說,我們要讓把這些不同灰度的值分成8組,并且每一組都賦予一個ASCII字符作為標(biāo)示,當(dāng)然選取的ASCII字符也要有一定規(guī)律,簡單來說就是顏色由深到淺相應(yīng)的字符由繁到簡。而量化過程就是將0-255范圍等分成8個區(qū)間,依次判斷灰度值在哪一個區(qū)間內(nèi),代碼如下。
由于圖像像素?cái)?shù)目巨大,為了效率,判決時可以采取“二分判決”法提高判決速度。
function gray2asc(gray) { /*ASCII--I'mYasic*/ /*32 64 96 128 160 192 224 256*/ gray = 255 - gray; if (gray < 128){ if (gray < 64){ if (gray < 32){ return '\'' } else { return 'c' } } else { if (gray < 96){ return 'i' } else { return 's' } } } else { if (gray < 192){ if (gray < 160){ return 'I' } else { return 'm' } } else { if (gray < 224){ return 'a' } else { return 'Y' } } } }
遍歷與顯示
上面大概講解完了對于一個像素點(diǎn)變換為ASCII碼的過程,接下來就是遍歷和顯示了。
遍歷
遍歷全部像素點(diǎn)并變換為ASCII碼基本是不可能的,因?yàn)閳D片稍微大一些計(jì)算量就增長很多,所以我們折中一下,對于像素陣列的行與列都進(jìn)行等間隔采樣,最終展示出來的圖片分辨率會隨著采樣間隔減小而增強(qiáng)。另外要注意 data 數(shù)組是一維數(shù)組,并且每4個元素為一組RGBA數(shù)據(jù)。相應(yīng)代碼如下
var result = ""; var lineIndex = 0; for (var lineHeight = 0; lineHeight < sourceImg.height; lineHeight += 12){ var lineASC = ""; for (var lineFlag = 0; lineFlag < sourceImg.width; lineFlag += 5){ lineIndex = (lineHeight * sourceImg.width + lineFlag) * 4; var r = imgDataArray[lineIndex]; var g = imgDataArray[lineIndex + 1]; var b = imgDataArray[lineIndex + 2]; lineASC += gray2asc(rgb2gray(r, g, b)); } lineASC += '\n'; result += lineASC; }
顯示
最終獲得的 result
字符串就是需要展示的ASCII碼。但是必須注意,如果直接展示到頁面上會因?yàn)槊恳粋€字符的字符寬度不一樣而導(dǎo)致ASCII圖“失真”,這里我們可以采用 Monospace 字體來確保字符寬度一致。
制作單極圖
其實(shí)看完上面部分,就應(yīng)該知道單極圖非常好實(shí)現(xiàn),同樣需要獲取像素信息并灰度化,只是量化時直接量化為 rgb(0, 0, 0) 和 rgb(255, 255, 255) 兩種顏色就可以。
var canvasContext = targetCanvas.getContext("2d"); canvasContext.drawImage(sourceImg, 0, 0); var imgData = canvasContext.getImageData(0 , 0, sourceImg.width, sourceImg.height); var imgDataArray = imgData.data; for (var index = 0; index <= sourceImg.width * sourceImg.height * 4; index += 4){ var red = imgDataArray[index]; var green = imgDataArray[index + 1]; var blue = imgDataArray[index + 2]; var gray = rgb2gray(red, green, blue); if (gray < 128){ imgData.data[index] = 0; imgData.data[index + 1] = 0; imgData.data[index + 2] = 0; } else { imgData.data[index] = 255; imgData.data[index + 1] = 255; imgData.data[index + 2] = 255; } } canvasContext.putImageData(imgData, 0, 0);
別忘了最后要用 putImageData
方法將修改后的像素信息放回 canvas 中進(jìn)行顯示。
ASCII圖完整代碼
function rgb2gray(r, g, b) { return r * 0.333 + g * 0.333 + b * 0.333; } function gray2asc(gray) { /*ASCII--I'mYasic*/ /*32 64 96 128 160 192 224 256*/ gray = 255 - gray; if (gray < 128){ if (gray < 64){ if (gray < 32){ return '\'' } else { return 'c' } } else { if (gray < 96){ return 'i' } else { return 's' } } } else { if (gray < 192){ if (gray < 160){ return 'I' } else { return 'm' } } else { if (gray < 224){ return 'a' } else { return 'Y' } } } } var img2ASC = function (canvas, sourceImg) { console.log(sourceImg.width + " " + sourceImg.height); var canvasContext = canvas.getContext("2d"); canvasContext.drawImage(sourceImg, 0, 0); var imgData = canvasContext.getImageData(0 , 0, sourceImg.width, sourceImg.height); var imgDataArray = imgData.data; var result = ""; var lineIndex = 0; for (var lineHeight = 0; lineHeight < sourceImg.height; lineHeight += 12){ var lineASC = ""; for (var lineFlag = 0; lineFlag < sourceImg.width; lineFlag += 5){ lineIndex = (lineHeight * sourceImg.width + lineFlag) * 4; var r = imgDataArray[lineIndex]; var g = imgDataArray[lineIndex + 1]; var b = imgDataArray[lineIndex + 2]; lineASC += gray2asc(rgb2gray(r, g, b)); } lineASC += '\n'; result += lineASC; } document.getElementById("result").innerHTML = result; };
單極圖完整代碼
function rgb2gray(r, g, b) { return r * 0.333 + g * 0.333 + b * 0.333; } function gray2asc(gray) { /*ASCII--I'mYasic*/ /*32 64 96 128 160 192 224 256*/ if (gray < 128){ if (gray < 64){ if (gray < 32){ return '\'' } else { return 'c' } } else { if (gray < 96){ return 'i' } else { return 's' } } } else { if (gray < 192){ if (gray < 160){ return 'I' } else { return 'm' } } else { if (gray < 224){ return 'a' } else { return 'Y' } } } } var monoImg = function (targetCanvas, sourceImg) { targetCanvas.width = sourceImg.width; targetCanvas.height = sourceImg.height; var canvasContext = targetCanvas.getContext("2d"); canvasContext.drawImage(sourceImg, 0, 0); var imgData = canvasContext.getImageData(0 , 0, sourceImg.width, sourceImg.height); var imgDataArray = imgData.data; for (var index = 0; index <= sourceImg.width * sourceImg.height * 4; index += 4){ var red = imgDataArray[index]; var green = imgDataArray[index + 1]; var blue = imgDataArray[index + 2]; var gray = rgb2gray(red, green, blue); if (gray < 128){ imgData.data[index] = 0; imgData.data[index + 1] = 0; imgData.data[index + 2] = 0; } else { imgData.data[index] = 255; imgData.data[index + 1] = 255; imgData.data[index + 2] = 255; } } canvasContext.putImageData(imgData, 0, 0); };
總結(jié)
這一篇博客主要講了利用JS中的 Canvas API 進(jìn)行一些簡單的像素化操作,但其實(shí)還有很多地方可以繼續(xù)改進(jìn)。比如一般單極圖出來后很多地方會有噪點(diǎn),也就是一些礙眼的白點(diǎn)和黑點(diǎn),可以通過一些方式“去掉噪點(diǎn)”,就留在以后寫吧!以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
JavaScript實(shí)現(xiàn)頁面無操作倒計(jì)時退出
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)頁面無操作倒計(jì)時退出,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10JS操作input標(biāo)簽屬性checkbox全選的實(shí)現(xiàn)代碼
這篇文章主要介紹了JS操作input標(biāo)簽屬性checkbox全選的代碼,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-03-03如何使用ES6的class類繼承來實(shí)現(xiàn)絢麗小球效果
JS是由ES(ECMAScript)、DOM(瀏覽器文檔對象)、BOM(瀏覽器對象模型)組成,這篇文章主要給大家介紹了關(guān)于如何使用ES6的class類繼承來實(shí)現(xiàn)絢麗小球效果的相關(guān)資料,需要的朋友可以參考下2021-06-06使用微信小程序開發(fā)彈出框應(yīng)用實(shí)例詳解
本文通過實(shí)例代碼給大家介紹了使用微信小程序開發(fā)彈出框功能,代碼簡單易懂,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2018-10-10bootstrap實(shí)現(xiàn)動態(tài)進(jìn)度條效果
本篇文章主要介紹了bootstrap實(shí)現(xiàn)動態(tài)進(jìn)度條效果,進(jìn)度條可以加強(qiáng)應(yīng)用的用戶體驗(yàn)效果,看到數(shù)字,具有一定的參考價值,有興趣的可以了解一下。2017-03-03