一、效果预览
用户选择本地图片,上传后,从下方选择相框。并在相框中缩放调整图片。确认后即可生成照片,图片长按即可保存。
二、node.js部分
图片上传部分,使用express和multer。注意不要使用跳转方式,不然无法上传成功。
var express = require("express");
var multer = require("multer");
var compression = require('compression');
var storage = multer.diskStorage({
destination: function(req, file, cb) {
cb(null, './dist/uploads');
},
filename: function(req, file, cb) {
cb(null, `${file.originalname}`)
}
})
var upload = multer({ storage: storage });
var app = express();
app.use(express.static('./dist'));
app.post('/upimages', upload.array('imgfile', 40), function(req, res, next) {
var files = req.files;
if (!files[0]) {
res.send('error');
} else {
res.send('success');
}
});
app.use(compression());
app.use(function(req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
var server = app.listen(8054, function() {
console.log('server is running at port 8054...');
});
三、前端部分
首先是对图片的位置进行处理,让图片自适应相框的位置,部分参数需要在PS里手动测量像素数量。
上传图片后,使用PhotoClip对照片进行裁剪,还使用了图片缩放功能,相框选择使用Swiper,使用canvas生成图片,实现图片上传。
使用了较多js库,参见html页面。
//获取URL参数
function getQueryVariable(variable) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
if (pair[0] == variable) { return pair[1]; }
}
return (false);
}
var imgID = getQueryVariable("img");
if (imgID) {
$("title").html("我的专属纪念地图");
$(".page3").show();
var imgName = './uploads/' + imgID;
$('#daiyan_bg1').attr('src', imgName);
var tipTop = $(window).width() * 1866 / 1276 / 2 - 40;
$(".save_tip").show().css("top", tipTop);
$(".save_tip").css("opacity", '0.6');
setTimeout(function() {
$(".save_tip").hide();
}, 8000);
} else {
$("title").html("专属定制");
$(".page1").show();
}
var fontSizeStr = document.documentElement.style.fontSize;
var fontSize = parseFloat(fontSizeStr.substring(0, fontSizeStr.length - 2));
//url转blob
function dataURLToBlob(dataurl) {
let arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], {
type: mime
});
}
$(document).ready(function() {
var clipImg, frameID = 0,
imgSrc;
var screenWidth = $(window).width();
var screenHeight = $(window).height();
var maskWidth = 1276;
var maskHeight = 1866;
var sizeRate = maskHeight / maskWidth;
var realBgWidth = screenWidth;
var realBgHeight = screenHeight - 4.2 * fontSize;
var isLong = (realBgHeight > realBgWidth * sizeRate);
var bgWidth = isLong ? realBgWidth : realBgHeight / sizeRate;
var bgHeight = isLong ? realBgWidth * sizeRate : realBgHeight;
var topMargin = (realBgHeight - bgHeight) / 2;
var leftMargin = (realBgWidth - bgWidth) / 2;
var frameHeight = realBgHeight.toString() + "px";
var canvasHeight = (screenWidth * sizeRate).toString() + "px";
var imgWidth = bgWidth.toString() + "px";
var imgHeight = bgHeight.toString() + "px";
var imageTop = topMargin.toString() + "px";
var imageLeft = leftMargin.toString() + "px";
$('.daiyan0').css("height", frameHeight);
$('.daiyan0').css("width", "100%");
$('.daiyan0 .qrcode').css("height", imgHeight);
$('.daiyan0 .qrcode').css("width", imgWidth);
$('.daiyan0 .qrcode').css("top", imageTop);
$('.daiyan0 .qrcode').css("left", imageLeft);
//边框距离
var sizeList = [{
margin: [370 / maskHeight, 110 / maskWidth, 1010 / maskHeight, 410 / maskWidth]
}, {
margin: [120 / maskHeight, 80 / maskWidth, 1180 / maskHeight, 630 / maskWidth]
}, {
margin: [280 / maskHeight, 215 / maskWidth, 1080 / maskHeight, 215 / maskWidth]
}];
var pcWidth = function(i) {
return bgWidth * (1 - sizeList[i].margin[1] - sizeList[i].margin[3]);
};
var pcHeight = function(i) {
return bgHeight * (1 - sizeList[i].margin[0] - sizeList[i].margin[2]);
};
var frameLeft = function(i) {
return maskWidth * sizeList[i].margin[3];
};
var frameTop = function(i) {
return maskHeight * sizeList[i].margin[0];
};
var photoWidth = function(i) {
return maskWidth * (1 - sizeList[i].margin[1] - sizeList[i].margin[3]);
};
var photoHeight = function(i) {
return maskHeight * (1 - sizeList[i].margin[0] - sizeList[i].margin[2]);
};
var clipWidth = function(i) {
return pcWidth(i).toString() + "px";
};
var clipHeight = function(i) {
return pcHeight(i).toString() + "px";
};
var clipMargin = function(i) {
return (bgHeight * sizeList[i].margin[0] + topMargin).toString() + "px " + (bgWidth * sizeList[i].margin[1] + leftMargin).toString() + "px " + (bgHeight * sizeList[i].margin[2] + topMargin).toString() + "px " + (bgWidth * sizeList[i].margin[3] + leftMargin).toString() + "px";
};
$('.daiyan1').css("height", clipHeight(0));
$('.daiyan1').css("width", clipWidth(0));
$('.daiyan1').css("margin", clipMargin(0));
$('#daiyan_bg1').css("height", canvasHeight);
//裁剪上传的照片
var pc = new PhotoClip('.page2 .daiyan1', {
size: [pcWidth(0), pcHeight(0)],
file: '.uploadfile',
outputQuality: 1,
maxZoom: 2,
ok: '.save',
loadStart: function() {
console.log('开始读取照片');
},
loadComplete: function() {
console.log('照片读取完成');
$(".page1").hide().siblings(".page2").show();
$('.photo-clip-mask').css("display", 'none');
var swiperCount = (screenWidth / (fontSize * 2.5 / sizeRate)) - 0.2;
var swiper = new Swiper('.swiper-container', {
slidesPerView: 3,
spaceBetween: 10,
on: {
},
});
swiper.slides.each(function(index, val) {
var ele = $(this);
ele.addClass("opacity");
ele.on("click", function() {
var index = $(this).index();
imgSrc = './images/frame_' + (index + 1).toString() + '.png';
$('.qrcode').attr('src', imgSrc);
$('.swiper-slide').addClass("opacity");
$(this).removeClass("opacity");
$('.daiyan1').css("height", clipHeight(index));
$('.daiyan1').css("width", clipWidth(index));
$('.daiyan1').css("margin", clipMargin(index));
pc.size(pcWidth(index), pcHeight(index));
frameID = index;
});
});
$('.swiper-slide-active').removeClass("opacity");
},
done: function(dataURL) {
clipImg = dataURL;
},
fail: function(msg) {
alert(msg);
}
});
// 加载的图片必须要与本程序同源,否则无法截图
$(".prev").click(function() {
window.location.href = "?"; //"?random=" + Date.parse(new Date());
formatTransform(0, 0);
});
$(".share").click(function() {
$(".alert").show();
$(".save_tip").hide();
});
$(".alert").click(function() {
$(".alert").hide();
});
function formatTransform(offx, offy) {
var translate = 'translate3d(' + (offx + 'px,') + (offy + 'px,') + '0)';
return translate;
}
$(".save").click(function() {
var tipTop = screenWidth * sizeRate / 2;
$(".load_tip").show().css("top", tipTop);
$(".load_tip").show();
var imgbox = document.getElementById("daiyan_bg"),
canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
canvas.width = maskWidth;
canvas.height = maskHeight;
var imgUrl = new Image,
qrCodeUrl = new Image;
qrCodeUrl.src = './images/frame_' + (frameID + 1).toString() + '.png';
qrCodeUrl.onload = function() {
imgUrl.crossOrigin = "anonymous";
imgUrl.src = clipImg;
};
imgUrl.onload = function() {
$(".load_tip").hide();
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, maskWidth, maskHeight);
ctx.beginPath();
ctx.drawImage(imgUrl, 0, 0, this.width, this.height, frameLeft(frameID), frameTop(frameID), photoWidth(frameID), photoHeight(frameID));
ctx.drawImage(qrCodeUrl, 0, 0, maskWidth, maskHeight, 0, 0, maskWidth, maskHeight);
var xhr = new XMLHttpRequest();
var formdata = new FormData();
var dataurl = canvas.toDataURL('image/jpeg');
var blobImg = new Blob();
blobImg = dataURLToBlob(dataurl);
var imgName = Date.now() + '.jpg';
var progress = document.querySelector('progress');
formdata.append('imgfile', blobImg, imgName);
xhr.open('POST', '/upimages');
xhr.onload = () => {
console.log(xhr);
if (xhr.status === 200 && xhr.responseText === 'success') {
xhr = null;
console.log('图片上传成功!');
window.location.href = "?img=" + imgName;
}
}
xhr.send(formdata);
};
});
});
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="format-detection" content="telephone=no">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<title></title>
<script src="js/jquery-3.5.1.min.js"></script>
<script>
//计算根节点HTML的字体大小
function resizeRoot(width) {
var deviceWidth = document.documentElement.clientWidth,
num = width,
num1 = num / 100;
if (deviceWidth > num) {
deviceWidth = num;
}
document.documentElement.style.fontSize = deviceWidth / num1 + "px";
}
//根节点HTML的字体大小初始化
resizeRoot(750);
window.onresize = function() {
resizeRoot(750);
};
</script>
<link rel="stylesheet" href="css/swiper-bundle.min.css">
<link rel="stylesheet" href="css/index.css">
</head>
<body>
<section class="page1">
<div class="daiyan">
<p class="play"><input type="file" accept="image/*" class="uploadfile"></p>
</div>
</section>
<section class="page2">
<div class="daiyan daiyan0"><img id="qrcode" src="images/frame_1.png" alt="" class="qrcode"></div>
<div class="daiyan daiyan1"></div>
<div class="swiper-container">
<div class="swiper-wrapper">
<div class="swiper-slide"><img src="images/frame_1.png"></div>
<div class="swiper-slide"><img src="images/frame_2.png"></div>
<div class="swiper-slide"><img src="images/frame_3.png"></div>
</div>
</div>
<img src="images/load.png" alt="" class="load_tip">
<div class="daiyan_ctrl"><button class="prev">重新选择</button><button class="save">点击生成</button></div>
</section>
<section class="page3">
<div class="daiyan daiyan2">
<img id="daiyan_bg1" src="" alt="" class="daiyan_bg" style="width: 100%;opacity: 1;">
</div>
<img src="images/save.png" alt="" class="save_tip">
<div class="daiyan_ctrl"><button class="prev">我也试试</button><button class="share">点击分享</button></div>
</section><canvas id="myCanvas" style="display:none">您的浏览器不支持canvas</canvas>
<div id="alert" class="alert">
<div id="alert_text" class="alert_text">点击右上角,分享到朋友圈</div>
</div>
<form action="/upimages" method="post" enctype="multipart/form-data" style="display: none;">
<input type="file" name="imgfile">
</form>
<script src="js/zepto.min.js"></script>
<script src="js/touch-0.2.14.min.js"></script>
<script src="js/exif.js"></script>
<script src="js/hammer.min.js"></script>
<script src="js/iscroll-zoom-min.js"></script>
<script src="js/lrz.all.bundle.js"></script>
<script src="js/PhotoClip.js"></script>
<script src="js/swiper-bundle.min.js"></script>
<script src="js/index.js"></script>
</body>
</html>
生成的图片保存在服务器上,用时间戳作为文件名,并返回图片url,供用户下载。