vue實(shí)現(xiàn)裁切圖片同時(shí)實(shí)現(xiàn)放大、縮小、旋轉(zhuǎn)功能
本篇文章主要介紹了vue實(shí)現(xiàn)裁切圖片同時(shí)實(shí)現(xiàn)放大、縮小、旋轉(zhuǎn)功能,分享給大家,具體如下:
實(shí)現(xiàn)效果:
- 裁切指定區(qū)域內(nèi)的圖片
- 旋轉(zhuǎn)圖片
- 放大圖片
- 輸出bolb 格式數(shù)據(jù) 提供給 formData 對(duì)象
效果圖







大概原理:
利用h5 FileReader 對(duì)象, 獲取 <input type="file"/> “上傳到瀏覽器的文件” ,文件形式 為base64形式, 把 base64 賦給canvas的上下文。
然后給canvas 元素上加入對(duì)(mousedown)監(jiān)聽(tīng)事件。 當(dāng)用戶(hù)鼠標(biāo)左鍵在canvas按下時(shí):
- 掛載對(duì) window 對(duì)象mousemove事件 ---> 獲取 鼠標(biāo)移動(dòng)x,y距離.從而操作 canvas里的圖像的位置移動(dòng)。
- 掛載對(duì) window 對(duì)象mouseup 事件, 清除 mousemove事件的綁定。(同時(shí)該事件觸發(fā)后會(huì)被刪除)
剩下的 放大、縮小 、 旋轉(zhuǎn) 是對(duì) canvas 對(duì)象的操作/坐標(biāo)體系的操作。具體api詳見(jiàn)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})
}
// 最后一個(gè)
export const fbTwice = ({fn, time = 300}) => {
let [cTime, k] = [null, null]
// 獲取當(dāng)前時(shí)間
const getTime = () => new Date().getTime()
// 混合函數(shù)
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) {
// 清除之前的函數(shù)堆 ---- 重新記錄
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 () => {
// 當(dāng)前時(shí)間
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 () {
// 類(lèi)型
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">轉(zhuǎn)</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="請(qǐng)打開(kāi)控制臺(tái)">生成文件</a>
</div>
</div>
</template>
<script>
import {on, off, once} from '../../utils/dom'
export default {
ctx: null,
file: null,
x: 0, // 點(diǎn)擊canvas x 鼠標(biāo)地址
y: 0,// 點(diǎn)擊canvas y 鼠標(biāo)地址
xV: 0, // 鼠標(biāo)移動(dòng) x距離
yV: 0, // 鼠標(biāo)移動(dòng) y距離
nX: 0, // 原始坐標(biāo)點(diǎn) 圖像 x
nY: 0,// 原始坐標(biāo)點(diǎn) 圖像 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 //旋轉(zhuǎn)角度
}
},
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: {
// 旋轉(zhuǎn)操作
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 點(diǎn)擊事件
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
},
// 操作畫(huà)布畫(huà)圖
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
})
}
})
},
// ... 處理鼠標(biāo)移動(dòng)
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;
}
}
},
// 清除狀態(tài)
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;
}
}
項(xiàng)目代碼:https://github.com/L6zt/vuesrr
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- vue+element實(shí)現(xiàn)圖片上傳及裁剪功能
- vue實(shí)現(xiàn)移動(dòng)端圖片裁剪上傳功能
- vue 實(shí)現(xiàn)剪裁圖片并上傳服務(wù)器功能
- vue-cli結(jié)合Element-ui基于cropper.js封裝vue實(shí)現(xiàn)圖片裁剪組件功能
- Vue-cropper 圖片裁剪的基本原理及思路講解
- vue移動(dòng)端裁剪圖片結(jié)合插件Cropper的使用實(shí)例代碼
- 詳解vue項(xiàng)目中實(shí)現(xiàn)圖片裁剪功能
- 基于Vue的移動(dòng)端圖片裁剪組件功能
- cropper js基于vue的圖片裁剪上傳功能的實(shí)現(xiàn)代碼
- Vue實(shí)現(xiàn)裁切圖片功能
相關(guān)文章
Vue Element前端應(yīng)用開(kāi)發(fā)之組織機(jī)構(gòu)和角色管理
本篇文章繼續(xù)深化Vue Element權(quán)限管理模塊管理的內(nèi)容,介紹組織機(jī)構(gòu)和角色管理模塊的處理,使得我們了解界面組件化模塊的開(kāi)發(fā)思路和做法,提高我們界面設(shè)計(jì)的技巧,并減少代碼的復(fù)雜性,提高界面代碼的可讀性,同時(shí)也是利用組件的復(fù)用管理。2021-05-05
Vue2中使用axios的3種方法實(shí)例總結(jié)
axios從功能上來(lái)說(shuō)就是主要用于我們前端向后端發(fā)送請(qǐng)求,是基于http客戶(hù)端的promise,面向?yàn)g覽器和nodejs,下面這篇文章主要給大家介紹了關(guān)于Vue2中使用axios的3種方法,需要的朋友可以參考下2022-09-09
vue+springboot前后端分離實(shí)現(xiàn)單點(diǎn)登錄跨域問(wèn)題解決方法
這篇文章主要介紹了vue+springboot前后端分離實(shí)現(xiàn)單點(diǎn)登錄跨域問(wèn)題的解決方法,需要的朋友可以參考下2018-01-01
vue2中的keep-alive使用總結(jié)及注意事項(xiàng)
vue2.0提供了一個(gè)keep-alive組件用來(lái)緩存組件,避免多次加載相應(yīng)的組件,減少性能消耗。本文給大家介紹vue2中的keep-alive使用總結(jié)及注意事項(xiàng),需要的朋友參考下吧2017-12-12
vue 每次渲染完頁(yè)面后div的滾動(dòng)條保持在最底部的方法
下面小編就為大家分享一篇vue 每次渲染完頁(yè)面后div的滾動(dòng)條保持在最底部的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03
vue項(xiàng)目下載文件重命名監(jiān)測(cè)進(jìn)度demo
這篇文章主要為大家介紹了vue項(xiàng)目下載文件重命名監(jiān)測(cè)進(jìn)度demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
Vue對(duì)Element中的el-tag添加@click事件無(wú)效的解決
本文主要介紹了Vue對(duì)Element中的el-tag添加@click事件無(wú)效的解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05

