项目简介
- 本组件是vue下的头像裁剪组件,可以直接拷贝代码使用,无需安装依赖
- 使用九宫格进行裁剪,自由选择裁剪区域。
- 实时预览裁剪后效果。
- 可以将裁剪好的图片,导出为封装好的file文件,直接上传到服务器。
- 导出图片链接,可以导出为图片链接,直接使用裁剪后的效果。
- 实现原理简单,纯CSS技术处理图片,几乎不需要用到canvas相关知识
面向人群
- 急于使用vue头像裁剪组件的同学。直接下载文件,拷贝代码即可运行。
- 喜欢看源码,希望了解组件背后原理的同学。
刚接触前端的同学也可以通过本文章养成看源码的习惯。打破对源码的恐惧,相信自己,其实看源码并没有想象中的那么困难
技术难点
头像裁剪效果需要解决这么几个问题
- 【遮罩镂空效果】进行裁剪时,是先将原图添加一层透明的白色遮罩。并在遮罩中显示一块清晰的区块,作为当前裁剪区。如何制作该裁剪区。
- 【裁剪框初始宽高】上传图片后,裁剪区将预设为最大裁剪范围。当上传的图片非正方形时,如何设置裁剪区的最大范围。
- 【实时预览】如何将待裁剪区显示在实时预览区内。
- 【移动范围约束】移动伸裁剪区时,如何约束裁剪区的位置,让其一直位于原图上。
- 【拖拽范围扩展位置计算】当对裁剪区进行拉伸时,由于需要保持正方形,拉伸一边,需要将其临边一并拉伸。如何保证拉伸后都不溢出。
实现思路
-
上传图片
- 通过临时生成的img标签获取图片的大小信息(getImgSize函数的具体实现请参考源码)
- 比较原图长和宽,以其长边作为标准进行图片的缩放显示,让整张图显示在待裁剪区域中
- 以图片缩放后的短边作为选择框的宽,使裁剪框初始显示为最大可选范围
- 从而解决【裁剪框初始宽高】问题
// 选择图片 fileChange(event) { const fileObj = event.target.files[0] const reader = new FileReader() reader.onload = () => { const { selectData, containerBoxData } = this this.imgURL = reader.result this.getImgSize(this.imgURL).then((result) => { // 获取图片的大小 if (result.width > result.height) { // 350为外盒子宽高,比较原图长和宽,以其长边作为标准进行图片的缩放显示 this.scaleRate = 350 / result.width // 获取并记录图片缩放比 containerBoxData.width = 350 containerBoxData.height = Math.floor(result.height * this.scaleRate) selectData.top = 0 selectData.left = (350 - containerBoxData.height) / 2 // 裁剪选择框居中显示 selectData.width = containerBoxData.height // 以图片缩放后的短边作为选择框的宽,使其显示为最大可选范围 } else { this.scaleRate = 350 / result.height containerBoxData.height = 350 containerBoxData.width = Math.floor(result.width * this.scaleRate) selectData.left = 0 selectData.top = (350 - containerBoxData.width) / 2 selectData.width = containerBoxData.width } this.setPreview() }) } reader.readAsDataURL(fileObj) },
-
遮罩镂空的效果实现思路如下
- 共通过三个元素重叠显示,原图img标签放在最底下,遮罩层.img-mask放在中间,裁剪区域.select-box放在最外层
- .img-mask是遮住了整个img标签的,只是裁剪区域.select-box显示的内容刚好与原图上的一致。实现看起来遮罩被镂空的效果
- 其实遮罩并没有被镂空,只是遮罩上又多了一层图案,刚刚与原位置相同
- 裁剪区域.select-box,显示的内容和位置,通过background相关属性,background-position 和 background-size在实现
- 即选取图片的特定区域作为裁剪区域.select-box的背景
<img ref="$img" :src="imgURL"/> <div class="img-mask"></div> <div class="select-box" v-if="imgURL" @mousedown="onMouseDown($event, 'move')" :style="{ top:`${selectData.top}px`, left:`${selectData.left}px`, width:`${selectData.width}px`, height:`${selectData.width}px`, 'background-image':`url(${imgURL})`, 'background-position':`${-selectData.left}px ${-selectData.top}px`, 'background-size':`${containerBoxData.width}px ${containerBoxData.height}px`, }"></div>
-
渲染预览效果
- 利用canvas的drawImage函数,将候选区域显示在预览区
- drawImage函数可以设置图形采取的范围和位置
- 并设置采取获得的图形,显示在canvas的那个地方,以多大的size显示
- 从而实现采取区,于预览区缩放的形式显示
- 实现【实时预览】效果
// 设置预览图 setPreview() { const { selectData, scaleRate } = this const $canvas = this.$refs.$canvas.getContext('2d') $canvas.clearRect(0, 0, 190, 190) // 清除canvas中的内容 $canvas.drawImage( // 将原图中,选定区域的图案通过canvas渲染出来 this.$refs.$img, // 图形元素 Math.floor(selectData.left / scaleRate), // 截取原图中的那个位置作为起始点,X轴方向上 Math.floor(selectData.top / scaleRate), // 截取原图中的那个位置作为起始点,Y轴方向上 selectData.width / scaleRate, // 截取原图中多大的范围,宽度 selectData.width / scaleRate, // 截取原图中多大的范围,高度 0, // 显示在canvas中的X坐标 0, // 显示在canvas中的Y坐标 190, // 将内容伸缩为多大的宽度显示 190, // 将内容伸缩为多大的高度显示 ) },
-
拉伸操作的监听
- 在裁剪区域中,使用ul,li标签充当各个操作点,及形成九宫格中的虚线功能
- 在每个操作点中,监听mousedown事件,记录即将移动的方向,表示接下来的mousemove事件中,是进行那个方向上的拉伸
- 在created钩子中,监听全局 mouseup 和 mousemove 事件。因为鼠标的离开和利用不一样在裁剪区域中,在其他地方触发也应该同样执行相关操作
- 在 mousemove 事件中区裁剪区移动操作「move」,裁剪区拉伸操作「stretch」。执行不同的函数
- 数据设置完成后,重新渲染预览区
- 实现【拖拽范围扩展位置计算】及【拖拽范围扩展约束】功能
<div class="select-box" v-if="imgURL" @mousedown="onMouseDown($event, 'move')"> <ul class="x-line"> <li @mousedown.stop="onMouseDown($event, 'stretch', 'top')"></li> <li></li> <li></li> <li @mousedown.stop="onMouseDown($event, 'stretch', 'bottom')"></li> </ul> <ul class="y-line"> <li @mousedown.stop="onMouseDown($event, 'stretch', 'left')"></li> <li></li> <li></li> <li @mousedown.stop="onMouseDown($event, 'stretch', 'right')"></li> </ul> <ul class="point"> <li @mousedown.stop="onMouseDown($event, 'stretch', 'top-left')"></li> <li @mousedown.stop="onMouseDown($event, 'stretch', 'top-right')"></li> <li @mousedown.stop="onMouseDown($event, 'stretch', 'bottom-left')"></li> <li @mousedown.stop="onMouseDown($event, 'stretch', 'bottom-right')"></li> </ul> <div class="cross"></div> </div>
onMouseMove(event) { const { selectData, containerBoxData } = this const { x, y } = selectData.originPoint const moveX = event.clientX - x // X轴移动的距离 const moveY = event.clientY - y // Y轴移动的距离 if (selectData.action === 'move') { // 移动选择框 this.doMove(selectData, containerBoxData, moveX, moveY) } else if (selectData.action === 'stretch') { // 拉伸选择框 this.doStretch(selectData, containerBoxData, moveX, moveY) } else { return } selectData.originPoint = { x: event.clientX > 0 ? event.clientX : 0, y: event.clientY > 0 ? event.clientY : 0, } this.setPreview() },
-
裁剪区移动
- 比较移动后上下左右各个边与原图可裁剪区域的位置关系
- 若超出范围,则设置为边界值
- 实现【移动范围约束】功能
// 鼠标移动 doMove(selectData, containerBoxData, moveX, moveY) { selectData.top += moveY selectData.left += moveX if (selectData.top < 0) { selectData.top = 0 } if (selectData.left < 0) { selectData.left = 0 } if (selectData.top + selectData.width > containerBoxData.height) { selectData.top = containerBoxData.height - selectData.width } if (selectData.left + selectData.width > containerBoxData.width) { selectData.left = containerBoxData.width - selectData.width } },
-
裁剪区拉伸
- 设置各个方位具体的拉伸操作
- 调用对应函数,先进行一次拉伸操作
- 拉伸完成后,比较上下左右,宽高的溢出情况,获得最大的溢出值
- 以最大溢出值进行反向操作,确保裁剪区一直在可选范围内
// 选择框拉伸 doStretch(selectData, containerBoxData, moveX, moveY) { const { minWidth } = this // 比较上下左右,宽高的溢出情况,返回最大的溢出值 function getOverflowLength() { const overflowLeft = selectData.left < 0 ? -selectData.left : 0 const overflowTop = selectData.top < 0 ? -selectData.top : 0 const overflowRight = selectData.left + selectData.width > containerBoxData.width ? selectData.left + selectData.width - containerBoxData.width : 0 const overflowBottom = selectData.top + selectData.width > containerBoxData.height ? selectData.top + selectData.width - containerBoxData.height : 0 const overflowWidth = selectData.width < minWidth ? minWidth - selectData.width : 0 return Math.max(overflowLeft, overflowTop, overflowRight, overflowBottom, overflowWidth) } // 向左拉伸 function doStretchLeft(action) { let space = moveX space = action === 'preDo' ? space : -space selectData.top += space / 2 selectData.left += space selectData.width -= space } function doStretchRight(action) { let space = moveX space = action === 'preDo' ? space : -space selectData.top -= space / 2 selectData.width += space } function doStretchTop(action) { let space = moveY space = action === 'preDo' ? space : -space selectData.top += space selectData.left += space / 2 selectData.width -= space } function doStretchBottom(action) { let space = moveY space = action === 'preDo' ? space : -space selectData.left -= space / 2 selectData.width += space } function doStretchTopLeft(action) { let space = Math.abs(moveX) > Math.abs(moveY) ? moveX : moveY space = action === 'preDo' ? space : -space selectData.top += space selectData.left += space selectData.width -= space } function doStretchTopRight(action) { let space = Math.abs(moveX) > Math.abs(moveY) ? moveX : -moveY space = action === 'preDo' ? space : -space selectData.top -= space selectData.width += space } function doStretchBottomLeft(action) { let space = Math.abs(moveX) > Math.abs(moveY) ? moveX : -moveY space = action === 'preDo' ? space : -space selectData.left += space selectData.width -= space } function doStretchBottomRight(action) { let space = Math.abs(moveX) > Math.abs(moveY) ? moveX : moveY space = action === 'preDo' ? space : -space selectData.width += space } const doStretchFun = { doStretchLeft, doStretchRight, doStretchTop, doStretchBottom, doStretchTopLeft, doStretchTopRight, doStretchBottomLeft, doStretchBottomRight, }[`doStretch${this.getWord(this.getCamelCase(selectData.direction))}`] // 进行拉伸操作 doStretchFun('preDo') let overflowLength = getOverflowLength() // 拉伸完成后,获取最大溢出值 if (overflowLength > 0) { // 以最大溢出值进行反向操作,确保裁剪区一直在可选范围内 doStretchFun('reset') } },
后续优化
- 【代码优化】可以看到上面的代码,有部分冗余,略显繁琐。后续将简化实现逻辑。
- 【支持矩形裁剪】目前九宫仅支持将图片裁剪为正方形,不能裁剪为矩形,该功能将在后续优化。
- 【原图的缩放】目前上传的裁剪目标图并不支持缩放,上传大图时,裁剪显得不那么灵活,这也将在后续优化
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。