圖片上傳組件開發
我就要自行車 - 需求整理
放眼WWW,一般的圖片上傳模塊,主要就是實現了三個功能,圖片的預覽,圖片的剪裁及預覽,圖片的上傳,那我也就整這麽一個吧,再細化一下需求。
圖片的預覽
用戶使用:用戶點擊“選擇圖片”,彈出文件瀏覽器,可以選擇本地的圖片,點擊確認後,所選圖片會按照原始比例出現在頁面的瀏覽區域中。
組件調用:開發者可以自己定義圖片預覽區域的大小,並限定所傳圖片的文件大小和尺寸大小。
圖片的剪裁
用戶使用:用戶根據提示,在預覽區域的圖片上拖動鼠標框出想要上傳的圖片區域,並且能在結果預覽區域看到自己的剪裁結果。
組件調用:開發者可以自定義是否剪裁圖片,並可以定義是否限定剪裁圖片的大小及比例,並且設定具體大小及比例。
圖片的上傳
用戶使用:用戶點擊“圖片上傳”,圖片開始上傳,現實“上傳中…”,完成後顯示“上傳完成”。
組件調用:開發者得到base64格式的urlData圖片,自己編寫調用Ajax的函數及其回調函數。
扔出原型圖
作為設計師,扔圖是我的最愛,畫了一套全功能,包含剪裁及剪裁瀏覽的原型圖
state-1:初始狀態:
state-2:點擊"選擇圖片",瀏覽本地後載入圖片:
state-3:剪裁,在圖片區域上拖動鼠標選擇要剪裁的部分,確認要上傳的部分:
一次歷史性的對話 - 本地圖片讀取
自打幹上web開發這活,就都是在搗鼓瀏覽器內部這點事,從沒想過跟瀏覽器之外計算機本地的一些文件能發生什麽關系。但是該來的總要來,既然要上傳圖片,就肯定要從計算機本地來選擇文件並在瀏覽器內打開,這歷史性的對話就要這麽開啟了…
圖片的選擇
其實在HTML中的 <input>
標簽就提供了瀏覽本地文件的功能,前提是 type="file"
,真是很講道理… 試過就知道一點擊就會打開文件瀏覽器。
-
<input id="inputArea" type="file" />
但這麽做有兩個經典的問題:
-
第一,會有一個輸入框傻乎乎的在那裏…
-
第二,我用的是Ajax,怎麽才能get到表單當中的文件呢?
對於問題一,很好解決直接各種方式hide這個input標簽即可,再主動觸發 click()
:
-
var imgFrom = document.getElementById("inputArea");
-
function loadImg(){
-
imgFrom.click();
-
}
對於問題二,這就要介紹一下 FormData
對象了。
XMLHttpRequest Level 2添加了一個新的接口FormData.利用FormData對象,我們可以通過JavaScript用一些鍵值對來模擬一系列表單控件,我們還可以使用XMLHttpRequest的send()方法來異步的提交這個"表單".比起普通的ajax,使用FormData的最大優點就是我們可以異步上傳一個二進制文件.
摘自MDN Web docs - Web技術文檔/Web API 接口/FormData
正如上面的文檔所說 FormData
對象可以幹的事無非就是用javascript模擬表單控件,也正因為如此所以可以在模擬的表單中放入一個文件:
-
var myFrom = new FormData();
-
var imageData = imgFrom.files[0];//獲取表單中第一個文件
-
myFrom.append("image",imageDate);//向表單中添加一個鍵值對
-
console.log(myFrom.getAll("image"));//獲取表單中image字段對應的值,結果見下圖
正如我們所見,文件我們已經通過Web拿到手了。
圖片的展現
既然是要上傳圖片,我們肯定得知道自己傳的是啥圖片啊,所以下一步就是如何把讀取的圖片展現在頁面上了,正如上圖中的顯示,我的得到的圖片是一個 File
對象,而 File
對象是特殊的 Blob
對象,那 Blob
對象又是個啥呢…
Blob 對象表示不可變的類似文件對象的原始數據。Blob表示不一定是JavaScript原生形式的數據。File 接口基於Blob,繼承了 blob的功能並將其擴展使其支持用戶系統上的文件。
摘自MDN Web docs - Web技術文檔/Web API 接口/Blob
說實話,真是懵逼。但仔細理解下大概意思就是 Blob
對象是用來表示/承載文件對象的原始數據(二進制)的,借助一些博文會有助於理解:
-
js中關於Blob對象的介紹與使用 - 可樂Script
-
HTML5 Blob對象 - zdy0_2004
說到底,重點不在這,了解一下有個概念即可,重點在於我們怎麽展示這個 File
對象
這就要請出 FileReader
對象了。
FileReader 對象允許Web應用程序異步讀取存儲在用戶計算機上的文件(或原始數據緩沖區)的內容,使用 File 或 Blob 對象指定要讀取的文件或數據。
摘自MDN Web docs - Web技術文檔/Web API 接口/FileReader
不難看出, FileReader
對象就是用來讀取本地文件的,而這其方法 readAsDataURL()
就是我們要用的東西啦。
該方法會讀取指定的 Blob 或 File 對象。讀取操作完成的時候,readyState 會變成已完成(DONE),並觸發 loadend 事件,同時 result 屬性將包含一個data:URL格式的字符串(base64編碼)以表示所讀取文件的內容。
摘自MDN Web docs - Web技術文檔/Web API 接口/FileReader/FileReader.readAsDataURL()
這裏面又提到一個新名詞data:URL,也就是說 readAsDataURL()
的作用就是能把文件轉換為data:URL,不過這個data:URL又是什麽呢,執行來看看:
-
var reader = new FileReader(); //調用FileReader對象
-
reader.readAsDataURL(imgData); //通過DataURL的方式返回圖像
-
reader.onload = function(e) {
-
console.log(e.target.result);//看看你是個啥
-
}
控制臺的結果全臉懵逼。
可以通過這篇文章去大概了解一下DATA URL簡介及DATA URL的利弊 - 薛陳磊
說到底這dataURL我就粗略的理解它為URL形式的data,也就是說這段URL並不是與普通的URL一樣指向某個地址,而是它本身就是數據,我們試著把這一堆字符粘到一個 <img>
的 src
屬性中。
終於看到了,結果正如所料,將這段包含了數據的URL賦給一個 <img>
確實可以讓數據被展現為圖片。
至此,我們實現了本地文件的讀取及展現
指哪兒截哪兒 - 利用canvas的圖片截取
溫馨提示-亂入:看明白這裏需要對canvas有基本的了解:MDN Web docs - Web技術文檔/Web API接口/Canvas/Canvas教程
在Web上對圖像進行操作,沒有比canvas相關技術更合適的了,所以本文用canvas技術來實現對圖片的截取。
canvas中的圖片展現
在上文中,我們利用 <img>
展現出了我們選擇的圖片,但是我們的圖片截取功能可是要利用 <canvas>
來實現的,所以怎麽在 <canvas>
中展現我們剛才獲取的圖片就是下一步要幹的事情了。
canvas的API中自帶 drawImage()
函數,其作用就是在 <canvas>
中渲染一張圖片出來,其可以支持多種圖片來源見:MDN Web docs - Web技術文檔/Web API接口/CanvasRenderingContext2D/CanvasRenderingContext2D.drawImage()
最簡單的,我們直接把剛剛顯示圖片的那個 <img>
傳入是不是就可以呢?
-
var theCanvas = document.getElementById("imgCanvas");
-
var canvasImg = theCanvas.getContext("2d");//獲取2D渲染背景
-
var img = document.getElementById("image");
-
img.onload = function(){//確認圖片已載入
-
canvasImg.drawImage(img,0,0);
-
}
結果如下:
從圖中看,左側是之前的‘‘,右側是渲染了圖片信息的 <canvas>
。這麽看來雖然成功?在 <canvas>
中渲染出了圖片但是有兩個明顯的問題:
-
左邊的‘‘留著幹啥?
-
右邊看上去是不是有點不一樣?
這倆問題其實都好辦,針對第一個問題,我們其實可以根本不用實體的‘‘直接利用‘Image‘對象即可,第二個問題明顯是因為 <canvas>
的大小與獲取到的圖片大小不一致所產生的,綜合這兩點,對代碼進行進化!
-
var theCanvas = document.getElementById("imgCanvas");
-
var canvasImg = theCanvas.getContext("2d");
-
var img = new Image();//創建img對象
-
-
reader.onload = function(e) {
-
img.src = e.target.result;
-
}
-
-
img.onload = function(){
-
theCanvas.Width = img.width;//將img對象的長款賦給canvas標簽
-
theCanvas.height = img.height;
-
canvasImg.drawImage(img,0,0);
-
}
結果與我們所期待的一樣,至此我們成功的在 <canvas>
中展現了從本地獲取的圖片。
canvas中圖片的截取
其實截圖,說白了就是在一個圖像上,獲取某個區域中的圖像信息。
canvas作為專門用來處理圖像及像素相關的一套API,獲取區域中的相關圖像信息可以說是再簡單不過的事情,利用 getImageData()
函數即可 //詳情,當然我們不光要把圖像信息獲取到,最好還能展現出來我們的截圖結果,這裏就要用到與之相對的 putImageData()
函數 //詳情。
-
var resultCanvas = document.getElementById("resultCanvas");
-
var resultImg = resultCanvas.getContext("2d");
-
var cutData = canvasImg.getImageData(100,100,200,200);
-
resultImg.putImageData(cutData,0,0);
結果如圖:
我也要畫一個圈/框
既然這個工具是面向用戶的,截圖的過程肯定是要所見即所得的,在函數 getImageData()
中有4個參數,分別是截圖起點的兩個坐標和區域的寬度及高度,所以問題就變成了如何更合理的讓用戶輸入這4個值。
其實現存的主流解決方案就做的非常好了:在圖上拖動鼠標,拉出一個框,這個框內就是用戶希望截取的區域。
在畫布上畫出一個框很簡單,只需用到 strokeRect()
函數 //詳情。但是讓用戶自己拖出一個框就比較復雜了,先分析一下用戶的一套動作都有什麽:
-
用戶選定起始點,點下鼠標左鍵。
-
用戶選定截圖區域的大小,保持鼠標左鍵不擡起,同時移動鼠標選擇。
-
用戶完成選擇,擡起鼠標左鍵。
回過頭再來看程序需要幹什麽:
-
獲取起始點的坐標,並記錄為已點擊狀態。
-
判斷一下如果為已點擊狀態那麽,獲取每一次移動/幀的鼠標坐標,並計算出與起始點之間的橫縱坐標距離,而這距離就是所畫框的長度和寬度,清除上一幀的整個畫面,再繪制一個新的圖片再畫一個新的框,同時按照框的起始坐標及寬高,截取圖像信息,再清除預覽區域的上一幀的畫布,再將這一幀的圖像信息載入。
-
鼠標擡起後,停止記錄及繪制,保持最終一幀的框停留在畫面上。
在這裏,要說明一下,為什麽非要清除整個畫面不可,其實可以把通過 canvas.getContext("2d")
獲取到的2D 畫布的渲染上下文 //詳情 就當作一塊畫布,已經渲染出來的東西就已經留在了上面,無法再修改,如果想要更改畫面上已經存在的元素的大小位置形狀等等屬性,那麽在程序層面,就只能(個人理解,不一定對,如果有問題請一定跟我嘮嘮)把之前的畫布清空再重新渲染。
這個思路與我們之前端開發中動畫相關的開發思路不同,並不是像之前那樣直接操作現有元素屬性就可以改變該元素在畫面上的呈現結果的,而在這裏其實更像是在現實生活中的動畫制作原理就是:每一幀都需要重新繪制整張畫面。而其實這是任何動畫渲染方式的最底層思路與行為。話說回來按照上文相關的開發思路,實現這個功能的代碼如下:
-
var flag = false;//記錄是否為點擊狀態的標記
-
var W = img.width;
-
var H = img.height;
-
var startX = 0;
-
var startY = 0;
-
-
//當鼠標被按下
-
theCanvas.addEventListener("mousedown", e => {
-
flag = true;//改變標記狀態,置為點擊狀態
-
startX = e.clientX;//獲得起始點橫坐標
-
startY = e.clientY;//獲得起始點縱坐標
-
})
-
-
//當鼠標在移動
-
theCanvas.addEventListener("mousemove", e => {
-
if(flag){//判斷鼠標是否被拖動
-
canvasImg.clearRect(0,0,W,H);//清空整個畫面
-
canvasImg.drawImage(img,0,0);//重新繪制圖片
-
canvasImg.strokeRect(startX, startY, e.clientX - startX, e.clientY - startY);//繪制黑框
-
resultImg.clearRect(0,0,cutData.width,cutData.height);//清空預覽區域
-
cutData = canvasImg.getImageData(startX, startY, e.clientX - startX, e.clientY - startY);//截取黑框區域圖片信息
-
resultImg.putImageData(cutData,0,0);//將圖片信息賦給預覽區域
-
}
-
})
-
-
//當鼠標左鍵擡起
-
theCanvas.addEventListener("mouseup", e => {
-
flag = false;//將標誌置為已擡起狀態
-
})
結果如圖:
能不能弄的高大上一點啊
主要吧,這個黑框太醜了,透露著一種原始和狂野,以及來自工科男審美的粗糙感…能不能弄的好看點,起碼讓它看上去是一個工具不是一個實驗。
我的想法是這樣的,待被截取的圖片上應該蒙上一層半透明白色遮罩,用戶框選出的部分是沒有遮罩的,這樣效果可以為功能增加視覺上的材質感及舒適感,同時顯得高端。
具體效果是這樣的-下圖來自ps:
是不是稍微好些了?可是,怎麽實現?
簡單來說,就是在原有的畫布上再蒙半透明的一層畫布,然後讓這一層有一部分是沒有的就可以實現了,總的來說就是蒙版和遮罩的思路,在canvas中也有相關的api,但是我楞是沒看明白。負責任的貼一個鏈接
不過開發就是這樣,條條大路出bug。我想到這個功能的瞬間腦子像抽了一樣,出現了這麽一種實現方法。見下圖:
mask層可以分為A,B,C,D四個矩形區域,在圖中兩個藍色的點是已知的(用戶自己畫出來的),在下層圖片大小已知的前提下,這四個矩形區域的四個點都是可以計算出來的,從而其高度和寬度也可以計算出來,這樣就可以利用這些數據畫出一個半透明的矩形,將四個半透明矩形都畫出來後,就能夠實現之前設計出的效果了,具體代碼如下:
-
theCanvas.addEventListener("mousemove", e => {
-
if(flag){
-
canvasImg.clearRect(0,0,W,H);
-
resultImg.clearRect(0,0,cutData.width,cutData.height);
-
canvasImg.drawImage(img,0,0);
-
canvasImg.fillStyle = ‘rgba(255,255,255,0.6)‘;//設定為半透明的白色
-
canvasImg.fillRect(0, 0, e.clientX, startY);//矩形A
-
canvasImg.fillRect(e.clientX, 0, W, e.clientY);//矩形B
-
canvasImg.fillRect(startX, e.clientY, W-startX, H-e.clientY);//矩形C
-
canvasImg.fillRect(0, startY, startX, H-startY);//矩形D
-
cutData = canvasImg.getImageData(startX, startY, e.clientX - startX, e.clientY - startY);
-
resultImg.putImageData(cutData,0,0);
-
}
-
})
效果如圖:
沒有什麽把自己的腦殘想法實現更爽的了
至此,截圖的基本功能都實現了,但還差最後一步。
另一次歷史性的對話 - 圖片上傳
圖片已經截出來了,下一步就是怎麽上傳了,通過Ajax上傳,需要將圖像數據轉化為 File
,而在canvas的API中自帶 toBlob()
函數。 //詳情
-
var resultFile = {}
-
theCanvas.addEventListener("mouseup", e => {
-
resultCanvas.toBlob(blob => {
-
resultFile = blob;
-
console.log(blob);//Blob(1797) {size: 1797, type: "image/png"}
-
}
-
})
-
flag = false;
-
})
圖片上傳組件開發