溫馨提示:這裡除了一些幼稚的小元件啥也沒有
溫馨提示-續:這是一個新的系列,寫一些實際開發中遇到的一些常用的功能,想法笨拙,程式碼亂套
寫在前面
圖片上傳,作為web端一個常用的功能,在不同的專案中有不同的需求,在這裡實現一個比價基本的上傳圖片外掛,主要能實現圖片的瀏覽,剪裁,上傳這三個功能,同時也是為了讓自己對圖片/檔案上傳和HTML5中名聲在外的canvas
相關能夠有一些瞭解
我就要自行車 – 需求整理
放眼WWW,一般的圖片上傳模組,主要就是實現了三個功能,圖片的預覽,圖片的剪裁及預覽,圖片的上傳,那我也就整這麼一個吧,再細化一下需求
圖片的預覽
使用者使用:使用者點選“選擇圖片”,彈出檔案瀏覽器,可以選擇本地的圖片,點選確認後,所選圖片會按照原始比例出現在頁面的瀏覽區域中
元件呼叫:開發者可以自己定義圖片預覽區域的大小,並限定所傳圖片的檔案大小和尺寸大小
圖片的剪裁
使用者使用:使用者根據提示,在預覽區域的圖片上拖動滑鼠框出想要上傳的圖片區域,並且能在結果預覽區域看到自己的剪裁結果
元件呼叫:開發者可以自定義是否剪裁圖片,並可以定義是否限定剪裁圖片的大小及比例,並且設定具體大小及比例
圖片的上傳
使用者使用:使用者點選“圖片上傳”,圖片開始上傳,現實“上傳中…”,完成後顯示“上傳完成”
元件呼叫:開發者得到base64格式的urlData圖片,自己編寫呼叫Ajax的函式及其回撥函式
扔出原型圖
作為設計師,扔圖是我的最愛,畫了一套全功能,包含剪裁及剪裁瀏覽的原型圖
state-3:剪裁,在圖片區域上拖動滑鼠選擇要剪裁的部分,確認要上傳的部分
一次歷史性的對話 – 本地圖片讀取
自打幹上web開發這活,就都是在搗鼓瀏覽器內部這點事,從沒想過跟瀏覽器之外計算機本地的一些檔案能發生什麼關係。但是該來的總要來,既然要上傳圖片,就肯定要從計算機本地來選擇檔案並在瀏覽器內開啟,這歷史性的對話就要這麼開啟了…
圖片的選擇
其實在HTML中的<input>標籤就提供了瀏覽本地檔案的功能,前提是
type="file"
,真是很講道理… 試過就知道一點選就會開啟檔案瀏覽器
1 |
<input id="inputArea" type="file"/> |
但這麼做有兩個經典的問題:
第一,會有一個輸入框傻乎乎的在那裡…
第二,我用的是Ajax,怎麼才能get到表單當中的檔案呢
對於問題一,很好解決直接各種方式hide這個input標籤即可,再主動觸發click()
1 2 3 4 |
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模擬表單控制元件,也正因為如此所以可以在模擬的表單中放入一個檔案
1 2 3 4 |
var myFrom = new FormData(); var imageData = imgFrom.files[0];//獲取表單中第一個檔案 myFrom.append("image",imageDate);//向表單中新增一個鍵值對 console.log(myFrom.getAll("image"));//獲取表單中image欄位對應的值,結果見下圖 |
圖片的展現
既然是要上傳圖片,我們肯定得知道自己傳的是啥圖片啊,所以下一步就是如何把讀取的圖片展現在頁面上了,正如上圖中的顯示,我的得到的圖片是一個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又是什麼呢,執行來看看
1 2 3 4 5 |
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的API中自帶drawImage()
函式,其作用就是在中渲染一張圖片出來,其可以支援多種圖片來源見MDN Web docs – Web技術文件/Web API介面/CanvasRenderingContext2D/CanvasRenderingContext2D.drawImage()
最簡單的,我們直接把剛剛顯示圖片的那個<img>傳入是不是就可以呢
1 2 3 4 5 6 |
var theCanvas = document.getElementById("imgCanvas"); var canvasImg = theCanvas.getContext("2d");//獲取2D渲染背景 var img = document.getElementById("image"); img.onload = function(){//確認圖片已載入 canvasImg.drawImage(img,0,0); } |
結果如下
從圖中看,左側是之前的'<img>’,右側是渲染了圖片資訊的<canvas>
這麼看來雖然成功?在<canvas>中渲染出了圖片但是有兩個明顯的問題
1.左邊的'<img>’留著幹啥?
2.右邊看上去是不是有點不一樣?
這倆問題其實都好辦,針對第一個問題,我們其實可以根本不用實體的'<img>’直接利用’Image’物件即可,第二個問題明顯是因為的大小與獲取到的圖片大小不一致所產生的,綜合這兩點,對程式碼進行進化!
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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()
函式 //詳情
1 2 3 4 |
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 畫布的渲染上下文 //詳情 就當作一塊畫布,已經渲染出來的東西就已經留在了上面,無法再修改,如果想要更改畫面上已經存在的元素的大小位置形狀等等屬性,那麼在程式層面,就只能(個人理解,不一定對,如果有問題請一定跟我嘮嘮)把之前的畫布清空再重新渲染。
這個思路與我們之前端開發中動畫相關的開發思路不同,並不是像之前那樣直接操作現有元素屬性就可以改變該元素在畫面上的呈現結果的,而在這裡其實更像是在現實生活中的動畫製作原理就是
每一幀都需要重新繪製整張畫面
而其實這是任何動畫渲染方式的最底層思路與行為
話說回來按照上文相關的開發思路,實現這個功能的程式碼如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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;//將標誌置為已抬起狀態 }) |
能不能弄的高大上一點啊
主要吧,這個黑框太醜了,透露著一種原始和狂野,以及來自工科男審美的粗糙感…
能不能弄的好看點,起碼讓它看上去是一個工具不是一個實驗
我的想法是這樣的,待被擷取的圖片上應該蒙上一層半透明白色遮罩,使用者框選出的部分是沒有遮罩的,這樣效果可以為功能增加視覺上的材質感及舒適感,同時顯得高階
是不是稍微好些了
可是,怎麼實現?
簡單來說,就是在原有的畫布上再蒙半透明的一層畫布,然後讓這一層有一部分是沒有的就可以實現了,總的來說就是蒙版和遮罩的思路,在canvas中也有相關的api,但是我愣是沒看明白
負責任的貼一個連結
不過開發就是這樣,條條大路出bug
我想到這個功能的瞬間腦子像抽了一樣,出現了這麼一種實現方法
見下圖
mask層可以分為A,B,C,D四個矩形區域,在圖中兩個藍色的點是已知的(使用者自己畫出來的),在下層圖片大小已知的前提下,這四個矩形區域的四個點都是可以計算出來的,從而其高度和寬度也可以計算出來,這樣就可以利用這些資料畫出一個半透明的矩形,將四個半透明矩形都畫出來後,就能夠實現之前設計出的效果了,具體程式碼如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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()
函式 //詳情
1 2 3 4 5 6 7 8 9 |
var resultFile = {} theCanvas.addEventListener("mouseup", e => { resultCanvas.toBlob(blob => { resultFile = blob; console.log(blob);//Blob(1797) {size: 1797, type: "image/png"} } }) flag = false; }) |
然後就可以用Ajax上傳拉,具體怎麼上傳就需要具體問題具體分析了
至此,整個外掛的思路及需要用到相關技術都捋清楚了,接下來就可以開始按照上文的需求進行開發了,而這是下一篇文章要講的事情了
能看到這的絕對很閒
這篇文章的長度讓我想起讀研時被畢業論文統治的恐懼
本來想著連同元件開發一起在一篇內寫完呢,但是實在太長還是放棄了
身體和家人都是最重要的,今年還沒過一個月就被上了很多課