赞
踩
首先,我们需要两张画布,一个展示选取图片的压缩图,一个展示截取后的图片(样式部分不做讲解)。代码如下:
<template>
<div>
<div>
<canvas>
您的浏览器不支持canvas,请升级最新版本
</canvas>
<p class="img-scale">
图片压缩:
<span></span>
</p>
<div>
<button type="button">Min</button>
<button type="button">-</button>
<button type="button">+</button>
<button type="button">Max</button>
</div>
<div>
选择图片
<input type="file">
</div>
</div>
<div>
<canvas>
您的浏览器不支持canvas,请升级最新版本
</canvas>
<p>
截图长度:
<span></span>
</p>
<button type="button">上传头像</button>
</div>
</div>
</template>
如图所示为初始效果,我们需要先隐藏第二张画布,以及调整图片的按钮组:
利用v-if指令,当图片选取成功(hasImgSrc = true)后,显示第二个画布,以及按钮,代码如下:
<template>
...
<div v-if="hasImgSrc">
<button type="button">Min</button>
<button type="button">-</button>
<button type="button">+</button>
<button type="button">Max</button>
</div>
...
<div v-show="hasImgSrc">
<canvas>
您的浏览器不支持canvas,请升级最新版本
</canvas>
<p>
截图长度:
<span></span>
</p>
<button type="button">上传头像</button>
</div>
...
</template>
这里选用 v-if 是因为选取图片时,不会频繁的切换,而隐藏第二个画布用 v-show 是因为 v-if 为 false 的时候会将Dom节点从文档流移除,而画布需要在mounted的时候进行初始化(若不在此初始化也可使用 v-if)。
接下来,我们需要画出默认图片(如上图所示),代码如下:
data () {
return {
originalCanvas: null, // 原图 画布
originalCtx: null, // 原图 上下文
originalImg: null, // 原图片的对象
cutCanvas: null, // 显示裁剪后图片的画布
cutCtx: null, // 裁图 画布 上下文
cutImg: null, // 裁剪后 图片的对象
originalCanvasWidth: 0, // 原图 压缩后 宽度
originalCanvasHeight: 0, // 原图 压缩后 高度
scaleSize: 300, // 原图压缩的尺寸
cutSize: 200, // 裁剪 正方形的宽高
hasImgSrc: false, // 是否有可裁剪图片
}
},
mounted () {
// 初始化 原图的画布
let originalCanvas = this.originalCanvas = this.$refs.originalCanvas
let originalCtx = this.originalCtx = originalCanvas.getContext('2d')
// 初始化 裁图 画布
let cutCanvas = this.cutCanvas = this.$refs.cutCanvas
this.cutCtx = cutCanvas.getContext('2d')
// 设置画布宽高
originalCanvas.width = this.originalCanvasWidth = this.scaleSize
originalCanvas.height = this.originalCanvasHeight = this.scaleSize
cutCanvas.width = this.cutSize
cutCanvas.height = this.cutSize
// 绘制默认图片
if (originalCanvas.getContext) {
let originalImg = this.originalImg = new Image()
originalImg.src = require('../assets/images/headImg/defineImg.png')
originalImg.onload = () => {
originalCtx.drawImage(originalImg, originalCanvas.width / 2 - this.cutSize / 2, originalCanvas.height / 2 - this.cutSize / 2, this.cutSize, this.cutSize)
}
}
}
使用canvas绘图,需要在生命周期:mounted,挂载完成之后执行,这时候data跟Dom都可以获取到。利用ref注册引用信息,$refs获取canvas。originalCanvasWidth、originalCanvasHeight则用于显示图片压缩尺寸,以及后期的计算,cutSize则用于显示截取长度,以及后期的计算。结构代码修改如下:
...
<canvas ref="originalCanvas">
您的浏览器不支持canvas,请升级最新版本
</canvas>
<p>
图片压缩:
<span v-text="Math.round(originalCanvasWidth) + ' * ' + Math.round(originalCanvasHeight)"></span>
</p>
...
<canvas ref="cutCanvas">
您的浏览器不支持canvas,请升级最新版本
</canvas>
<p>
截图长度:
<span v-text="Math.round(cutSize) + ' * ' + Math.round(cutSize)"></span>
</p>
...
第一个画布初始化时获取canvas画布( this.$refs.originalCanvas )、绘图上下文,实例化( new Image() )一个image对象,引入需要绘制的图片,当图片加载完成时( onload ),利用 drawImage(img,x,y,width,height) 绘制出图片。第二个画布则先初始化,在选取图片时再进行绘制。绘制完成可看到如下效果。
接下来我们要完成选取图片后绘制图片、裁剪框以及在第二个画布上绘制截取的图片。首先,选取图片时,需要监听input的onchange事件,带入$event获取原始的Dom事件对象,结构代码修改如下:
<div>
选择图片
<input type="file" @change="chooseImg($event)">
</div>
在methods () {} 里添加事件,当选中文件时,获取该文件,当满足图片格式时执行绘图(在data中加入 imgFormat: [‘jpg’, ‘png’] // 图片上传格式),代码如下:
// 选择图片
chooseImg (e) {
// 选中文件择执行
if (e.target.files.length !== 0) {
// 获取文件
let file = e.target.files[0]
// 限定上传格式
let arr = this.imgFormat
let str = file.name.split('.').pop()
if (arr.indexOf(str) !== -1) {
... ...
... ...
} else {
alert('图片格式错误')
}
}
}
当格式选择正确后,需要将获取到的 file 转换成base64位进行绘制,在绘制中要先清除原先的画布(不清除画布绘图时将一层覆盖一层,当图片是png格式时会有影响),以及绘制图片。这时候需要在methods中添加三个方法,代码如下:
// 转换成dataUrl
changeDataURL (fileObj, callback) {
let file = new FileReader()
file.readAsDataURL(fileObj)
file.onload = (e) => { callback(e.target.result) }
},
// 清除画布
clearCanvas () {
// 绘制原图片
this.originalCtx.clearRect(0, 0, this.originalCanvasWidth, this.originalCanvasHeight)
},
// 绘制 压缩后的图片
drawOriginal (img, width, height) {
// 绘制 原图
this.originalCtx.drawImage(img, 0, 0, width, height)
// 缓存 截图对象
this.imageData = this.originalCtx.getImageData(this.centerX - this.cutSize / 2, this.centerY - this.cutSize / 2, this.cutSize, this.cutSize)
// 清除截图区域时使用
this.tempImageData = this.originalCtx.getImageData(this.centerX - this.cutSize / 2 - 1, this.centerY - this.cutSize / 2 - 1, this.cutSize + 2, this.cutSize + 2)
},
转换成 Base64 位时,利用 readAsDataURL() 方法,异步的读取本地文件file,并以 data:URL 字符串的形式表示。绘制图片时,利用 getImageDate() 复制画布上指定矩形的像素数据,通过 putImageDate() 将图像数据放回画布, imageData 为绘制第二个画布时使用,tempImageData 为移动裁剪框是清除原先框所在位置时使用(这里的图片压缩比例可调,所以没有将整张画布进行清除,只清除局部)。当图片绘制完成之后,将绘制裁剪框,在methods中添加新的方法:
methods () {
... ...
// 绘制截图框
drawCuttingFrame (ctx, x, y, l, d) {
ctx.beginPath()
// 右上框
ctx.moveTo(x - 1, y - l)
ctx.lineTo(x + 1, y - l)
ctx.moveTo(x + d, y - l)
ctx.lineTo(x + l, y - l)
ctx.lineTo(x + l, y - d)
// 右下框
ctx.moveTo(x + l, y - 1)
ctx.lineTo(x + l, y + 1)
ctx.moveTo(x + l, y + d)
ctx.lineTo(x + l, y + l)
ctx.lineTo(x + d, y + l)
// 左下框
ctx.moveTo(x - 1, y + l)
ctx.lineTo(x + 1, y + l)
ctx.moveTo(x - d, y + l)
ctx.lineTo(x - l, y + l)
ctx.lineTo(x - l, y + d)
// 左上框
ctx.moveTo(x - l, y - 1)
ctx.lineTo(x - l, y + 1)
ctx.moveTo(x - l, y - d)
ctx.lineTo(x - l, y - l)
ctx.lineTo(x - d, y - l)
ctx.strokeStyle = 'green'
ctx.stroke()
ctx.closePath()
}
... ...
}
x、y 为坐标点,l 为线段长度,d 为点到线的距离,效果如下:
接下来绘制第二个画布时,在mothods中,添加 drawCutImage() 方法,利用putImageData将图像数据放入画布:
methods () {
... ...
// 绘制 裁剪后的图片
drawCutImage (imageData) {
this.cutCtx.putImageData(imageData, 0, 0)
}
... ...
}
由于每次选取图片时,都要绘制选取的图片,裁剪框以及截图,所以将方法放在一起调用:
// 绘制 压缩后的图片
drawOriginal (img, width, height) {
... ...
// 绘制 截图框
this.drawCuttingFrame(this.originalCtx, this.centerX, this.centerY, this.cutSize / 2, 20)
// 绘制 裁剪图片
this.drawCutImage(this.imageData)
}`这里写代码片`
这时返回到我们选取图片的时候,当格式选取正确的时候,将图片的 dataUrl 赋值给第一个画布的 image 对象(originalImg),并读取图片的宽高(如果图片宽高相等,则绘制出300*300的图片,如果不相等则按一边为300,另一边动态计算出长度):
if (arr.indexOf(str) !== -1) {
// 转换成Base 64位
this.changeDataURL(file, (dataUrl) => {
this.originalImg.src = dataUrl
// 获取图片 宽高
let width = this.originalImg.width
let height = this.originalImg.height
})
}
在画布没发生变化时,清除画布区域,重置画布的宽高为默认的值,然后对比宽高,短的一边给默认值(300),长的一边等比计算:
this.changeDataURL(file, (dataUrl) => {
... ...
// 清除画布
this.clearCanvas()
// 重置画布的宽高
let originalCanvas = this.originalCanvas
originalCanvas.width = this.originalCanvasWidth = this.scaleSize
originalCanvas.height = this.originalCanvasHeight = this.scaleSize
// 设置画布压缩后的宽高
if (width > height) {
originalCanvas.width = this.originalCanvasWidth = width * this.scaleSize / height
} else if (width < height) {
originalCanvas.height = this.originalCanvasHeight = height * this.scaleSize / width
}
})
在这时需要给定绘制裁剪框的位置(默认原点:cenrterX、centerY),绘制图片,以及允许裁剪框的拖动事件(hasImgSrc = true):
this.changeDataURL(file, (dataUrl) => {
... ...
// 设置绘图原点
this.centerX = Math.round(originalCanvas.width / 2)
this.centerY = Math.round(originalCanvas.height / 2)
// 图片加载完成 绘制选中的图片
this.originalImg.onload = () => {
// 允许鼠标点击事件
this.hasImgSrc = true
// 绘制图片
this.drawOriginal(this.originalImg, originalCanvas.width, originalCanvas.height)
}
})
这时候data添加以下变量:
data () {
return {
... ...
centerX: 0, // 原图画布中心 X坐标
centerY: 0, // 原图画布中心 Y坐标
mouseX: 0, // 鼠标所在点 X坐标
mouseY: 0, // 鼠标所在点 Y坐标
differenceX: 0, // 原点X - 鼠标X点 差值
differenceY: 0, // 原点Y - 鼠标Y点 差值
mouseDown: false, // 鼠标是否点击
hasImgSrc: false, // 是否有可裁剪图片
imageDate: null, // 截图 对象
tempImageData: null, // 实时截图 缓存对象
imgFormat: ['jpg', 'png'] // 图片上传格式
}
}
绘制完成,效果如下:
裁剪框的拖动,需要onmousedown、onmousemove、onmouseup来共同完成,结构修改如下:
<canvas ref="originalCanvas" @mousedown.prevent="hasImgSrc ? pressDown(originalCtx, $event) : false" @mousemove="mouseDown ? moveMouse($event) : false" @mouseup="stopCut">
您的浏览器不支持canvas,请升级最新版本
</canvas>
当图片绘制完成,允许拖动事件载入(hasImgSrc = true),这时再methods中加入鼠标点击,鼠标移动,松开鼠标的事件:
// 鼠标点击事件
pressDown (ctx, e) {
// 鼠标 坐标
this.mouseX = e.offsetX
this.mouseY = e.offsetY
// 原点到鼠标之间的差值
this.differenceX = this.centerX - this.mouseX
this.differenceY = this.centerY - this.mouseY
// 截图框内才能拖动
if (Math.abs(this.differenceX) <= this.cutSize / 2 && Math.abs(this.differenceY) <= this.cutSize / 2) {
// 允许鼠标移动事件
this.mouseDown = true
}
}
当鼠标点击的区域在裁剪框内时可以进行拖动,只有当鼠标点击时才允许有鼠标移动事件,这时候 mouseDown = true 允许拖动, 在裁剪框移动时,需要先清除上个裁剪框,并利用 putImageData 将之前保存的变量tempImageData放入空白处,这里需注意,canvas绘制线段的时候会将线段平分,如果裁剪框的线段为1PX,则框内跟框外均分0.5PX,这时候清除的区域要大于整个绿色的框,代码如下:
// 鼠标移动事件
moveMouse (e) {
if (this.mouseDown) {
// 清空 上个路径的截图框 X、Y坐标 - 1 宽高 + 2 彻底清除整个截图框区域
this.originalCtx.clearRect(this.centerX - this.cutSize / 2 - 1, this.centerY - this.cutSize / 2 - 1, this.cutSize + 2, this.cutSize + 2)
this.originalCtx.putImageData(this.tempImageData, this.centerX - this.cutSize / 2 - 1, this.centerY - this.cutSize / 2 - 1)
}
}
移动裁剪框时,实时计算裁剪框原点坐标,并实时存储imageData(实时绘制第二张画布)、tempImageData(实时提供上个裁剪框图像)
// 鼠标移动事件
moveMouse (e) {
if (this.mouseDown) {
... ...
// 重新赋值 截图框原点位置
if (e.offsetX > this.cutSize / 2 - this.differenceX && e.offsetX + this.cutSize / 2 + this.differenceX < this.originalCanvas.width) {
// 可移动范围内赋值
this.centerX = e.offsetX + this.differenceX
} else {
// 不可移动范围内赋值
this.centerX = e.offsetX <= this.cutSize / 2 - this.differenceX ? this.cutSize / 2 : this.originalCanvas.width - this.cutSize / 2
}
if (e.offsetY > this.cutSize / 2 - this.differenceY && e.offsetY + this.cutSize / 2 + this.differenceY < this.originalCanvas.height) {
this.centerY = e.offsetY + this.differenceY
} else {
this.centerY = e.offsetY <= this.cutSize / 2 - this.differenceY ? this.cutSize / 2 : this.originalCanvas.height - this.cutSize / 2
}
// 缓存 截图对象
this.imageData = this.originalCtx.getImageData(this.centerX - this.cutSize / 2, this.centerY - this.cutSize / 2, this.cutSize, this.cutSize)
// 缓存 清除截图框的 绘图模块
this.tempImageData = this.originalCtx.getImageData(this.centerX - this.cutSize / 2 - 1, this.centerY - this.cutSize / 2 - 1, this.cutSize + 2, this.cutSize + 2)
// 重绘截图框
this.drawCuttingFrame(this.originalCtx, this.centerX, this.centerY, this.cutSize / 2, 20)
// 绘制 裁剪图片
this.drawCutImage(this.imageData)
}
}
松开鼠标后,则停止拖动事件:
// 松开鼠标点击
stopCut () {
// 停止鼠标拖动事件
this.mouseDown = false
}
最后添加四个调整图片的功能:等比减1、等比加1、等比最小、等比最大来绘制图片,结构修改如下:
<div v-if="hasImgSrc">
<button @click="chooseSize(cutSize)" type="button">Min</button>
<button @click="reduceSize" type="button">-</button>
<button @click="plusSize" type="button">+</button>
<button @click="chooseSize(scaleSize)" type="button">Max</button>
</div>
在methods中加入三个方法:
// 尺寸减1 重绘
reduceSize () {
// 不能小于裁剪大小
if (this.originalCanvasWidth > this.cutSize && this.originalCanvasHeight > this.cutSize) {
// 清除画布
this.clearCanvas()
// 固定短边,计算长边的值
if (this.originalCanvasWidth > this.originalCanvasHeight) {
this.originalCanvas.height = this.originalCanvasHeight -= 1
this.originalCanvas.width = this.originalCanvasWidth = this.originalCanvasHeight / (this.originalCanvasHeight + 1) * this.originalCanvasWidth
} else if (this.originalCanvasWidth < this.originalCanvasHeight) {
this.originalCanvas.width = this.originalCanvasWidth -= 1
this.originalCanvas.height = this.originalCanvasHeight = this.originalCanvasWidth / (this.originalCanvasWidth + 1) * this.originalCanvasHeight
} else {
this.originalCanvas.width = this.originalCanvasWidth -= 1
this.originalCanvas.height = this.originalCanvasHeight -= 1
}
// 设置绘图原点
this.centerX = Math.round(this.originalCanvas.width / 2)
this.centerY = Math.round(this.originalCanvas.height / 2)
// 绘制 原图 截图框
this.drawOriginal(this.originalImg, this.originalCanvasWidth, this.originalCanvasHeight)
}
},
// 尺寸加1 重绘
plusSize () {
if (!(this.originalCanvasWidth >= 300 && this.originalCanvasHeight >= 300)) {
// 清除画布
this.clearCanvas()
// 计算长边的值
if (this.originalCanvasWidth > this.originalCanvasHeight) {
this.originalCanvas.height = this.originalCanvasHeight += 1
this.originalCanvas.width = this.originalCanvasWidth = this.originalCanvasHeight / (this.originalCanvasHeight - 1) * this.originalCanvasWidth
} else if (this.originalCanvasWidth < this.originalCanvasHeight) {
this.originalCanvas.width = this.originalCanvasWidth += 1
this.originalCanvas.height = this.originalCanvasHeight = this.originalCanvasWidth / (this.originalCanvasWidth - 1) * this.originalCanvasHeight
} else {
this.originalCanvas.width = this.originalCanvasWidth += 1
this.originalCanvas.height = this.originalCanvasHeight += 1
}
// 设置绘图原点
this.centerX = Math.round(this.originalCanvas.width / 2)
this.centerY = Math.round(this.originalCanvas.height / 2)
// 绘制 原图 截图框
this.drawOriginal(this.originalImg, this.originalCanvasWidth, this.originalCanvasHeight)
}
},
// 选择最大 最小 缩放比例 重绘
chooseSize (size) {
// 清除画布
this.clearCanvas()
// 计算长边的值
let width = this.originalCanvasWidth
let height = this.originalCanvasHeight
let originalCanvas = this.originalCanvas
if (width > height) {
originalCanvas.width = this.originalCanvasWidth = size / height * width
originalCanvas.height = this.originalCanvasHeight = size
} else if (width < height) {
originalCanvas.height = this.originalCanvasHeight = size / width * height
originalCanvas.width = this.originalCanvasWidth = size
} else {
originalCanvas.width = this.originalCanvasWidth = size
originalCanvas.height = this.originalCanvasHeight = size
}
// 设置绘图原点,最好取整,有奇怪的bug
this.centerX = Math.round(originalCanvas.width / 2)
this.centerY = Math.round(originalCanvas.height / 2)
// 绘制 原图 截图框
this.drawOriginal(this.originalImg, originalCanvas.width, originalCanvas.height)
}
再给第二个画布一个上传的按钮,结构修改如下:
<button @click="submitImg" type="button">上传头像</button>
利用 toDataUrl() 以Base64位导出画布(可以再转换成file对象进行传值):
// 上传头像
submitImg () {
// 生成截图
let dataUrl = this.cutCanvas.toDataURL('image/png')
}
以上为全部的内容,有三点需要注意哦:
若有不足之处,欢迎指出,谢谢……
代码地址:https://github.com/amwydqqk/upload-head-image
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。