手把手教学系列之: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.目标

这个月末以前,我争取开发一下裁剪的插件,共你们使用,毕竟造轮子仍是颇有趣的。但愿你们都能来捧场~缓存

相关文章
相关标签/搜索