舒適提示:這裏除了一些幼稚的小組件啥也沒有javascript
距離寫完上一篇實踐是檢驗程序員的惟一標準01:用戶不想跟你說話並向你扔出一張圖片 - 圖片上傳組件開發【思路篇】過去了大半年,纔開始寫開發篇真的是使人悲哀,不過有句話說的好,開始作一件事最好的時間是大半年前,其次是如今
上一篇偏設計和嘗試技術可否實現,這一篇會在工程層面實現,而且保證他能被(輕易)引用!
上一篇文章的評論裏好多同窗(差很少3我的)但願我傳到git上。好吧,本文最終的勞動成果會放上去的,不過那是下一篇文章乾的事了,不過這裏我已經把所有源碼貼上來了- -css
上傳到了github上,以爲好的給星哦!l-imgupload //181119html
在以前那篇文章中,又習慣性的作了不少無用的設計,你就是一個上傳圖片的組件,低調點謝謝,因此最終我搞成了這樣子java
state-1:初始狀態git
state-2:完成載入狀態程序員
state-3:圖片截取github
整體來講,把能剩的按鈕都省了,本體就是個框,適合放在任何地方,此外爲了防止破壞頁面的總體性,組件再也不自帶截圖預覽功能,而是經過事件的方式將所截取的圖像的DataURL實時穿給父組件,方便父組件自由使用(圖中的展現區就是在父組件中寫的)web
在一開始設計組件的時候簡直就是父母給孩子報課外班的心情,但願能儘量的知足各類需求,但轉頭想一想先把最基本的功能(作個好人)作好別的都是能夠慢慢加上的(懶)canvas
要保證基本功能能(好)用,大概如下這幾點:
1.要讓其大小可控,方便應用於不一樣場景,因此組件的寬高有必要成爲參數
2.對於被裁出的部分,在原圖中看和拎出來單獨看視覺上差異還挺大的,因此一個能夠實時單獨展示所截取內容的功能就挺重要的
3.在大多數狀況下,裁剪區域的選定多是有固定比例的,因此要將是否限制比例以及按照什麼比例做爲參數,根據適用場景決定segmentfault
因此組件的參數和事件大概也就這麼幾個了
參數名:inputWidth
說明:組件寬度
類型:Number
默認值:200px
參數名:inputHeight
說明:組件高度
類型:Number
默認值:200px
參數名:cuttingRatio
說明:裁剪比例,限定比例爲寬/高,爲空時沒有比例限制
類型:Number
默認值:0
事件名:getImageData
說明:框選完成後鼠標擡起時觸發,返回選定區域的圖像數據
參數:blobData
參數格式:Blob對象
事件名:getImageDataURL
說明:鼠標拖動的每一幀觸發,返回選定區域的圖像數據,可用於預覽區域展現
參數:dataURL
參數格式:dataURL
因爲功能很單一,HTML的佈局也就很簡單
大概結構以下
<根標籤> <提示信息 />//絕對定位,位於組件下方,初始狀態不可見,載入圖片後出現 <從新選擇按鈕 />//絕對定位,位於組件右上角,初始狀態不可見,載入圖片後出現 <初始及載入層 />//絕對定位,位於畫布上方,大小與畫布徹底相同 <畫布 />//canvas <隱藏的input標籤 />//不可見 </根標籤>
HTML代碼以下
<template> <div class="inputArea" :style="{height:inputHeight+'px',width:inputWidth+'px'}"> <!--提示區域--> <div class="notice" :class="{showNotice:noticeFlag}"> {{notice}} <div class="close-notice" @click="closeNotice">X</div> </div> <!--從新選擇按鈕--> <div class="reloadBtn" @click="openWindow"> 從新選擇 </div> <!--初始及載入層--> <div class="blankMask" @click="openWindow" v-if="loadFlag!=2"> <img v-if="loadFlag==0" src="../assets/img.png" /> <img v-if="loadFlag==1" src="../assets/loading.png" /> <div class="text">{{loadFlag == 0?'點擊瀏覽圖片':'加載中'}}</div> </div> <!--畫布--> <div class="canvasArea"> <canvas id="inputAreaCanvas" @mousedown="setStartPoint" @mousemove="drawArea" @mouseup="reset"> </canvas> </div> <!--隱藏的input標籤--> <input id="input" type="file" @change="loadImg" /> </div> </template>
對應的css以下
<style> .inputArea { position: relative; background: #000; } .inputArea .notice { height: 30px; line-height: 30px; text-align: center; background: #FFF; color: #2C3E50; font-size: 12px; text-align: center; position: absolute; width: 90%; margin-left: 5%; left: 0px; transition: all .5s; bottom: -30px; opacity: 0; box-shadow: 0px 0px 5px rgba(0,0,0,0.3); border-radius: 2px; -moz-user-select: none; -ms-user-select: none; -webkit-user-select: none; } .inputArea .notice.showNotice { bottom: 0px; opacity: 1; } .inputArea .notice .close-notice { position: absolute; right: 10px; top: 0px; height: 30px; line-height: 30px; cursor: pointer; } .inputArea .reloadBtn { height: 20px; padding: 2px 5px 2px 5px; text-align: center; line-height: 20px; font-size: 12px; background: #FFFFFF; box-shadow: 0px 0px 5px rgba(0,0,0,0.3); color: #2C3E50; position: absolute; top: 5px; right: 5px; cursor: pointer; transition: all 0.5s; } .inputArea .reloadBtn:hover { box-shadow: 0px 0px 8px rgba(0,0,0,0.5); } .inputArea .blankMask { position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; display: flex; color: gainsboro; border-radius: 2px; background: #FFF; cursor: pointer; flex-direction: column; -ms-flex-direction: column; justify-content: center; -webkit-justify-content: center; align-items: center; -webkit-align-items: center; transition: all 0.5s; z-index: 2; } .inputArea .blankMask:hover { background: #F6F6F6; } .inputArea .blankMask .text { margin-top: 10px; font-size: 16px; font-weight: bold; } .inputArea .blankMask img { height: 40px; width: 40px; } .inputArea .canvasArea { display: flex; align-items: center; -webkit-align-items: center; justify-content: center; -webkit-justify-content: center; height: 100%; width: 100%; } #input { display: none; } </style>
props:{ inputWidth:{ type:Number, default:200 }, inputHeight:{ type:Number, default:200 }, cuttingRatio:{ type:Number, default:0 } }, data() { return { mouseDownFlag: false,//記錄鼠標點擊狀態用標記 loadFlag: 0,//記錄圖像家在狀態用標記 resultImgData: {},//被截取數據 input: {},//輸入框對象 imgObj: new Image(),//圖片對象 inputAreaCanvas: {},//主體canvas對象 inputArea2D: {},//主體CanvasRenderingContext2D對象 notice: "拖拽鼠標框選所須要的區域",//提示區域文本 noticeFlag: false,//提示區域展現狀態標記 dataURL:"",//被截取dataURL tempCanvas:{},//存放截取結果用canvas對象 tempCanvas2D:{},//存放截取結果用CanvasRenderingContext2D對象 resetX:0,//組件起點橫座標 resetY:0,//組件起點縱座標 /* 181031改:其實並不用這兩個變量 startX:0,//截取開始點橫座標 startY:0,//截取開始點縱座標 */ resultX:0,//截取結束點橫座標 resultY:0,//截取結束點縱座標 } }, mounted: function() { //對象初始化 this.input = document.getElementById('input') this.inputAreaCanvas = document.getElementById("inputAreaCanvas"); this.inputArea2D = this.inputAreaCanvas.getContext("2d"); this.tempCanvas = document.createElement('canvas'); this.tempCanvas2D = this.tempCanvas.getContext('2d'); },
此部分開始放在methods對象下
圖片讀取的功能主要設計兩個方法:
openWindow
方法主要用於觸發隱藏的<input>
標籤的文件讀取功能
//打開文件選擇窗口 openWindow() { this.input.click(); },
loadImg
方法完成了如下幾個步驟
//載入圖片方法,當圖片被選中後,input的value發生改變時觸發 loadImg() { let vm = this; let reader = new FileReader(); //每次載入後傳給父組件的dataURL清空 this.dataURL = ''; //文件爲空時返回 if(this.input.files[0] == null) { return } //開始載入圖片,並將數據經過dataURL的方式讀取,展示載入層信息 this.loadFlag = 1; reader.readAsDataURL(this.input.files[0]); //讀取完成後將圖像的dataURL數據賦給image對象的src的屬性,使其加載圖像 reader.onload = function(e) { vm.imgObj.src = e.target.result; } //圖像加載完成,利用drawImage將image對象渲染至canvas this.imgObj.onload = function() { vm.loadFlag = 2; vm.noticeFlag = true; //計算載入圖像的長寬比,決定圖片顯示方式 let ratioHW = (vm.imgObj.height/vm.imgObj.width) //每張圖片根據比例不一樣,總有一個方向佔滿顯示區域 if(ratioHW > 1) { vm.inputAreaCanvas.height = vm.inputHeight; vm.inputAreaCanvas.width = vm.inputHeight / ratioHW; } else { vm.inputAreaCanvas.width = vm.inputWidth; vm.inputAreaCanvas.height = vm.inputWidth * ratioHW; } /* 181031改:其實並不用這兩個變量,直接用offset屬性便可 //獲取組件起點座標 vm.resetX = vm.inputAreaCanvas.getBoundingClientRect().left; vm.resetY = vm.inputAreaCanvas.getBoundingClientRect().top; */ //將獲取的圖像數據選在至canvas vm.inputArea2D.clearRect(0, 0, vm.inputAreaCanvas.width, vm.inputAreaCanvas.height); vm.inputArea2D.drawImage(vm.imgObj, 0, 0, vm.inputAreaCanvas.width, vm.inputAreaCanvas.height); vm.inputArea2D.fillStyle = 'rgba(0,0,0,0.5)'; //設定爲半透明的黑色 vm.inputArea2D.fillRect(0, 0, vm.inputWidth, vm.inputHeight); //矩形A } },
圖像截取功能包含四個方法:
setStartPoint
方法用於獲取截取範圍的起點以及更改點擊狀態
//獲取截取範圍起始座標,當鼠標在canvas標籤上點擊時觸發 setStartPoint(e) { this.mouseDownFlag = true; //改變標記狀態,置爲點擊狀態 this.startX = e.offsetX //得到起始點橫座標 this.startY = e.offsetY //得到起始點縱座標 },
drawArea
方法經過如下步驟實現了選定區域的展示和截取功能:
//選擇截取範圍,當鼠標被拖動時觸發 drawArea(e) { //當鼠標被拖動時觸發(處於按下狀態且移動) if(this.mouseDownFlag) { /*181031改:結束座標的獲取方式進行了優化,請忽略此處 //在canvas標籤上範圍的終點橫座標 this.resultX = parseInt(e.clientX - this.resetX); //在canvas標籤上範圍的終點縱座標,根據比例參數決定 if(this.cuttingRatio != 0) { //根據必定比例截取 this.resultY = this.startY + parseInt((1 / this.cuttingRatio) * (this.resultX - this.startX)) } else { //自由截取 this.resultY = parseInt(e.clientY - this.resetY); } */ //在canvas標籤上範圍的終點橫座標 this.resultX = e.offsetX; //在canvas標籤上範圍的終點縱座標,根據比例參數決定 if(this.cuttingRatio != 0) { //根據必定比例截取 this.resultY = this.startY + parseInt((1 / this.cuttingRatio) * (this.resultX - this.startX)) } else { //自由截取 this.resultY = e.offsetX; } //所選區域外陰影部分 this.inputArea2D.clearRect(0, 0, this.inputWidth, this.inputHeight); //清空整個畫面 this.inputArea2D.drawImage(this.imgObj, 0, 0, this.inputAreaCanvas.width, this.inputAreaCanvas.height); //從新繪製圖片 this.inputArea2D.fillStyle = 'rgba(0,0,0,0.5)'; //設定爲半透明的白色 this.inputArea2D.fillRect(0, 0, this.resultX, this.startY); //矩形A this.inputArea2D.fillRect(this.resultX, 0, this.inputWidth, this.resultY); //矩形B this.inputArea2D.fillRect(this.startX, this.resultY, this.inputWidth - this.startX, this.inputHeight - this.resultY); //矩形C this.inputArea2D.fillRect(0, this.startY, this.startX, this.inputHeight - this.startY); //矩形D //當選擇區域大於0時,將所選範圍內的圖像數據實時返回 if(this.resultX - this.startX > 0 && this.resultY - this.startY > 0) { this.resultImgData = this.inputArea2D.getImageData(this.startX, this.startY, this.resultX - this.startX, this.resultY - this.startY); //canvas to DataURL this.tempCanvas.width = this.resultImgData.width; this.tempCanvas.height = this.resultImgData.height; this.tempCanvas2D.putImageData(this.resultImgData, 0, 0) this.dataURL = this.tempCanvas.toDataURL('image/jpeg', 1.0); } } },
reset
方法用於重製鼠標點擊狀態,並獲取blob格式的所截圖像數據,觸發getImageData
事件將數據專遞給父組件
//結束選擇截取範圍,返回所選範圍的數據,重製鼠標狀態,當鼠標點擊結束時觸發 reset() { this.mouseDownFlag = false; //將標誌置爲已擡起狀態 let blob = this.dataURLtoBlob(this.dataURL) this.$emit('getImageData', blob); },
dataURLtoBlob方法的做用是將dataURL對象轉化爲Blob對象,來自Blob/DataURL/canvas/image的相互轉換-Lorem
因爲在IE中並不支持Canvas.toBlob,因此須要這裏走個彎路,本身寫一下這個方法
//DataURL to Blob,兼容IE dataURLtoBlob(dataurl) { let arr = dataurl.split(',') let mime = arr[0].match(/:(.*?);/)[1] let bstr = atob(arr[1]) let n = bstr.length let u8arr = new Uint8Array(n) while(n--) { u8arr[n] = bstr.charCodeAt(n) } return new Blob([u8arr], { type: mime }); }
//關閉提示信息 closeNotice() { this.noticeFlag = false },
經過監聽dataURL
的變化,將結果實時返回給父組件以達到預覽的目的
watch:{ dataURL:function(newVal,oldVal){ this.$emit('getImageDataUrl', this.dataURL)//將所截圖的dataURL返回給父組件,共預覽使用 } },
用起來嘛,就很簡單了
<template> <div id="app"> <MainBlock @getImageData="getImageData" @getImageDataUrl="getImageDataUrl" :inputHeight='300' :inputWidth='300' ></MainBlock> <img :src="src"/> </div> </template>
<script> import MainBlock from './components/mainBlock' export default { name: 'App', components: { MainBlock, }, data() { return { imageData: '', src: "" } }, methods: { getImageData(imageData) { this.imageData = imageData console.log(this.imageData) }, getImageDataUrl(dataUrl) { this.src = dataUrl } }, } </script>
第一次寫相對獨立的組件
從有想法到徹底實現成一個能用的組件,中間仍是有不少路的,並且功能還簡單的使人髮質,怎麼說呢感受本身能夠進步的空間還很大啊
不過使人欣慰的是這個組件已經用在單位的一個項目中了,可喜可賀
雖然拖了好久,不過仍是有成就感的,但願能繼續下去,誰知道能走到哪呢
歡迎你們挑錯提意見,雖然不情願,可是接受
能看到這的,功能應該都實現了把?!
以前應該是腦子抽了,其實在drawArea
方法中,e.offsetX和e.offsetY
就是
parseInt(e.clientX - this.resetX) parseInt(e.clientY - this.resetY)
增長了選框拖拽功能,而且上傳到了github上,以爲好的給星哦!l-imgupload