手把手教學系列之:web圖片裁剪(一)

背景

你們好,我是六六。在學習裁剪功能的過程當中,發現有不少文章講的不是那麼清晰易懂,讓六六繞了不少彎路,因此今天呢,爲了讓你們再也不繞彎,六六要詳詳細細的手把手教你們寫一個裁剪功能出來。web

1.目錄

  • 神奇canvas的那些api
  • 一步一步地實戰教學
  • 總結
  • 我的目標

2.神奇的canvas和動畫

裁剪功能的核心之中固然是canvas這個技術,若是不懂的能夠去mdn上過一遍這個,不用看的太深,瞭解它是幹啥的,怎麼幹就好了。以個人大白話說,就是在座標系內可以操做每一個像素點的。接下來,咱們先來了解一下關於裁剪核心的api和相關知識:算法

2.1 drawImage(image, x, y):

用法:繪製一張圖片到canvas元素裏面  
image:image對象或者是canvas對象  
x,y:爲座標的起始點  
實例:
function draw() {
    var ctx = document.getElementById('canvas').getContext('2d');
    var img = new Image();
     img.src = 'images/backdrop.png';           // 圖片地址
    img.onload = function(){
      ctx.drawImage(img,0,0);                   // 拿到image對象,畫入canvas上
    }
  }

其實把,這個api有九個參數,藉助官網的圖:
複製代碼

2.2 ctx.getImageData(left, top, width, height):

用法:返回一個imageData對象,包含一個data像素數組,一個width爲寬,一個height爲高的
複製代碼

2.3何爲imageData對象?

關於data數據(以個人大白話理解來講,專業說法在上面),其實就是描述每一個像素點的信息
(數據很是大),咱們都知道rgba吧,前三個數字表明像素點的顏色,第四個數字表明透明度,
因此data數據呢,就是整個選取部分的像素點信息的集合。每一個像素點有四個值,
依次push到data數據中。
複製代碼

如上圖所示,咱們就知道第一個像素點的顏色就是rgba(114,112,113,255/255),那麼他的位置
就是位於座標(0,0)。假如選取的canvas大小爲500\*500的,
那麼這個data數組的大小就是500\*500\*4。
複製代碼

2.4 ctx.putImageData(myImageData, dx, dy);

用法:在場景中寫入像素數據  
myImageData:就是imageData對象  
dx,dy:就是場景的座標起點
複製代碼

2.5 canvas.toBlob(callback, type, encoderOptions)

用法:方法創造Blob對象,用以展現canvas上的圖片;這個圖片文件能夠被緩存或保存到本地  
callback:回調函數,可得到一個單獨的Blob對象參數  
type:DOMString類型,指定圖片格式,默認格式爲image/png。  
encoderOptions :Number類型,值在0與1之間,當請求圖片格式爲image/jpeg
或者image/webp時用來指定圖片展現質量。若是這個參數的值不在指定類型與範圍以內,
則使用默認值,其他參數將被忽略。

複製代碼

2.6 requestanimationframe:

首先咱們知道裁剪功能是須要運動,那麼確定會用到動畫。
h5提供了咱們一個終極動畫解決的函數,就是requestanimationframe.
複製代碼

3實戰教學

3.1思路分析

  • 首先咱們須要藉助input元素上傳圖片獲取img對象(易)
  • 把img對象寫入canvas元素中(易)
  • 須要建立一個裁剪方框對象用來裁剪圖片,方框隨用戶交互移動和縮放(難)
  • 獲取方框內的圖片信息導入出來
  • 視覺優化,將整張圖片繪製灰色,選中的部分爲亮色(中)
  • 完成上傳及其餘操做

思路很容易懂,接下來咱們就來一步一步實現。上面的每一個api必須熟練掌握,不熟悉的回頭再看一看。下面就是代碼加gif圖片演示,基本上每句代碼都是有備註的。canvas

3.2獲取圖片及顯示圖片:

思路:
首先經過input元素咱們能夠獲取到img對象
,在圖片加載出來後就能夠畫入畫圖中,
並能夠循環動畫。
複製代碼
<template>
  <div id="app">
    <canvas width='500' height='500' id='canvas'></canvas>
     <input type="file"
           @change="handleChange"
           multiple='true'
           accept='image/*'
           >
  </div>
</template>

<script>
export default{
  data(){
    return{
      ctx:'',
      img:''
    }
  },
  mounted(){
    // 獲取canvas對象
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    // 講ctx存入data中
    this.ctx=ctx
  },
  methods:{
       // 讀取圖片
    handleChange (e) {
      const that = this
      // 建立一個文件讀取流
      const reader = new FileReader()
      reader.readAsDataURL(e.target.files[0])
      // 文件加載完成後能夠讀入
      reader.onload = function () {
        // this.reslut 爲圖片信息,就開始調用drawImg方法將圖片寫入canvas中
        that.drawImg(this.result)
      }
    },
    // 建立一個圖片對象 畫到畫布上
    drawImg (imgData) {
      const that = this
      const img = new Image()
      img.src = imgData
      this.img = img
      img.onload = function () {
        // 循環動畫
        window.requestAnimationFrame(that.animate)
      }
    },
    animate(){
         // 清除畫布,在下一次循環會畫入從新
      this.ctx.clearRect(0,0,500,500)
      // 畫照片
      this.ctx.drawImage(this.img, 0, 0, this.img.width, this.img.height, 0, 0, 500, 500)
      // 循環動畫
      window.requestAnimationFrame(this.animate)
    }
  }
}
</script>

<style scoped>
canvas{
  position: absolute;
  top:50%;
  left:50%;
  transform: translate(-50%,-50%);
  border:1px solid red
}
</style>

複製代碼

3.3 繪製裁剪框,隨着鼠標移動及縮放

思路:藉助三個事件,鼠標按下,移動,擡起。在移動事件
裏面經過offsetX和offsetY獲取鼠標在canvas內的座標
。而後建立一個函數根據鼠標的座標用來畫出這個裁剪框,
放入animate函數裏面循環。
複製代碼

演示: 後端

<template>
  <div id="app">
    <canvas width='500' height='500' id='canvas'></canvas>
     <input type="file"
           @change="handleChange"
           multiple='true'
           accept='image/*'
           >
  </div>
</template>

<script>
/* eslint-disable */
export default{
  data(){
    return{
      ctx:'',
      img:'',
      rectLeft:150,
      rectTop:150,
      rectWidth:200,
      rectHeight:200
    }
  },
  mounted(){
    const that=this
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    this.ctx=ctx
    // 新增----綁定點擊事件,根據鼠標移動座標畫出裁剪框
      this.down=canvas.addEventListener('mousedown',()=>{
        canvas.onmousemove=function(e){
        let x=e.offsetX
        let y=e.offsetY
         x <= 100 ? that.rectLeft=0: that.rectLeft = x - 100
         y <= 100 ? that.rectTop =0: that.rectTop = y - 100
        x >= 400 ? that.rectLeft = 300 : that.rectLeft = x - 100
        y >= 400 ? that.rectTop = 300 : that.rectTop = y - 100
        if(x<=100){
          that.rectLeft=0
        }
        if(y<=100){
           that.rectTop=0
        }
      }
    })
    canvas.addEventListener('mouseup',()=>{
    console.log('擡起了')
   canvas.onmousemove=null
  })
  },
  methods:{
    handleChange (e) {
      const that = this
      const reader = new FileReader()
      reader.readAsDataURL(e.target.files[0])
      reader.onload = function () {
        that.drawImg(this.result)
      }
    },
    drawImg (imgData) {
      const that = this
      const img = new Image()
      img.src = imgData
      this.img = img
      img.onload = function () {
        window.requestAnimationFrame(that.animate)
      }
    },
    animate(){
      this.ctx.clearRect(0,0,500,500)
      this.ctx.drawImage(this.img, 0, 0, this.img.width, this.img.height, 0, 0, 500, 500)
      // 新增---畫裁剪框
      this.drawRect()
      window.requestAnimationFrame(this.animate)
    },
    // 新增----畫出裁剪框
    drawRect(){
      this.ctx.beginPath()
      this.ctx.lineWidth = 2
      this.ctx.strokeStyle = 'rgba(0,0,0,0.6)'
      this.ctx.strokeRect(this.rectLeft, this.rectTop, this.rectWidth, this.rectHeight)
    }
  }
}
</script>

<style scoped>
canvas{
  position: absolute;
  top:50%;
  left:50%;
  transform: translate(-50%,-50%);
  border:1px solid red
}
input{
  position: absolute;
  top:10%;
  left:50%;
  transform: translate(-50%,-50%);
}
</style>

複製代碼

3.4 視覺優化,背景呈現灰色,裁剪出呈現亮色

爲了更好的提升用戶體驗感,不得不得進行視覺優化:
思路:在每次循環動畫的開始,咱們能夠先獲取裁剪框
內的imageData,就是獲取照片原色彩,
以後經過算法使用putImageData方法將整個canvas對象色彩變成灰色,
而後再把以前獲取
裁剪框內的彩色在使用putImageData繪製上去便可。
複製代碼

<template>
  <div id="app">
    <canvas width='500' height='500' id='canvas'></canvas>
     <input type="file"
           @change="handleChange"
           multiple='true'
           accept='image/*'
           >
  </div>
</template>

<script>
/* eslint-disable */
export default{
  data(){
    return{
      ctx:'',
      img:'',
      rectLeft:150,
      rectTop:150,
      rectWidth:200,
      rectHeight:200,
      chooseRgb:[]
    }
  },
  mounted(){
    const that=this
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d')
    this.ctx=ctx
      this.down=canvas.addEventListener('mousedown',()=>{
        canvas.onmousemove=function(e){
        let x=e.offsetX
        let y=e.offsetY
         x <= 100 ? that.rectLeft=0: that.rectLeft = x - 100
         y <= 100 ? that.rectTop =0: that.rectTop = y - 100
        x >= 400 ? that.rectLeft = 300 : that.rectLeft = x - 100
        y >= 400 ? that.rectTop = 300 : that.rectTop = y - 100
        if(x<=100){
          that.rectLeft=0
        }
        if(y<=100){
           that.rectTop=0
        }
      }
    })
    canvas.addEventListener('mouseup',()=>{
    console.log('擡起了')
   canvas.onmousemove=null
  })
  },
  methods:{
    handleChange (e) {
      const that = this
      const reader = new FileReader()
      reader.readAsDataURL(e.target.files[0])
      reader.onload = function () {
        that.drawImg(this.result)
      }
    },
    drawImg (imgData) {
      const that = this
      const img = new Image()
      img.src = imgData
      this.img = img
      img.onload = function () {
        window.requestAnimationFrame(that.animate)
      }
    },
    animate(){
      this.ctx.clearRect(0,0,500,500)
      this.ctx.drawImage(this.img, 0, 0, this.img.width, this.img.height, 0, 0, 500, 500)
      this.drawChoose()
      this.drawHui()
      this.drawRect()
      window.requestAnimationFrame(this.animate)
      
    },
    drawRect(){
      this.ctx.beginPath()
      this.ctx.lineWidth = 2
      this.ctx.strokeStyle = 'rgba(0,0,0,0.6)'
      this.ctx.strokeRect(this.rectLeft, this.rectTop, this.rectWidth, this.rectHeight)
    },
       // 新增----獲取裁剪框的色彩色彩
    drawChoose () {
      const data = this.ctx.getImageData(this.rectLeft, this.rectTop, this.rectWidth, this.rectHeight)
      this.chooseRgb = data
    },
        //新增---- 所有圖片變灰色而且畫上彩色的
    drawHui () {
      const data = this.ctx.getImageData(0, 0, 500, 500)
      for (let i = 0; i < data.data.length; i += 4) {
        const grey = (data.data[i] + data.data[i + 1] + data.data[i + 2]) / 3
        data.data[i] = data.data[i + 1] = data.data[i + 2] = grey
      }
      // 將變成灰色的像素繪製上去
      this.ctx.putImageData(data, 0, 0)
      // 將彩色的裁剪框繪製上去
      this.ctx.putImageData(this.chooseRgb, this.rectLeft, this.rectTop)
    },
  }
}
</script>

<style scoped>
canvas{
  position: absolute;
  top:50%;
  left:50%;
  transform: translate(-50%,-50%);
  border:1px solid red
}
input{
  position: absolute;
  top:10%;
  left:50%;
  transform: translate(-50%,-50%);
}
</style>

複製代碼

3.5獲取裁剪框的內容並顯示出來

思路:須要建立一個新的canvas對象
,隨後調用putImageData將裁剪的像素對象畫上去便可
複製代碼
<template>
  <div id="app">
    <canvas width='500' height='500' class='canvas1'></canvas>
    <!-- 新增的canvas元素-->
    <canvas width='200' height='200' class='canvas2'></canvas>
     <input type="file"
           @change="handleChange"
           multiple='true'
           accept='image/*'
           >
          <button @click='caijian'>點擊裁剪</button>
  </div>
</template>

<script>
/* eslint-disable */
export default{
  data(){
    return{
      ctx:'',
      img:'',
      rectLeft:150,
      rectTop:150,
      rectWidth:200,
      rectHeight:200,
      chooseRgb:[]
    }
  },
  mounted(){
    const that=this
    const canvas = document.querySelector('.canvas1')
    const ctx = canvas.getContext('2d')
    this.ctx=ctx
      this.down=canvas.addEventListener('mousedown',()=>{
        canvas.onmousemove=function(e){
        let x=e.offsetX
        let y=e.offsetY
         x <= 100 ? that.rectLeft=0: that.rectLeft = x - 100
         y <= 100 ? that.rectTop =0: that.rectTop = y - 100
        x >= 400 ? that.rectLeft = 300 : that.rectLeft = x - 100
        y >= 400 ? that.rectTop = 300 : that.rectTop = y - 100
        if(x<=100){
          that.rectLeft=0
        }
        if(y<=100){
           that.rectTop=0
        }
      }
    })
    canvas.addEventListener('mouseup',()=>{
    console.log('擡起了')
   canvas.onmousemove=null
  })
  },
  methods:{
    handleChange (e) {
      const that = this
      const reader = new FileReader()
      reader.readAsDataURL(e.target.files[0])
      reader.onload = function () {
        that.drawImg(this.result)
      }
    },
    drawImg (imgData) {
      const that = this
      const img = new Image()
      img.src = imgData
      this.img = img
      img.onload = function () {
        window.requestAnimationFrame(that.animate)
      }
    },
    animate(){
      this.ctx.clearRect(0,0,500,500)
      this.ctx.drawImage(this.img, 0, 0, this.img.width, this.img.height, 0, 0, 500, 500)
      this.drawChoose()
      this.drawHui()
      this.drawRect()
      window.requestAnimationFrame(this.animate)
      
    },
    drawRect(){
      this.ctx.beginPath()
      this.ctx.lineWidth = 2
      this.ctx.strokeStyle = 'rgba(0,0,0,0.6)'
      this.ctx.strokeRect(this.rectLeft, this.rectTop, this.rectWidth, this.rectHeight)
    },
    drawChoose () {
      const data = this.ctx.getImageData(this.rectLeft, this.rectTop, this.rectWidth, this.rectHeight)
      this.chooseRgb = data
    },
    drawHui () {
      const data = this.ctx.getImageData(0, 0, 500, 500)
      for (let i = 0; i < data.data.length; i += 4) {
        const grey = (data.data[i] + data.data[i + 1] + data.data[i + 2]) / 3
        data.data[i] = data.data[i + 1] = data.data[i + 2] = grey
      }
      this.ctx.putImageData(data, 0, 0)
      this.ctx.putImageData(this.chooseRgb, this.rectLeft, this.rectTop)
    },
    // 新增一個canvas元素 用來存儲裁剪的部分,以及上傳時須要建立這個元素。
    caijian(){
      const canvas=document.querySelector('.canvas2')
       const ctx = canvas.getContext("2d")
      canvas.width = 200
      canvas.height = 200
      ctx.putImageData(this.chooseRgb, 0, 0)
    }
  }
}
</script>

<style scoped>
.canvas1{
  position: absolute;
  top:50%;
  left:50%;
  transform: translate(-50%,-50%);
  border:1px solid red
}
.canvas2{
  position: absolute;
  top:50%;
  left:75%;
  transform: translate(-50%,-50%);
  border:1px solid red
}
input{
  position: absolute;
  top:10%;
  left:50%;
  transform: translate(-50%,-50%);
}
button{
   position: absolute;
  top:10%;
  left:70%;
  transform: translate(-50%,-50%);
}
</style>

複製代碼

3.6上傳裁剪頭像

思路:就是經過canvas.toBlob這個api實現的,
下面代碼是我以前上傳到服務器的時候寫的,因爲如今木有服務器,
就不能演示了。
複製代碼
// 須要接受canvas元素對象,上一部分建立的
uploadImg (canvas) {
// 異步獲取blob對象
      canvas.toBlob(async (blob) => {
      // 實例化一個FormData
        let fd = new FormData()
        fd.append('file', blob)
        fd.name = new Date().getTime()
        // 傳送數據
        const result = await this.$http({
          method: 'post',
          url: 'api/users/upload',
          headers: {
            'Content-Type': 'multipart/form-data'
          },
          data: fd
        })
        if (result.data.success) {
          this.isShow = false
          this.$bus.$emit('on1', result.data.url)
          this.$alert.success('更換頭像成功', 1000)
        }
      }, 'image/png')
    }
  }
複製代碼

4.總結

整個裁剪過程已經講完了,上傳那一塊仍是有疑惑的同窗,能夠參考個人這篇文章api

Vue先後端開發仿蘑菇街商城項目 這裏面有上傳頭像的先後端代碼,你們能夠去參考一下。仍是有疑惑的能夠給我留言哈。數組

5.目標

這個月末以前,我爭取開發一下裁剪的插件,共你們使用,畢竟造輪子仍是頗有趣的。但願你們都能來捧場~緩存

相關文章
相關標籤/搜索