美文网首页canvas
canvas拼图效果

canvas拼图效果

作者: 谢炳南 | 来源:发表于2022-01-25 16:49 被阅读0次

效果图

7645970-bc4d0c7dac6dcc99.gif
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);
    }
}

相关文章

网友评论

    本文标题:canvas拼图效果

    本文链接:https://www.haomeiwen.com/subject/ojcwhrtx.html