實踐是檢驗程序員的惟一標準01:用戶不想跟你說話並向你扔出一張圖片 - 圖片上傳組件開發【思路篇】

舒適提示:這裏除了一些幼稚的小組件啥也沒有
舒適提示-續:這是一個新的系列,寫一些實際開發中遇到的一些經常使用的功能,想法笨拙,代碼亂套javascript

寫在前面

圖片上傳,做爲web端一個經常使用的功能,在不一樣的項目中有不一樣的需求,在這裏實現一個比價基本的上傳圖片插件,主要能實現圖片的瀏覽,剪裁,上傳這三個功能,同時也是爲了讓本身對圖片/文件上傳和HTML5中名聲在外的canvas相關可以有一些瞭解html

上傳到了github上,以爲好的給星哦!l-imgupload //181119前端

我就要自行車 - 需求整理

放眼WWW,通常的圖片上傳模塊,主要就是實現了三個功能,圖片的預覽,圖片的剪裁及預覽,圖片的上傳,那我也就整這麼一個吧,再細化一下需求java

圖片的預覽

用戶使用:用戶點擊「選擇圖片」,彈出文件瀏覽器,能夠選擇本地的圖片,點擊確認後,所選圖片會按照原始比例出如今頁面的瀏覽區域中
組件調用:開發者能夠本身定義圖片預覽區域的大小,並限定所傳圖片的文件大小和尺寸大小git

圖片的剪裁

用戶使用:用戶根據提示,在預覽區域的圖片上拖動鼠標框出想要上傳的圖片區域,而且能在結果預覽區域看到本身的剪裁結果
組件調用:開發者能夠自定義是否剪裁圖片,並能夠定義是否限定剪裁圖片的大小及比例,而且設定具體大小及比例程序員

圖片的上傳

用戶使用:用戶點擊「圖片上傳」,圖片開始上傳,現實「上傳中…」,完成後顯示「上傳完成」
組件調用:開發者獲得base64格式的urlData圖片,本身編寫調用Ajax的函數及其回調函數github

扔出原型圖

做爲設計師,扔圖是個人最愛,畫了一套全功能,包含剪裁及剪裁瀏覽的原型圖web

state-1:初始狀態
插件原型-1ajax

state-2:點擊"選擇圖片",瀏覽本地後載入圖片
插件原型-2canvas

state-3:剪裁,在圖片區域上拖動鼠標選擇要剪裁的部分,確認要上傳的部分
插件原型-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簡介及DATA URL的利弊 - 薛陳磊

說到底這dataURL我就粗略的理解它爲URL形式的data,也就是說這段URL並非與普通的URL同樣指向某個地址,而是它自己就是數據,咱們試着把這一堆字符粘到一個<img>src屬性中
Data:URL結果

終於看到了,結果正如所料,將這段包含了數據的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);
}

結果以下
CanvasImg結果-1
從圖中看,左側是以前的'<img>',右側是渲染了圖片信息的<canvas>
這麼看來雖然成功?在<canvas>中渲染出了圖片可是有兩個明顯的問題

1.左邊的'<img>'留着幹啥?
2.右邊看上去是否是有點不同?

這倆問題其實都好辦,針對第一個問題,咱們其實能夠根本不用實體的'<img>'直接利用'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);
}

CanvasImg結果-2

結果與咱們所期待的同樣,至此咱們成功的在'<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()函數 //詳情
可是讓用戶本身拖出一個框就比較複雜了,先分析一下用戶的一套動做都有什麼

  1. 用戶選定起始點,點下鼠標左鍵
  2. 用戶選定截圖區域的大小,保持鼠標左鍵不擡起,同時移動鼠標選擇
  3. 用戶完成選擇,擡起鼠標左鍵

回過頭再來看程序須要幹什麼

  1. 獲取起始點的座標,並記錄爲已點擊狀態
  2. 判斷一下若是爲已點擊狀態那麼,獲取每一次移動/幀的鼠標座標,並計算出與起始點之間的橫縱座標距離,而這距離就是所畫框的長度和寬度,清除上一幀的整個畫面,再繪製一個新的圖片再畫一個新的框,同時按照框的起始座標及寬高,截取圖像信息,再清除預覽區域的上一幀的畫布,再將這一幀的圖像信息載入
  3. 鼠標擡起後,中止記錄及繪製,保持最終一幀的框停留在畫面上

在這裏,要說明一下,爲何非要清除整個畫面不可,其實能夠把經過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;//得到起始點縱座標
    //添加於2018.3.6:
    //這裏有些問題,在本文的條件下e.clientX是對的,但是實際上是應該爲相對對象的座標而不是瀏覽器,因此應該爲e.offsetX 感謝 @高遠 同窗提醒
    startX = e.offsetX;//得到起始點橫座標
    startY = e.offsetY;//得到起始點縱座標
})

//當鼠標在移動
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;
})

而後就能夠用Ajax上傳拉,具體怎麼上傳就須要具體問題具體分析了

至此,整個插件的思路及須要用到相關技術都捋清楚了,接下來就能夠開始按照上文的需求進行開發了,而這是下一篇文章要講的事情了

能看到這的絕對很閒
這篇文章的長度讓我想起讀研時被畢業論文統治的恐懼
原本想着連同組件開發一塊兒在一篇內寫完呢,可是實在太長仍是放棄了
身體和家人都是最重要的,今年還沒過一個月就被上了不少課

181119修改

前端時間寫完了實踐是檢驗程序員的惟一標準02:用戶不想跟你說話並向你扔出一張圖片 - 圖片上傳組件開發【開發篇】,逐行代碼說明的- -

增長了選框拖拽功能,而且上傳到了github上,以爲好的給星哦!l-imgupload

相關文章
相關標籤/搜索