两年前的项目了,今天翻出来看一下。主要还是从需求分析和设计实现两大块进行介绍。
当时做这个项目的时候,公司正想入局微信小程序,而我此时微信小程序也已经学了半年了,正好派上了用场。由于微信小程序的“用完即走”的产品理念,加上当时AR/VR正火,老师(新中软研究中心)想着AR/VR的场景特别适合微信小程序的这个产品理念,想着将它们两个结合起来设计一款基于微信小程序和AR的产品。这个一旦实现,一来可以作为公司的技术储备,二来也可以在实践中提升我们的项目能力。
产品简要描述
经过讨论决定做一款具备教育和娱乐功能的微信小程序,主要面向的是3~12岁的儿童。用户通过该微信小程序进行拍照/选择相机照片,然后用户可以直接使用AR画笔在所拍人像或物品上面进行二次创作,作品可通过微信小程序进行分享。当然用户也可以不进行拍照,使用我们的AR画笔功能直接在摄像头上面进行绘画,以实现增强现实的效果。
场景描述
刚开始的需求场景大概是这样的:用户打开微信—>进入ARpainting微信小程序—>用户进入首页—>首页中页面以列表方式呈现,底栏是类似于微信底栏的可以进行直接切换—>用户选择拍照功能—>用户进行拍照—>用户可直接在上面使用AR画笔涂鸦—>用户可以实现保存、分享等功能。
然后需求又增加了用户可以直接通过微信小程序打开摄像头—>用户可以直接在摄像头上面使用画笔进行涂鸦。
然后又增加了用户可以通过扫描二维码,微信小程序在摄像头上实现显示设置的标签,以达到增强现实的效果。然后用户点击相应标签,进入相应的VR页面(比如说点的是我们学院教学楼的标签显示的便是我们教学楼的全景图片),然后用户可以查看是否有优惠券等功能。
后面又为了增加产品的趣味性,结合腾讯优图的人脸融合API,添加了基于微信小程序的人脸融合功能。
bgtuya | 用户可以进行直接在摄像头页面涂鸦 |
---|---|
export_img | 导入图片模块 |
first | 首页 |
logs | 日志模块 |
new_board | 画板模块 |
my_info | 个人信息模块 |
preview | 预览模块 |
tuya | 涂鸦模块 |
resource | 所用资源模块 |
Weixin | 微信卡券开发模块 |
开发 | 具体实现所用技术/工具 |
---|---|
微信小程序端所用框架 | 微信小程序原生框架MINA |
开发语言(微信小程序端) | JavaScript、wxml、wxss |
数据封装格式 | json文件格式 |
后端所用框架 | springboot |
项目管理工具 | maven |
数据库(主要是微信卡券实现) | mysql |
服务端开发所用语言 | java |
开发工具 | IDEA、微信开发者工具 |
开发主要参考 | 微信小程序开发者文档 |
其他 | 墨刀、processOn、Ali矢量图库 |
以下仅列出重要模块实现。
此模块为核心功能模块,难点主要是画图功能的实现,以及如何呈现出AR画布的效果,需要灵活运用微信官方给出的组件。
/***
AR绘图功能实现
*/
//画图事件
pointData:{
begin_x:0,
begin_y:0,
end_x:null,
end_y:null,
},
//开始绘画
start: function (e) {
var that = this;
that.setData({
showBgSet: false,
showPenSet: false,
});
that.pointData.begin_x = e.touches[0].x;
that.pointData.begin_y = e.touches[0].y;
if (that.data.isClear) { //判断是否启用的橡皮擦功能 ture表示清除 false表示画画
ctx.setStrokeStyle(that.data.canvasBgData.canvasBgColor); //设置线条样式 此处设置为画布的背景颜色 橡皮擦原理就是:利用擦过的地方被填充为画布的背景颜色一致 从而达到橡皮擦的效果
ctx.setLineCap('round'); //设置线条端点的样式
ctx.setLineJoin('round'); //设置两线相交处的样式
ctx.setLineWidth(20); //设置线条宽度
ctx.save(); //保存当前坐标轴的缩放、旋转、平移信息
ctx.beginPath(); //开始一个路径
ctx.arc(that.pointData.begin_x, that.pointData.begin_y, 5, 0, 2 * Math.PI, true); //添加一个弧形路径到当前路径,顺时针绘制 这里总共画了360度 也就是一个圆形
ctx.fill(); //对当前路径进行填充
ctx.restore(); //恢复之前保存过的坐标轴的缩放、旋转、平移信息
} else {
ctx.setStrokeStyle(that.data.penData.color);
ctx.setLineWidth(that.data.penData.penSize);
ctx.setLineCap('round'); // 让线条圆润
ctx.beginPath();
}
ctx.moveTo(that.pointData.begin_x, that.pointData.begin_y);
ctx.lineTo(that.pointData.begin_x, that.pointData.begin_y);
ctx.stroke();
ctx.draw(true);
},
move: function (e) {
var that = this;
if (that.data.isClear) { //判断是否启用的橡皮擦功能 ture表示清除 false表示画画
ctx.save(); //保存当前坐标轴的缩放、旋转、平移信息
ctx.moveTo(that.pointData.begin_x, that.pointData.begin_y); //把路径移动到画布中的指定点,但不创建线条
ctx.lineTo(e.touches[0].x, e.touches[0].y); //添加一个新点,然后在画布中创建从该点到最后指定点的线条
ctx.stroke(); //对当前路径进行描边
ctx.restore() //恢复之前保存过的坐标轴的缩放、旋转、平移信息
} else {
ctx.moveTo(that.pointData.begin_x, that.pointData.begin_y); //把路径移动到画布中的指定点,但不创建线条
ctx.lineTo(e.touches[0].x, e.touches[0].y); //添加一个新点,然后在画布中创建从该点到最后指定点的线条
ctx.stroke(); //对当前路径进行描边
}
ctx.draw(true);
that.pointData.begin_x = e.touches[0].x;
that.pointData.begin_y = e.touches[0].y;
},
end: function (e) {
},
//以下为自定义点击事件
openBgSet:function(){
this.setData({
showBgSet:true,
isClear: false
})
},
setBgColor:function (e) {
var that = this;
wx.showModal({
title: '提示',
content: '设置背景将清空画布',
success: function (res) {
if (res.confirm) {
var color = e.target.dataset.color;
that.data.canvasBgData.canvasBgColor = color;
ctx.rect(0, 0, 1000, 2000);
ctx.setFillStyle(color);
ctx.fill();
ctx.draw();
}
that.setData({
showBgSet: false
})
}
})
},
openPenSet: function () {
this.setData({
showPenSet: true,
isClear: false
})
},
setPenSize:function(e){
var data = this.data.penData;
data.penSize = e.detail.value;
this.setData({
penData: data
})
},
setPenColor:function(e) {
this.data.penData.color = e.target.dataset.color;
this.setData({
showPenSet: false
})
},
openErraserSet: function () {
this.setData({
isClear: !this.data.isClear
})
},
clearAll: function () {
var that = this;
wx.showModal({
title: '提示',
content: '确认清除画板所有内容',
success: function (res) {
if (res.confirm) {
console.log('用户点击确定');
ctx.draw();
that.resetColor();
}
}
})
},
/**
* 保存自己所绘制的涂鸦
*/
下面是配置文件wxml部分关键代码
<camera class='myCamera' id='myCamera' device-position='back' flash='auto' >
<cover-view class="controls">
<cover-view class="play" bindtap="play">
<cover-image class="img" src="{{path}}" />
cover-view>
cover-view>
<canvas canvas-id="myCanvas" bindtouchstart="start" disable-scroll="true"
bindtouchmove="move" bindtouchend="end" class='canvas'>
canvas>
camera>
/**
* 人脸融合接口
* url:localhost:8080/face/facemerge/uploadFM
*/
@Slf4j
@Controller
@RequestMapping(value="/face_merge")
@Scope("prototype")
public class FaceMergeController extends FaceMergeBaseController{
/**
* 人脸融合
* @throws Exception
*/
@RequestMapping(value="/uploadFM",method= RequestMethod.POST)
/**
* 对用户上传的图片进行处理
*/
public void UploadBDANIMAL()throws Exception{
TAipPtu aipPtu = new TAipPtu(AIConstant.QQ_AI_APPID,
AIConstant.QQ_AI_APPKEY);
String model = request.getParameter("model");
log.info("model的值是===="+model);
String result = "";
MultipartHttpServletRequest mpRequest = (MultipartHttpServletRequest)this.request;
Iterator iterator = mpRequest.getFileNames();
log.info(iterator.toString());
MultipartFile file = null;
while (iterator.hasNext()) {
file = mpRequest.getFile((String)iterator.next());
/**
* 如果上传的file不为空且size不为0
* 进行人脸融合操作
*/
if ((file != null) && (file.getSize() != 0L)){
log.info("image不为空");
byte[] image = file.getBytes();
log.debug("image",image);
/**
* 人脸融合结果
*/
String apiPtuResult = aipPtu.faceMerge(image,Integer.parseInt(model));
log.info(apiPtuResult);
/**
* 将返回的数据转换为json格式进行返回
*/
PrintUtil.printJson(this.response, apiPtuResult);
} else {
log.error("请检查上传文件是否正确");
result = "{\"result\", \"FAIL\",\"msg\":\"服务器出现了一些问题\"}";
PrintUtil.printJson(this.response, result);
}
}
}
}
【1】微信小程序官方文档