效果图

uniapp下实现拼图效果
vue文件代码
<template>
<view class="page">
<view class="edit-canvas">
<canvas @touchend="touchend" @touchstart="touchstart" @touchmove="touchmove" class="edit-area" canvas-id="edit-area" :style="'width:'+ editCanvasWidth +'px;height:'+ editCanvasHeight +'px;'"></canvas>
</view>
<view class="btn" @click="generate">生成图片</view>
<view class="area">
<canvas @touchstart="compositTouchstart" class="composit-area" canvas-id="composit-area" :style="'width:'+ compositeWidth +'px;height:'+ compositeHeight +'px;'"></canvas>
</view>
</view>
</template>
<script>
import { Pannel,PicElement,Puzzle } from '@/utils/puzzle2.js'
export default {
data() {
return {
// 操作canvas的宽度
editCanvasWidth: uni.upx2px(750),
// 操作canvas的高度
editCanvasHeight: uni.upx2px(750),
ctx: null,
compositCtx: null,
pannel: null,
puzzle: null,
// 合成canvas的宽度
compositeWidth: uni.upx2px(300),
// 合成canvas的高度
compositeHeight: uni.upx2px(300),
}
},
async onReady() {
this.ctx = uni.createCanvasContext('edit-area');
this.compositCtx = uni.createCanvasContext('composit-area');
// 如果小程序显示不出来图片(后台图片下载域名没有配置或设置调试状态) 浏览器图片显示不出来(后台图片没用设置跨域下载)
let imageList = [
"https://yssc.oss-cn-beijing.aliyuncs.com/pictures/pre/products/e16cf7d3-87ed-4cc8-b6a5-c39c6ff74453_src=http___a0.att.hudong.com_30_29_01300000201438121627296084016.jpg&refer=http___a0.att.hudong.jpg",
"https://yssc.oss-cn-beijing.aliyuncs.com/pictures/pre/products/7e9fb1d2-61b6-4963-bb26-c4ca7ce73776_0ed8d202111081803517980.jpg",
"https://yssc.oss-cn-beijing.aliyuncs.com/pictures/pre/products/022fc46f-04db-4507-b155-84f0e6323c37_src=http___a0.att.hudong.com_52_62_31300542679117141195629117826.jpg&refer=http___a0.att.hudong.jpg",
];
// 初始化拼图类
this.puzzle = new Puzzle(this.compositeWidth, this.compositeHeight, this.compositCtx, this.ctx);
await this.puzzle.switchImageList(imageList);
// 此模式为三拼,也可设置两拼,上面imageList设置两张图片
this.puzzle.switchMode(2);
this.pannel = this.puzzle.getCurPannel();
this.puzzle.draw();
},
methods: {
touchstart(e){
this.puzzle.signleTouchstart(e);
},
touchmove(e){
this.puzzle.signleTouchmove(e);
},
touchend(e){
this.puzzle.signleTouchend(e);
},
compositTouchstart(e){
this.puzzle.touchstart(e.touches);
},
generate(){
this.puzzle.downImage('composit-area', this);
},
}
}
</script>
<style scoped lang="scss">
.edit-area{
background: red;
}
.area{
position: fixed;
left: 0;
bottom: 0;
width: 100%;
display: flex;
justify-content: center;
.composit-area{
border: 1rpx solid #eee;
}
}
.btn{
position: fixed;
left: 40rpx;
bottom: 40rpx;
z-index: 10;
}
</style>
puzzle.js文件封装拼图对象
// 图片对象
export class PicElement {
constructor(url,width = 100,height = 100,centerX = 0,centerY = 0,angle = 0,scale = 1,translateX = 0,translateY = 0) {
// 图片路径
this.url = url;
// 中心点x位置
this.centerX = centerX;
// 中心点y位置
this.centerY = centerY;
// 宽度
this.width = width;
// 高度
this.height = height;
// 旋转的角度
this.angle = angle;
// 缩放的比例
this.scale = scale;
// 偏移x轴量
this.translateX = translateX;
// 偏移y轴量
this.translateY = translateY;
// 上一次结束后保留的状态值
this.lastStatus = {
translateX: 0,
translateY: 0,
scale: 1,
angle: 0,
}
}
// 计算单个适合图片展示的宽高
static getImageInfo(url) {
return new Promise((resolve,reject) => {
uni.getImageInfo({
src: url,
success: (info)=> {
resolve({
path: info.path,
width: info.width,
height: info.height
})
}
})
})
}
// 获取多个适合图片展示的宽高
static getImagesInfo(images = []) {
return new Promise((resolve,reject) => {
let imagesPromise = [];
images.forEach(async ele => {
imagesPromise.push(PicElement.getImageInfo(ele));
});
Promise.all(imagesPromise).then(values => {
resolve(values);
}).catch(res=>{
reject(res);
});
});
}
// 拖动操作
setTranslate(translateX,translateY){
// 根据角度计算位移的大小
this.translateX += (translateX * Math.cos(this.angle) + translateY * Math.sin(this.angle));
this.translateY += (-translateX * Math.sin(this.angle) + translateY * Math.cos(this.angle));
}
// 缩放操作
setScale(scale){
this.scale *= scale;
}
// 缩放操作
setAngle(angle){
this.angle += angle;
}
// 绘制
draw(ctx) {
ctx.save();
let scale = this.scale*this.lastStatus.scale;
let translateX = this.translateX + this.lastStatus.translateX;
let translateY = this.translateY + this.lastStatus.translateY;
let left = this.centerX - this.width*scale/2 + translateX;
let top = this.centerY - this.height*scale/2 + translateY;
ctx.rotate(this.angle + this.lastStatus.angle);
ctx.drawImage(this.url,left,top,this.width*scale,this.height*scale);
ctx.restore();
}
// 绘制拼接图(此方法绘制合成拼图)
drawPuzzle(ctx) {
let scale = this.scale*this.lastStatus.scale;
let translateX = this.translateX + this.lastStatus.translateX;
let translateY = this.translateY + this.lastStatus.translateY;
let left = this.centerX - this.width*scale/2 + translateX;
let top = this.centerY - this.height*scale/2 + translateY;
ctx.rotate(this.angle + this.lastStatus.angle);
ctx.drawImage(this.url,left,top,this.width*scale,this.height*scale);
}
}
// 展示的区域对象
export class Pannel{
constructor(width = 750,height = 750) {
// 区域的宽度
this.width = width;
// 区域的高度
this.height = height;
// 区域内的元素
this.eleArr = [];
// 上一次触摸点的位置
this.lastTouches = null;
}
// 添加元素
addEle(ele){
this.eleArr.push(ele);
}
// 计算元素在区域显示的宽高
calculateRange(eleWidth,eleHeight){
let width = eleWidth,height = eleHeight;
if(this.width/this.height > eleWidth/eleHeight){
// 比较高
if(eleHeight > this.height*0.6){
width = (this.height*0.6)/eleHeight*eleWidth;
height = this.height*0.6;
}
}else{
// 比较宽
if(eleWidth > this.width*0.6){
width = this.width*0.6;
height = (this.width*0.6)/eleWidth*eleHeight;
}
}
return {
width,
height
}
}
// 计算距离
getDistance(p1,p2){
return Math.hypot(p1.x - p2.x,p1.y - p2.y);
}
// 计算角度
getAngle(p1,p2){
let x = p1.x - p2.x;
let y = p1.y - p2.y;
return Math.atan2(y,x);
}
// 开始触摸
touchstart(e){
const { touches } = this.compatible(e);
if(touches.length == 1){
this.lastTouches = touches;
}else if(touches.length > 1){
// 微信小程序单指和多指bug
// 需要在处理一层
// https://developers.weixin.qq.com/community/develop/doc/0008a6353b0af000e60a4e8045d000?highLine=%25E5%258D%2595%25E6%258C%2587%25E5%2592%258C%25E5%258F%258C%25E6%258C%2587%25E6%2593%258D%25E4%25BD%259C
// if (touches.length == 2 && (touches[0].identifier == 1 && touches[1].identifier == 0)) {
// touches.shift();
// }
this.lastTouches = touches;
}
}
// 兼容小程序canvas touchmove bug
// https://developers.weixin.qq.com/community/develop/doc/0008a6353b0af000e60a4e8045d000?fromCreate=0
//小程序bug(未解决)
compatible(e) {
// if (e.mp.changedTouches.length < 1) {
// return e;
// }
// const base = e.mp.changedTouches[0].identifier;
// const index = (e.touches || []).findIndex(i => i.identifier == base);
// let newTouches = [];
// if (index > 0 && e.touches[index - 1].identifier === base - 1) {
// newTouches.push(e.touches[index - 1]);
// }
// newTouches = newTouches.concat(e.touches.filter(i => i.identifier >= base));
let newTouches = e.touches;
// if (e.touches.length >= 2 && (e.touches[0].identifier == 1 && e.touches[1].identifier == 0)) {
// newTouches = [e.touches[1]];
// }
return { ...e, touches: newTouches };
}
// 触摸移动中
touchmove(e){
const { touches } = this.compatible(e);
if(touches.length == 1){
// 计算上一次触碰和此处触碰相差的距离
this.setTranslate(touches[0].x - this.lastTouches[0].x,touches[0].y - this.lastTouches[0].y);
}else if(touches.length > 1){
let scale = this.getDistance(touches[0],touches[1]) / this.getDistance(this.lastTouches[0],this.lastTouches[1]);
// console.log(`${touches[0].x}:${touches[0].y}----0+++1----${this.lastTouches[0].x}:${this.lastTouches[0].y}`);
// console.log(`${touches[1].x}:${touches[1].y}----0+++1----${this.lastTouches[1].x}:${this.lastTouches[1].y}`);
// console.log(`${this.getDistance(touches[0],touches[1])}:${this.getDistance(this.lastTouches[0],this.lastTouches[1])}`);
// console.log(`scale:${scale}`);
// 设置缩放
this.setScale(scale);
let angle = this.getAngle(touches[0],touches[1]) - this.getAngle(this.lastTouches[0],this.lastTouches[1]);
// 设置旋转
this.setAngle(angle);
// console.log(`distance:${distance}---angle:${angle}`);
}
this.lastTouches = touches;
}
// 触摸结束
touchend(touches){
// console.log(`over`);
// console.log(touches);
// if (touches && touches[0] && touches[0].identifier == 1) {
// touches.shift();
// }
}
// 拖动操作
setTranslate(translateX,translateY){
if(this.eleArr.length > 0){
this.eleArr.forEach(ele => {
ele.setTranslate(translateX,translateY);
});
}
}
// 缩放操作
setScale(scale){
if(this.eleArr.length > 0){
this.eleArr.forEach(ele => {
ele.setScale(scale);
});
}
}
// 旋转操作
setAngle(angle){
if(this.eleArr.length > 0){
this.eleArr.forEach(ele => {
ele.setAngle(angle);
});
}
}
// 绘制
draw(ctx){
ctx.translate(this.width/2,this.height/2);
this.eleArr.forEach(ele => {
ele.draw(ctx);
})
ctx.draw();
}
// 拼接图绘制(此方法绘制合成拼图)
drawPuzzle(ctx){
this.eleArr.forEach(ele => {
ele.drawPuzzle(ctx);
})
}
}
// 拼图
export class Puzzle {
constructor(width = 100, height = 100, ctx = null, signleCtx = null, mode = 1, pic = []) {
// 区域的宽度
this.width = width;
// 区域的高度
this.height = height;
// 模式
this.mode = mode;
// 区域数组
this.pannelArr = [];
// 当前区域显示的pannel 索引
this.curIndex = 0;
// 当前拼图的图片列表
this.pic = pic;
// 当前合成拼图canvas
this.ctx = ctx;
// 当个区域canvas
this.signleCtx = signleCtx;
}
// 切换拼图方式
async switchMode(mode = 1) {
this.mode = mode;
switch (this.mode){
// 设置双拼
case 1:
if (!this.pic || this.pic.length !== 2) {
console.error('图片只能两张');
return;
}
// 设置三拼
case 2:
if (!this.pic || this.pic.length !== 3) {
console.error('图片只能三张');
return;
}
break;
default:
break;
}
let pannelArr = [];
this.pic.forEach(ele => {
let pannel =new Pannel(uni.upx2px(750), uni.upx2px(750));
let imageSize = pannel.calculateRange(ele.width,ele.height);
const pic = new PicElement(ele.path,imageSize.width,imageSize.height);
pannel.addEle(pic);
pannelArr.push(pannel);
});
this.pannelArr = pannelArr;
}
// 返回当前pannel
getCurPannel() {
return this.pannelArr[this.curIndex];
}
// 切换图片列表
async switchImageList(pic = []) {
this.pic = await PicElement.getImagesInfo(pic);
return this.pic;
}
// 当个canvas操作区域的开始触碰
signleTouchstart(touches) {
this.getCurPannel().touchstart(touches);
this.draw();
}
// 当个canvas操作区域的触碰中
signleTouchmove(touches) {
this.getCurPannel().touchmove(touches);
this.draw();
}
// 当个canvas操作区域的触碰结束
signleTouchend(touches) {
this.getCurPannel().touchend(touches);
this.draw();
}
// 拼图区域触碰
touchstart(touches) {
let index = this.curIndex;
let len = this.pannelArr.length;
for (let i = 0; i < len; i++) {
let pannelArea = this.getPannelArea(i);
let condition = (
(pannelArea.left < touches[0].x && touches[0].x <= pannelArea.right)
&&(pannelArea.top < touches[0].y && touches[0].y <= pannelArea.bottom)
);
if (condition) {
index = i;
break;
}
}
if (index != this.curIndex) {
this.curIndex = index;
this.draw();
}
}
// 获取每个区域的信息
getPannelArea(index) {
let panne;
switch (this.mode){
// 双拼
case 1:
panne = {
left: this.width/2*index,
top: 0,
right: this.width/2*(index+1),
bottom: this.height,
width: this.width/2,
height: this.height,
centerX: this.width/4 + (this.width/2*index),
centerY: this.height/2,
};
break;
// 三拼
case 2:
panne = {
left: this.width/3*index,
top: 0,
right: this.width/3*(index+1),
bottom: this.height,
width: this.width/3,
height: this.height,
centerX: this.width/6 + (this.width/3*index),
centerY: this.height/2,
};
break;
default:
break;
}
return panne;
}
// 绘制
draw() {
this.getCurPannel().draw(this.signleCtx);
this.ctx.setFillStyle('white');
this.pannelArr.forEach((pannel,index) => {
this.ctx.save();
let pannelArea = this.getPannelArea(index);
this.ctx.translate(pannelArea.centerX,pannelArea.centerY);
this.ctx.beginPath();
this.ctx.rect(pannelArea.left - pannelArea.centerX,pannelArea.top - pannelArea.centerY,pannelArea.width,pannelArea.height);
this.ctx.fill();
this.ctx.clip();
pannel.drawPuzzle(this.ctx);
this.ctx.restore();
});
this.ctx.draw();
}
// 生成图片保存本地
downImage(canvasId,env) {
uni.canvasToTempFilePath({
x: 0,
y: 0,
width: this.width,
height: this.height,
canvasId: canvasId,
success: (res) => {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: (res) => {
}
})
},
fail: (err) => {
console.error(`生成图片失败`);
}
},env);
}
}
网友评论