本文是一篇学习了 vue-cropper 组件之后的随笔,关于裁剪组件的一些理解,当我们需要实现一个裁剪组件时,我们需要做些什么,如果你正在使用或看 vue-cropper的源码,可搭配本文食用。
剪裁函数
如果我们想要实现一个裁剪组件,最核心的方法就是裁剪函数
裁剪函数需要用到 Html Canvas API
-
创建画布
const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); -
绘制图像
ctx.drawImage(img, dx, dy, width, height); -
输出裁剪图像
// 输出为 blob 格式 canvas.toBlob(blob=>{}, type, encoderOptions) // 输出为 dataUrl 格式 canvas.toDataURL(type, encoderOptions) -
调整画布
// 旋转 ctx.rotate() // 拉伸 ctx.translate() //缩放 ctx.scale() // ! save 只是保存的 CanvasRenderingContext2D 对象的状态以及对象的所有属性 ctx.save() / ctx.restore()
Crop 容器
当然裁剪组件不可能这么简单,我们还得需要一个容器,让使用者在里面进行图片的裁剪
- 在容器内放置用户上传的图片,给图片设置 scale tanslate rotate 属性
<div
class="cropper-box"
:style="{
transform: 'scale(' + scale + ',' + scale + ') ' + 'translate3d('+ x / scale + 'px,' + y / scale + 'px,' + '0)' + 'rotateZ('+ rotate * 90 +'deg)'
}"
>
<img :src="imgs" />
</div>
- 设置图片可拖拽区域,一般与容器大小相同,其中产生的x , y 就是图片在容器左上角移动的距离
<div
class="cropper-drag-box"
:class="{'cropper-move': canMove,'cropper-modal': cropping}"
@mousedown="startMove"
@touchstart="startMove"
></div>
剪裁框
我们还需要一个裁剪框容器,裁剪框内出现的就是裁剪出的内容
- 在裁剪框容器内同样放置用户上传的图片,给图片设置 scale tanslate rotate 属性
<div class="cropper-view-box">
<img
:src="imgs"
:style="{
'transform': 'scale(' + scale + ',' + scale + ') ' + 'translate3d('+ (x - cropOffsertX) / scale + 'px,' + (y - cropOffsertY) / scale + 'px,' + '0)'
+ 'rotateZ('+ rotate * 90 +'deg)'
}"
/>
</div>
这里的 x - cropOffsertX 与 y - cropOffsertY 就是裁剪框内图片相对于裁剪框左上角的位置
- 设置图片可拖拽区域,一般与容器大小相同,其中产生的changeX, changeY就是裁剪框容器距外层容器左上角移动的距离,注意裁剪框不要超出外层容器即可。
<span class="cropper-face cropper-move" @mousedown="cropMove" @touchstart="cropMove"></span>
-
这里之所以还要放置一个与外面相同的图片,是为了让图片进入裁剪框时高亮显示,我们需要明白两张图片其实在视觉上在同一位置
- 首先因为两张图片都是基于各自容器设置的绝对位置
position: absolute; top: 0; right: 0; bottom: 0; left: 0;- 所以当 translate 时,外层裁剪容器根据自身移动时,移动为 x 与 y 那么内层裁剪框的根据自身移动 translate 就是 x- x - cropOffsertX 与 y - cropOffsertY,cropOffsertX 与 cropOffsertY 为裁剪框距离外层裁剪容器的距离
-
还有一个逻辑较为复杂的地方是,裁剪框会被拉伸,随意改变大小
-
首先判断改变的是 X 还是 Y
-
在里层继续判断产生的 changeX 和 changeY 是正数还是负数,结合下层来判断裁剪框是增大还是减小
-
继续在第三内层判断 change 与 crop 的大小, 因为存在两极反转的情况
这里比较绕,但我们可以这么想,首先第一层我们需要知道此时拖拽改变的是 X 还是 Y 轴,其次在 X 轴上裁剪框(正方形)存在两条边,那么我们就要第二层来知道产生的 change (拖拽距离) 为正还是为负,来分别处理两条边的情况,比如左边那条边 change 为负裁剪框增加,但是右边那条边却是减小。
第三层 if 逻辑存在的理由是存在左边因为拖拽变成右边的情况(两极反转),在第三层里面,我们就去做 crop 大小和与外层容器距离的计算了(位置 + 大小 就能确定 crop 的情况了)
- 剪裁框拖拽核心代码如下
const changeW = nowX - this.cropX; const changeH = nowY - this.cropY; if (this.canChangeX) { if (this.changeCropTypeX === 1) { // 判断「两极反转」的情况 if (this.cropOldW - changeW > 0) { this.cropW = this.containerWidth - this.cropPositionX - changeW <= this.containerWidth ? this.cropOldW - changeW : this.cropOldW + this.cropPositionX; this.cropOffsertX = this.containerWidth - this.cropPositionX - changeW <= this.containerWidth ? this.cropPositionX + changeW : 0; } else { // 左坐标线与右坐标线两极反转 this.cropW = Math.abs(changeW) + this.cropPositionX <= this.containerWidth ? Math.abs(changeW) - this.cropOldW : this.containerWidth - this.cropW - this.cropPositionX; this.cropOffsertX = this.cropPositionX + this.cropOldW; } } else if (this.changeCropTypeX === 2) { if (this.cropOldW + changeW > 0) { this.cropW = this.cropOldW + changeW + this.cropPositionX <= this.containerWidth ? this.cropOldW + changeW : this.containerWidth - this.cropPositionX; this.cropOffsertX = this.cropPositionX; } else { // 左坐标线与右坐标线两极反转 this.cropW = Math.abs(changeW + this.cropOldW) <= this.cropPositionX ? Math.abs(changeW + this.cropOldW) : this.cropPositionX; this.cropOffsertX = Math.abs(changeW + this.cropOldW) <= this.cropPositionX ? this.cropPositionX - Math.abs(changeW + this.cropOldW) : 0; } } } if (this.canChangeY) { if (this.changeCropTypeY === 1) { // 判断「两极反转」 if (this.cropOldH - changeH > 0) { this.cropH = this.cropPositionY + changeH >= 0 ? this.cropOldH - changeH : this.cropPositionY + this.cropOldH; this.cropOffsertY = this.cropPositionY + changeH >= 0 ? this.cropPositionY + changeH : 0; } else { this.cropH = changeH - this.cropOldH <= this.containerHeight - this.cropPositionY - this.cropOldH ? changeH - this.cropOldH : this.containerHeight - this.cropPositionY - this.cropOldH; this.cropOffsertY = this.cropPositionY + this.cropOldH; } } else if (this.changeCropTypeY === 2) { if (changeH + this.cropOldH > 0) { this.cropH = changeH <= this.containerHeight - this.cropOldH - this.cropPositionY ? this.cropOldH + changeH : this.containerHeight - this.cropPositionY; this.cropOffsertY = this.cropPositionY; } else { this.cropH = Math.abs(changeH + this.cropOldH) <= this.cropPositionY ? Math.abs(changeH + this.cropOldH) : this.cropPositionY; this.cropOffsertY = Math.abs(changeH + this.cropOldH) <= this.cropPositionY ? this.cropPositionY - Math.abs(changeH + this.cropOldH) : 0; } } } -
总结
以上是我在使用和学习 vue-cropper 时的一些感悟,可能有些凌乱,但希望能在你使用或封装一个裁剪组件时能有所帮助。