分享一次使用 canvas 来做合成头像的经验
核心API
drawImage
canvas的 drawImage,可以直接将图片绘制到 canvas 内;所绘的图像叫做 "源图像"(source image)。而绘制到的地方叫做 "目标canvas"(destination canvas)。以字母 s 来开头的变量名代表源图像,以字母 d 开头的变量用于代指目标canvas。 drawImage 方法一共可以接受一下 3 套传参方式。
-
drawImage(image, dx, dy)该方法将源图像同比例的放在 canvas中,通过
dx, dy来控制,源图像在目标 canvas 中的位置 -
drawImage(image, dx, dy, dw, dh)该方法将源图像的宽高,修改为
dw, dh然后绘制到目标 canvas中,位置为dx,dy -
drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)该方法将源图像需要绘制的宽高
sw, sh,以及从源图像什么位置开始sx, sy,绘制到目标canvas 时的尺寸dw, dh,在目标canvas 的什么位置开始绘制dx, dy
drawImage 可以将一幅图,一个 canvas 对象或一个视频帧的整体或者某个部分绘制到canvas中。在使用时要注意一点,绘制的元素一定要加载完毕,否则在绘制的过程中会绘制出错且没有任何提示
var img = new Image();
img.src = '1.png';
canvas.drawImage(img, 0, 0); // 在此处绘制会因为图片资源还未加载完全而绘制失败
img.onload = () => {
canvas.drawImage(img, 0, 0); // 在元素的图片资源加载完毕后才能进行正确的绘制
}
toDataURL
将 canvas 转成图片,默认为 PNG 格式。默认为 96dpi
canvas.toDataURL(type, encoderOptions)
- type:图片格式,默认为
image/png, 可选image/jpeg,image/webp encoderOptions:在指定图片格式为image/jpeg或image/webp的情况下,可以从 0 到 1的区间内选择图片质量。如果超出取值范围,将会使用默认值0.92。其他参数会被忽略
准备工作,理清实现思路
合成头像,首先得具备一个底图模板,然后用户需要上传头像,用户的上传头像需要居中,然后实现移动图片,缩放图片等功能。最后一个点击合成头像时,需要将绘制出来的 canvas 转换成图片。
设置底图
底图可以随便找一张图,但作为合成头像的模板图还是具备一定要求的,需要旁边是有底色的,中间头像区域是透明的

底图的条件的是清晰,不能模糊,然后占满整个canvas,留出中间的区域作为用户上传头像的可视区域。canvas的大小一般是根据底图设置的,我这边的设置为 2600 * 2600
// 初始化
canvas.width = 2600;
canvas.height = 2600;
ctx = canvas.getContext('2d');
上传图片
上传图片一样用 input 标签来实现上传功能
<input type="file" accept="image/*" @change="imgUpload" />
imgUpload(event) {
var imgFile = event.target.files[0];
var reader = new FileReader();
// 解析图像后的回调
reader.onload = evt => {
var img = new Image();
img.src = evt.target.result;
img.onload = () => {
ctx.globalCompositeOperation = 'destination-over';
// 图片缩放倍数,因为我的底图是 2600 * 2600 的所以得需要大一点的放大倍数
targetImgScale = 5;
// 图片上传后的初始位置设置
targetImgX = (-img.width * targetImgScale) / 2 + 1300;
targetImgY = (-img.height * targetImgScale) / 2 + 1300;
ctx.drawImage(
img,
0,
0,
img.width,
img.height,
targetImgX,
targetImgY,
img.width * targetImgScale,
img.height * targetImgScale
);
};
};
reader.readAsDataURL(imgFile);
}
实现拖拽图片
实现思路:用 touchstart 记录手指按下的初始坐标值。用 touchmove 来记录移动时的变化,用 touchend 来销毁 touchmove && touchend 两个事件。
canvas.addEventListener('touchstart', handlerTouchstart); // 给canvas 添加手指按下监听事件
function handlerTouchstart(event) {
event.preventDefault();
moveIMG(event.touches[0]); // 正常移动是一个触碰点,所有采用 0 的位置
}
// 移动图片
function moveIMG(targetTouch) {
// 获取起始坐标
let top = this.$refs.canvas.offsetParent.offsetTop;
let startX = targetTouch.pageX - 10;
let startY = targetTouch.pageY - top - 10;
// 定义两个变量 作为记录移动时位置的变化
let nowX = 0;
let nowY = 0;
// 获取底图 移动时需要每次都清除画板,但底图位置是不变的
var baseImg = new Image();
baseImg.src = '1.png';
// 等待图片初始化完毕后
baseImg.onload = () => {
// 添加一个 touchmove 事件
canvas.ontouchmove = e => {
ctx.clearRect(0, 0, 2600, 2600); // 清除画布
// 获取原图的宽高 并将其绘制在目标 canvas 上
let baseWidth = baseImg.width;
let baseHeight = baseImg.height;
ctx.drawImage(baseImg, 0, 0, 2600, 2600);
// 得到移动时触碰点的坐标
let moveX = e.touches[0].pageX - 10;
let moveY = e.touches[0].pageY - top - 10;
let img = '2.png';
// 设置两张图片的叠加方式
ctx.globalCompositeOperation = 'destination-over';
// 当前的图像位置等于 初始化绘制时的偏移度 - 移动时的差值
nowX = targetImgX - (startX - moveX);
nowY = targetImgY - (startY - moveY);
ctx.drawImage(
img,
0,
0,
img.width,
img.height,
nowX,
nowY,
img.width * targetImgScale,
img.height * targetImgScale
);
};
canvas.ontouchend = () => {
// 当用户手指抬起时,将 当前的 偏移度 覆盖原有的偏移度,并且移除两个事件
targetImgX = nowX;
targetImgY = nowY;
canvas.ontouchmove = null;
canvas.ontouchend = null;
};
};
}
实现缩放图片
缩放思路:一个触碰点实现不了缩放,需要两个,当 touchstart 执行时,需要记录初始的坐标点,touchmove 执行时,需要将初始的和当前的进行计算得到一定的比例值,在乘以原有的比例,就是当前的缩放比例
canvas.addEventListener('touchstart', handlerTouchstart); // 给canvas 添加手指按下监听事件
function handlerTouchstart(event) {
event.preventDefault();
// 缩放需要两个触碰点,所以需要判断触碰点的个数
if (event.touches.length > 1) {
scaleImg(event.touches);
}
}
// 缩放图片
function scaleImg(targetTouch) {
// 存放初始坐标点
let preTouchesClientx1y1x2y2 = [];
let one = targetTouch[0];
let two = targetTouch[1];
preTouchesClientx1y1x2y2 = [one.clientX, one.clientY, two.clientX, two.clientY];
const distance = (x1, y1, x2, y2) => {
let a = x1 - x2;
let b = y1 - y2;
return Math.sqrt(a * a + b * b);
};
var baseImg = new Image();
baseImg.src = base.imgUrl;
let nowScale = this.targetImgScale;
baseImg.onload = () => {
// 记录老的图片宽高
let oldWidth = null;
let oldHeight = null;
canvas.ontouchmove = e => {
// 记录两个触碰点移动的位置 并 计算与起始点的比例
if (e.touches.length > 1) {
let ones = e.touches['0'];
let twos = e.touches['1'];
nowScale = (
distance(ones.clientX, ones.clientY, twos.clientX, twos.clientY) /
distance(...preTouchesClientx1y1x2y2)
).toFixed(2);
}
this.ctx.clearRect(0, 0, 2600, 2600);
let baseWidth = baseImg.width;
let baseHeight = baseImg.height;
ctx.drawImage(baseImg, 0, 0, 2600, 2600);
let img = '2.png';
if (!oldWidth) {
oldWidth = img.width * targetImgScale;
oldHeight = img.height * targetImgScale;
}
// 获取放大后的图片宽高
let width = img.width * nowScale * targetImgScale;
let height = img.height * nowScale * targetImgScale;
// 计算边距 old - new = 多出来的边距 / 2 = 定位需要移动的距离
// 这样就可以保证是根据图片中心来进行缩放
targetImgX = targetImgX + (oldWidth - width) / 2;
targetImgY = targetImgY + (oldHeight - height) / 2;
oldWidth = width;
oldHeight = height;
ctx.globalCompositeOperation = 'destination-over';
// 将当前的缩放比例 乘以原有的缩放比例
ctx.drawImage(
img,
0,
0,
img.width,
img.height,
targetImgX,
targetImgY,
width,
height
);
};
canvas.ontouchend = () => {
// 更新缩放值,并且移除事件
targetImgScale = nowScale * targetImgScale;
canvas.ontouchmove = null;
canvas.ontouchend = null;
};
};
}
合成头像
最简单的点击按钮将canvas 转换成图片, 在赋值给页面上准备好的 img 元素
let img = this.canvas.toDataURL('image/png');
this.$refs.img.src = img;
以上就是使用本次分享的全部内容,在实现的过程以及在书写该文章的过程也对其他内容进行了引用
核心API drawImage ---- HTML5 Canvas核心技术
缩放功能借鉴了该篇文章 ---- 一步步实现网页图片的手势拖拽与缩放