您的当前位置:首页正文

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

2020-11-27 来源:汇智旅游网

本篇文章主要介绍了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

显示全文