本篇文章主要介绍了vue实现裁切图片同时实现放大、缩小、旋转功能,分享给大家,具体如下:
实现效果:
-
裁切指定区域内的图片
旋转图片
放大图片
输出bolb 格式数据 提供给 formData 对象
效果图
大概原理:
利用h5 FileReader 对象, 获取 <input type=\”file\”/> “上传到浏览器的文件” ,文件形式 为base64形式, 把 base64 赋给canvas的上下文。
然后给canvas 元素上加入对(mousedown)监听事件。 当用户鼠标左键在canvas按下时:
-
挂载对 window 对象mousemove事件 —> 获取 鼠标移动x,y距离.从而操作 canvas里的图像的位置移动。
挂载对 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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
© 版权声明
THE END
暂无评论内容