首页 > 编程 > JavaScript > 正文

vue实现裁切图片同时实现放大、缩小、旋转功能

2019-11-19 14:15:06
字体:
来源:转载
供稿:网友

本篇文章主要介绍了vue实现裁切图片同时实现放大、缩小、旋转功能,分享给大家,具体如下:

实现效果:

  1. 裁切指定区域内的图片
  2. 旋转图片
  3. 放大图片
  4. 输出bolb 格式数据 提供给 formData 对象

效果图







大概原理:

利用h5 FileReader 对象, 获取 <input type="file"/> “上传到浏览器的文件” ,文件形式 为base64形式, 把 base64 赋给canvas的上下文。

然后给canvas 元素上加入对(mousedown)监听事件。 当用户鼠标左键在canvas按下时:

  1. 挂载对 window 对象mousemove事件 ---> 获取 鼠标移动x,y距离.从而操作 canvas里的图像的位置移动。
  2. 挂载对 window 对象mouseup 事件, 清除 mousemove事件的绑定。(同时该事件触发后会被删除)

剩下的 放大、缩小 、 旋转 是对 canvas 对象的操作/坐标体系的操作。具体api详见mdn canvas 文档

代码

dom.js

export const on = ({el, type, fn}) => {     if (typeof window) {       if (window.addEventListener) {         el.addEventListener(type, fn, false)      } else {         el.attachEvent(`on${type}`, fn)      }     }  }  export const off = ({el, type, fn}) => {    if (typeof window) {      if (window.addEventListener) {        el.removeEventListener(type, fn)      } else {        el.detachEvent(`on${type}`, fn)      }    }  }  export const once = ({el, type, fn}) => {    const hyFn = (event) => {      try {        fn(event)      }       finally {        off({el, type, fn: hyFn})      }    }    on({el, type, fn: hyFn})  }  // 最后一个  export const fbTwice = ({fn, time = 300}) => {    let [cTime, k] = [null, null]    // 获取当前时间    const getTime = () => new Date().getTime()    // 混合函数    const hyFn = () => {      const ags = argments      return () => {        clearTimeout(k)        k = cTime = null        fn(...ags)      }    }    return () => {      if (cTime == null) {        k = setTimeout(hyFn(...arguments), time)        cTime = getTime()      } else {        if ( getTime() - cTime < 0) {          // 清除之前的函数堆 ---- 重新记录          clearTimeout(k)          k = null          cTime = getTime()          k = setTimeout(hyFn(...arguments), time)        }      }}  }  export const contains = function(parentNode, childNode) {    if (parentNode.contains) {      return parentNode != childNode && parentNode.contains(childNode)    } else {      return !!(parentNode.compareDocumentPosition(childNode) & 16)    }  }  export const addClass = function (el, className) {    if (typeof el !== "object") {      console.log('el is not elem')      return null    }    let classList = el['className']    classList = classList === '' ? [] : classList.split(//s+/)    if (classList.indexOf(className) === -1) {      classList.push(className)      el.className = classList.join(' ')    } else {      console.warn('warn className current')    }  }  export const removeClass = function (el, className) {    let classList = el['className']    classList = classList === '' ? [] : classList.split(//s+/)    classList = classList.filter(item => {      return item !== className    })    el.className =   classList.join(' ')  }  export const delay = ({fn, time}) => {    let oT = null    let k = null    return () => {      // 当前时间      let cT = new Date().getTime()      const fixFn = () => {        k = oT = null        fn()      }      if (k === null) {        oT = cT        k = setTimeout(fixFn, time)        return      }      if (cT - oT < time) {        oT = cT        clearTimeout(k)        k = setTimeout(fixFn, time)      }        }  }  export const Event = function () {    // 类型    this.typeList = {}  }  Event.prototype.on = function ({type, fn}){    if (this.typeList.hasOwnProperty(type)) {      this.typeList[type].push(fn)    } else {      this.typeList[type] = []      this.typeList[type].push(fn)    }  }  Event.prototype.off = function({type, fn}) {    if (this.typeList.hasOwnProperty(type)) {       let list = this.typeList[type]     let index = list.indexOf(fn)     if (index !== -1 ) {         list.splice(index, 1)     }         } else {      console.warn('not has this type')    }  }  Event.prototype.once = function ({type, fn}) {    const fixFn = () => {      fn()      this.off({type, fn: fixFn})    }    this.on({type, fn: fixFn})  }  Event.prototype.trigger = function (type){    if (this.typeList.hasOwnProperty(type)) {      this.typeList[type].forEach(fn => {        fn()      })    }  }

组件模板

<template>  <div class="jc-clip-image" :style="{width: `${clip.width}`}">    <canvas ref="ctx"        :width="clip.width"        :height="clip.height"        @mousedown="handleClip($event)"    >    </canvas>    <input type="file" ref="file" @change="readFileMsg($event)">    <div class="clip-scale-btn">      <a class="add" @click="handleScale(false)">+</a>      <a @click="rotate" class="right-rotate">转</a>      <a class="poor" @click="handleScale(true)">-</a>      <span>{{scale}}</span>    </div>    <div class="upload-warp">      <a class="upload-btn" @click="dispatchUpload($event)">upload</a>      <a class="upload-cancel">cancel</a>    </div>    <div class="create-canvas">      <a class="to-send-file" @click="outFile" title="请打开控制台">生成文件</a>    </div>  </div></template><script>  import {on, off, once} from '../../utils/dom'  export default {    ctx: null,     file: null,     x: 0, // 点击canvas x 鼠标地址    y: 0,// 点击canvas y 鼠标地址    xV: 0, // 鼠标移动 x距离    yV: 0, // 鼠标移动 y距离    nX: 0, // 原始坐标点 图像 x    nY: 0,// 原始坐标点 图像 y    img: null,    props: {        src: {          type: String,        default: null      },      clip: {          type: Object,        default () {         return {width: '200px', height: '200px'}        }      }    },    data () {      return {        isShow: false,      base64: null,      scale: 1.5, //放大比例      deg: 0 //旋转角度    }    },    computed: {      width () {       const {clip} = this     return parseFloat(clip.width.replace('px', ''))    },    height () {     const {clip} = this     return parseFloat(clip.height.replace('px', ''))    }    },    mounted () {       const {$options, $refs, width, height} = this       // 初始化 canvas file nX nY      Object.assign($options, {        ctx: $refs.ctx.getContext('2d'),        file: $refs.file,        nX: -width / 2,        nY: -height / 2      })    },    methods: {    // 旋转操作      rotate () {        const {$options, draw} = this        this.deg = (this.deg + Math.PI /2)% (Math.PI * 2)        draw($options.img, $options.nX + $options.xV, $options.nY + $options.yV, this.scale, this.deg)      },      // 处理放大        handleScale (flag) {        const {$options, draw, deg} = this        flag && this.scale > 0.1 && (this.scale = this.scale - 0.1)        !flag && this.scale < 1.9 && (this.scale = this.scale + 0.1)        $options.img && draw($options.img, $options.nX + $options.xV, $options.nY + $options.yV, this.scale, deg)      },      // 模拟file 点击事件      dispatchUpload (e) {        this.clearState()        const {file} = this.$options        e.preventDefault()        file.click()      },      // 读取 input file 信息      readFileMsg () {        const {file} = this.$options        const {draw, createImage, $options: {nX, nY}, scale, deg} = this        const wFile = file.files[0]        const reader = new FileReader()        reader.onload = (e) => {          const img = createImage(e.target.result, (img) => {            draw(img, nX, nY, scale, deg)          })          file.value = null        }        reader.readAsDataURL(wFile)      },      // 生成 图像      createImage (src, cb) {       const img = new Image()        this.$el.append(img)        img.className = 'base64-hidden'        img.onload = () => {         cb(img)        }       img.src = src       this.$options.img = img      },      // 操作画布画图      draw (img, x = 0, y = 0, scale = 0.5,deg = Math.PI ) {        const {ctx} = this.$options        let {width, height} = this        // 图片尺寸        let imgW = img.offsetWidth        let imgH = img.offsetHeight        ctx.save()        ctx.clearRect( 0, 0, width, height)        ctx.translate( width / 2, height / 2, img)        ctx.rotate(deg)        ctx.drawImage(img, x, y, imgW * scale, imgH * scale)        ctx.restore()      },      // ... 事件绑定      handleClip (e) {        const {handleMove, $options, deg} = this        if (!$options.img) {            return        }        Object.assign(this.$options, {          x: e.screenX,         y: e.screenY        })        on({          el: window,          type: 'mousemove',          fn: handleMove        })        once({          el: window,          type: 'mouseup',          fn: (e) =>{            console.log('down')           switch (deg) {              case 0: {                Object.assign($options, {                  nX: $options.nX + $options.xV,                  nY: $options.nY + $options.yV,                  xV: 0,                  yV: 0                })                break;              }              case Math.PI / 2: {                Object.assign($options, {                  nX: $options.nY + $options.yV,                  nY: $options.nX - $options.xV,                  xV: 0,                  yV: 0                })                break;              }              case Math.PI: {                Object.assign($options, {                  nX: $options.nX - $options.xV,                  nY: $options.nY - $options.yV,                  xV: 0,                  yV: 0                })                break;              }              default: {                // $options.nY - $options.yV, $options.nX + $options.xV                Object.assign($options, {                  nX: $options.nY - $options.yV,                  nY: $options.nX + $options.xV,                  xV: 0,                  yV: 0                })              }            }          off({            el: window,            type: 'mousemove',            fn: handleMove          })          }        })      },      // ... 处理鼠标移动      handleMove (e){        e.preventDefault()        e.stopPropagation()        const {$options, draw, scale, deg} = this        Object.assign($options, {          xV: e.screenX - $options.x,          yV: e.screenY - $options.y        })        switch (deg) {          case 0: {            draw($options.img, $options.nX + $options.xV, $options.nY + $options.yV, scale, deg)            break;          }          case Math.PI / 2: {            draw($options.img, $options.nY + $options.yV, $options.nX - $options.xV, scale, deg)            break;          }          case Math.PI: {            draw($options.img, $options.nX - $options.xV, $options.nY - $options.yV, scale, deg)            break;          }          default: {            draw($options.img, $options.nY - $options.yV, $options.nX + $options.xV, scale, deg)            break;          }        }      },      // 清除状态      clearState () {      const {$options, width, height} = this        if ($options.img) {        this.$el.removeChild($options.img)        Object.assign($options, {          x: 0,          y: 0,          xV: 0,          yV: 0,          nX: -width / 2,          nY: -height / 2,          img: null,        })      }      },      // 输出文件      outFile () {          const {$refs: {ctx}} = this        console.log(ctx.toDataURL())        ctx.toBlob((blob) => {console.log(blob)})      }    }  }</script><style>  @component-namespace jc {    @component clip-image{      position: relative;      width: 100%;      canvas {        position: relative;        width: 100%;        height: 100%;        cursor: pointer;        box-shadow: 0 0 3px #333;      }      input {        display: none;      }      .base64-hidden {        position: absolute;        top: 0;        left: 0;        display: block;        width: 100%;        height: auto;        z-index: -999;        opacity: 0;      }      .clip-scale-btn {        position: relative;      @utils-clearfix;       margin-bottom: 5px;        text-align: center;        a {          float: left;          width: 20px;          height: 20px;          border-radius: 50%;          color: #fff;          background: #49a9ee;          text-align: center;          cursor: pointer;        }       &>.poor, &>.right-rotate {        float: right;       }      &>span{      position: absolute;      z-index: -9;      top: 0;      left: 0;        display: block;        position: relative;        width: 100%;         text-align: center;        height: 20px;        line-height: 20px;      }      }      .upload-warp {      @utils-clearfix;      .upload-btn,.upload-cancel {          float: left;          display:inline-block;          width: 60px;          height: 25px;          line-height: 25px;          color: #fff;          border-radius: 5px;          background: #49a9ee;          box-shadow: 0 0 0 #333;          text-align: center;          top: 0;          left: 0;          right: 0;          bottom: 0;          margin: auto;          cursor: pointer;          margin-top: 5px;        }      .upload-cancel{        background: gray;        float: right;      }      }      .to-send-file {        margin-top: 5px;        display: block;        width: 50px;        height: 25px;        line-height: 25px;        color: #fff;        border-radius: 5px;        background: #49a9ee;        cursor: pointer;      }    }

项目代码:https://github.com/L6zt/vuesrr

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持武林网。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表