记录一下上周在工作中遇到的一个实用的canvas知识点。
周一的时候老大告诉我,系统需要这么一个功能:
用户在一张场景图中圈出自己需要的部分图片,然后再上传。
我本着能摸鱼就摸鱼的考虑,就开始反问,难道不能用户自己用截图工具截出来再上传吗?老大想了一会儿说:你做不了吗 ?这痛击灵魂的一句话,让我立马回答可以 。老大,微微一笑,好的,明天看效果。
我: 。。。。。(这该死的逞强)
先上最后实现的demo效果图和源码链接。
具体怎么做,我大概百度了一下 。其中有个回答就是使用canvas的api 。
使用context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height)将图片的部分片段绘制到canvas上,再通过canvas.toDataURL(type, encoderOptions)将绘制的片段转为base64进行显示。
实现思路:
首先显示一张需要裁剪的图片,当点击按钮的时候,把和图片大小一致的canvas显示出来,监听鼠标事件,记录裁剪开始点和结束点的位置。再点击按钮把裁剪区域转为base64显示在另一张图片上。 思路听起来很简单,但是这其中有很多的细节之处。先看看整个页面的结构:
dom结构
<template>
<div class="page">
<div class="operate-btn">
<a-button @click="handleBtnClick">{{screenshot?"就决定是你了":"开始截图"}}</a-button>
</div>
<div class="clip-area">
<canvas
v-show="screenshot"
class="clip-canvas"
@mousedown="handleDrawStart"
@mouseup="handleMouseUp"></canvas>
<img class="big-img" :style="bigImgStyle" src="https://juejin.cn/post/assets/duola.jpg" alt="小图">
<div class="draw-area" :style="drawAreaStyle" v-show="!screenshot"></div>
</div>
<div class="thumb">
<p style="text-align: center">截图预览</p>
<img class="thumb-img" :src="https://juejin.cn/post/clipBase64" v-show="clipBase64" alt="大图">
</div>
</div>
</template>
复制代码
css结构
html, body {
overflow: hidden;
}
.clip-area {
position: relative;
overflow: hidden;
width: 500px;
height: 500px;
margin: 10px auto;
display: flex;
justify-content: center;
align-items: center;
}
.operate-btn {
margin-top: 10px;
text-align: center;
}
.clip-canvas {
position: absolute;
top: 0;
bottom: 0;
cursor: crosshair;
}
.draw-area {
border: 2px solid #ff0000;
position: absolute;
z-index: 10;
}
.big-img {
}
.thumb-img {
height: 300px;
display: block;
margin: 10px auto;
border: 1px solid red;
}
复制代码
js部分大概梳理为以下三步:
- 步骤一:将图片居中显示并按照原比例显示在固定区域里
居中显示使用flex布局,主要记录js代码。
mounted() {
this.getDomELe();
// 监听图片的加载
this.bigImgEle = document.querySelector(".big-img");
this.bigImgEle.onload = () => {
this.getAreaSize();
this.getImgSize();
this.setImgContain();
};
},
复制代码
首先获取可以显示图片的固定区域的宽高(通过offsetWidth属性获取)
// 获取canvas绘制的尺寸,与外层div大小一致
getAreaSize() {
let areaEle = document.querySelector(".clip-area");
this.canvasElement.width = this.canvasWidth = areaEle.offsetWidth;
this.canvasElement.height = this.canvasHeight = areaEle.offsetHeight;
this.canvasContext.strokeStyle = "#ff0000";
this.canvasContext.lineWidth = 1;
},
复制代码
再获取图片的尺寸和比例
// 获取原图展示的尺寸
getImgSize() {
this.imgOriginalWidth = this.bigImgEle.width;
this.imgOriginalHeight = this.bigImgEle.height;
},
复制代码
保持图片比例进行显示并记录下图片距离
// 将图片尽可能地填满该区域
setImgContain() {
// 宽高比例
let ratio = this.imgOriginalWidth / this.imgOriginalHeight;
// 如果图片宽小于高,则以容器高为准,按比例还原宽度
if (this.imgOriginalWidth < this.imgOriginalHeight) {
this.imgDisplayHeight = this.canvasHeight;
this.imgDisplayWidth = ratio * this.imgDisplayHeight;
this.imgOffset = {
x: (this.canvasWidth - this.imgDisplayWidth) / 2,
y: 0,
};
} else {
this.imgDisplayWidth = this.canvasWidth;
this.imgDisplayHeight = this.imgDisplayWidth / ratio;
this.imgOffset = {
x: 0,
y: (this.canvasHeight - this.imgDisplayHeight) / 2,
};
}
},
复制代码
- 步骤二:canvas绘制矩形框
首先获取绘制开始点并监听鼠标移动
// 记录在canvas里的开始点-清空画布-监听鼠标移动
handleDrawStart(e) {
this.points.start = this.getRelativePosition(e);
document.addEventListener("mousemove", this.handleDrawMove);
},
getRelativePosition(e) {
let clientRect = this.canvasElement.getBoundingClientRect();
// 这里必须取整数
let x = Math.round(e.clientX - clientRect.x);
let y = Math.round(e.clientY - clientRect.y);
return [x, y];
},
复制代码
因为使用overflow:hidden,所以没有考虑滚动的高度,这里使用getBoundingClientRect函数来获取鼠标距离顶部的距离。但是如果非要考虑scroll的距离,需要参考jq的offset()方法。
鼠标移动过程实时绘制矩形框
// 记录移动的点位
handleDrawMove(e) {
this.points.end = this.getRelativePosition(e);
this.drawRect();
},
// 根据开始和结束点来绘制矩形
drawRect() {
let {
yStart,
yDistance,
xDistance,
xStart,
} = this.formatPointPosition;
// 清空画布
this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// 开始绘制
this.canvasContext.strokeRect(xStart, yStart, xDistance, yDistance);
},
复制代码
因为移动过程中,坐标可能比起始坐标大或小,所以这里用formatPointPosition 这个计算属性来获取绘制矩形框的起始坐标和宽高。
formatPointPosition() {
// 取开始点和结束点的最小值
let [x, y] = this.points.start;
let [x1, y1] = this.points.end;
let xStart = Math.min(x, x1);
let yStart = Math.min(y, y1);
// 计算移动距离
let xDistance = Math.abs(x - x1);
let yDistance = Math.abs(y - y1);
return {
yDistance,
xDistance,
yStart,
xStart,
};
},
复制代码
最后在鼠标抬起的时候取消mousemove事件的监听
handleMouseUp() {
document.removeEventListener("mousemove", this.handleDrawMove);
},
复制代码
- 步骤三:点击按钮时将canvas绘制的区域转为base64
这里计算框选区域相对于图片的定位比较繁琐,需要用到图片显示尺寸和原始尺寸的缩放比例、图片位置与canvas的偏移量。这就是为什么在mounted时进行了这些要素的计算。
handleBtnClick() {
this.screenshot = !this.screenshot;
if (!this.screenshot && this.formatPointPosition.xDistance && this.formatPointPosition.yDistance) {
this.exportBase64();
}
},
exportBase64() {
// 图片缩放比例
let scale = this.imgDisplayWidth / this.imgOriginalWidth;
// 创建一个新的canvas
let canvasElement = document.createElement("canvas");
let canvasContext = canvasElement.getContext("2d");
// 计算框选区域相对于图片的定位
let startX = (this.formatPointPosition.xStart - this.imgOffset.x) / scale;
let startY = (this.formatPointPosition.yStart - this.imgOffset.y) / scale;
canvasElement.width = this.formatPointPosition.xDistance / scale;
canvasElement.height = this.formatPointPosition.yDistance / scale;
// 框选区域绘制到canvas里
canvasContext.drawImage(this.bigImgEle,
startX, startY, canvasElement.width, canvasElement.height,
0, 0, canvasElement.width, canvasElement.height);
// 转为base64进行显示
this.clipBase64 = canvasElement.toDataURL("image/jpeg");
},
复制代码
总结
虽然剪裁图片只是一个小功能,但是涉及到的知识点还是很多,也学到了不少。现在的实现还需要完善:没有对框选图片之外的区域进行限制,图片框选时需要对图片进行放大。等有时间再一一完善吧。