前言
最近在为公司的一个比赛制作专题页,碰到一个使用参赛者上传的照片生成专属海报的需求,实现过程中用到了一些以前没用过的 api,也踩了一些坑,于是将其记录下来。
需求描述
用户点击按钮进行照片上传
照片上传完成后,将照片进行裁剪,并和海报背景、姓名等组合得到海报
将生成的海报上传
效果大概如下:
海报背景:
成品:
实现过程
1、初始化 canvas
canvas#poster-canvas(width='960' height='1280')
function initCanvas() {
canvasCtx = document.getElementById("poster-canvas").getContext('2d');
}
2、绘制海报背景
海报背景为预先提供的一张照片,将其设置到一个隐藏的 img
标签里面,并且预留一个 canvas
元素用于绘制海报:
img.poster-background(src='/assets/xxx/poster-background.jpeg')
页面加载完成后,将海报背景绘制到 canvas
内:
$('img.poster-background').on('load', function () {
var backgroundImg = $('img.poster-background')[0];
canvasCtx.drawImage(backgroundImg, 0, 0, 960, 1280);
renderName();
});
海报背景绘制完成之后,需要将用户姓名绘制到特定位置。由于用户姓名长度不一,因此需要进行计算确定字体大小:
function renderName() {
var name = $('input[name="chName"]').val();
var fontSize;
if (name.length < 3) {
fontSize = 100;
} else {
fontSize = parseInt(320 / name.length);
}
canvasCtx.font = "bold " + fontSize + "px Courier New";
canvasCtx.fillStyle = "#de071b";
canvasCtx.fillText(name, 20, 1066);
}
3、上传照片
使用 file
类型的 input
元素,因为页面上表现为点击按钮,因此使用经典的将 input
元素透明化并覆盖按钮的方法:
a.upload-btn
input#photo(type='file' name='photo' accept='image/jpeg, image/png')
| 上传自己的照片生成专属海报
.upload-btn input {
position: absolute;
left: 0;
top: 0;
opacity: 0;
width: 100%;
height: 68px;
cursor: pointer;
}
然后监听 input
元素的 change
事件,然后使用 FormData API
构造表单数据,使用 ajax
进行异步上传,照片上传完成之后。得到一个地址,将这个地址设置到页面上预留的一个 img
标签里面:
$('#photo').on('change', function (e) {
var file = e.target.files[0];
var type = file.type;
if (type !== 'image/jpeg' && type !== 'image/png') {
window.toastr.error('请上传 jpg 或 png 格式的图片');
} else {
var formData = new FormData();
formData.append('avatar', file);
$.ajax({
type: 'POST',
url: '/upload_url',
data: formData,
contentType: false,
processData: false,
success: function(result) {
var avatarUrl = result.data.url;
$('img.avatar').attr('src', avatarUrl);
},
error: function(err) {
}
});
}
});
4、绘制照片
海报中放置照片的区域为正方形,但是用户上传的照片却不一定,因此需要对照片进行裁剪,裁剪的原则为取照片中间部分。然后将裁剪参数传进 canvas
的 drawImage
方法,进行绘制:
$('img.avatar').on('load', function () {
var avatarImg = $('img.avatar')[0];
var originWidth = avatarImg.width;
var originHeight = avatarImg.height;
var newWidth, cutStartX, cutStartY;
if (originWidth < originHeight) {
newWidth = originWidth;
cutStartX = 0;
cutStartY = (originHeight - originWidth) / 2;
} else if (originWidth > originHeight) {
newWidth = originHeight;
cutStartX = (originWidth - originHeight) / 2;
cutStartY = 0;
} else {
newWidth = originWidth;
cutStartX = 0;
cutStartY = 0;
}
canvasCtx.drawImage(avatarImg, cutStartX, cutStartY, newWidth, newWidth, 0, 0, 960, 960);
uploadPoster();
});
前面绘制海报背景和这里绘制照片,调用的是同一个方法,只不过后者多传进去了裁剪参数。但是需要注意的是,裁剪参数是在绘制位置之前传进去的,而不是简单的补在后面:
canvasCtx.drawImage(backgroundImg, 0, 0, 960, 1280);
canvasCtx.drawImage(avatarImg, cutStartX, cutStartY, newWidth, newWidth, 0, 0, 960, 960);
5、上传海报
依然使用 FormData API
,因此需要先用 canvas
构造一个 Blob
对象。新版本的 Chrome 和 Firefox 支持 canvas
的 toBlob
方法,可以直接使用:
document.getElementById("poster-canvas").toBlob(function (blob) {});
其它浏览器里,可以先用 toDataURL
方法得到 base64
格式的图片数据,再转为 Blob
:
var blob = dataURLtoBlob(document.getElementById("poster-canvas").toDataURL());
function dataURLtoBlob(dataurl) {
if (dataurl.indexOf('base64') < 0) {
dataurl = 'data:image/jpeg;base64,' + dataurl;
}
var arr = dataurl.split(',');
var mime = arr[0].match(/:(.*?);/)[1];
var bstr = atob(arr[1]);
var n = bstr.length;
var u8arr = new Uint8Array(n);
while (n --) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], {type: mime});
}
然后进行上传,步骤和前面上传照片一致:
var formData = new FormData();
formData.append('poster', blob);
$.ajax({
type: 'POST',
url: '/upload_poster_url',
data: formdata,
contentType: false,
processData: false,
success: function(result) {
},
error: function(err) {
}
});
至此,整个流程完结。