canvas实现图片在固定容器中缩放和拖拽

191 阅读1分钟

因业务需求,需要实现canvas在固定容器中缩放和拖拽的功能,在网上查看了很多例子,多半都是内容小于容器的,并且放大后还会存在bug,只能用vue2手撸一个。

效果如下:

20230625-135523.gif

代码如下:


<template>
    <div class="container">
        <div class="">此页面除canvas拖动缩放以外的功能均待实现</div>
        <div class="scale-button">
            <button @click="zoom(-1)">+</button>
            <button @click="zoom(1)">-</button>
        </div>
        <el-upload
            class="upload-demo"
            action="https://jsonplaceholder.typicode.com/posts/"
            :on-preview="handlePreview"
            :on-remove="handleRemove"
            :on-success="handleSuccess"
            :before-remove="beforeRemove"
            multiple
            :limit="3"
            :on-exceed="handleExceed"
            :file-list="fileList"
        >
            <el-button size="small" type="primary">点击上传</el-button>
            <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
        </el-upload>
        <div id="img-container" class="img-container">
            <div id="canvas-shell" class="canvas-shell">
                <canvas id="canvas"></canvas>
            </div>
        </div>
    </div>
</template>

<script>
// import { toRaw } from 'vue';
import { countNum } from '@/utils/utils';

class Scene {
    maxOffset = { x: 0, y: 0 }; // 内容的最大偏移量
    transSet = { x: 0, y: 0 }; // 内容的偏移量
    x = 0; // 记录鼠标点击Canvas时的横坐标
    y = 0; // 记录鼠标点击Canvas时的纵坐标
    multiple = 1; // 缩放率
    outDomCoordinate = ''; // 最外层容器相对于浏览器窗口的各项数据

    constructor(
        options = {
            height: 0,
            shellName: '',
            outDomName: '',
        }
    ) {
        // 设置包裹canvas的外层div信息
        this.shell = document.getElementById(options.shellName);
        this.shell.style.width = `324px`;
        this.shell.style.height = `${options.height}px`;

        // 设置最外层容器
        this.outDom = document.getElementById(options.outDomName);

        // 绑定各类事件
        this.shell.addEventListener('mousedown', this.onMousedown);
        this.shell.addEventListener('wheel', this.onWheel);

        this.handleOutDomRect();

        // 初始化最大偏移量
        this.handleOffset();
        this.handleTransform();
    }

    // 记录最外层容器相对于浏览器窗口的各项数据
    handleOutDomRect = () => {
        const { top, left, right, bottom } = this.outDom.getBoundingClientRect();
        this.outDomCoordinate = {
            outDomTop: top,
            outDomLeft: left,
            outDomRight: right,
            outDomBottom: bottom,
        };
    };

    // 鼠标按下
    onMousedown = e => {
        if (e.button === 0) {
            this.x = e.clientX - this.transSet.x; // 图片初始位置
            this.y = e.clientY - this.transSet.y; // 图片初始位置
            window.addEventListener('mousemove', this.onMousemove);
            window.addEventListener('mouseup', this.onMouseup);
        }
    };

    // 鼠标拖动
    onMousemove = e => {
        this.transSet.x = countNum(e.clientX, this.x, 'sub', 2); // x向移动距离
        this.transSet.y = countNum(e.clientY, this.y, 'sub', 2); // y向移动距离

        this.paint();
    };

    // 鼠标抬起
    onMouseup = () => {
        window.removeEventListener('mousemove', this.onMousemove);
        window.removeEventListener('mouseup', this.onMouseup);
    };

    // 滚轮缩放
    onWheel = e => {
        const _currentMultiple = Number(this.multiple).toFixed(1);
        if (e.deltaY < 0 && _currentMultiple < 4) {
            this.multiple = countNum(this.multiple, 0.1, 'add', 1);
        } else if (e.deltaY >= 0 && _currentMultiple > 1) {
            this.multiple = countNum(this.multiple, 0.1, 'sub', 1);
            // 如果缩小则强制回正偏移,否则会造成拖拽的错误
            this.transSet = {
                x: 0,
                y: 0,
            };
        }
        this.handleOffset();
        this.handleTransform();
    };

    // 处理当前缩放率下的最大偏移量
    handleOffset = () => {
        let { width, height } = this.shell.style;
        width = width.replace(/px/g, '');
        height = height.replace(/px/g, '');
        this.maxOffset = {
            x: (width * this.multiple - 324) / 2 / this.multiple,
            y: (height * this.multiple - 576) / 2 / this.multiple,
        };
    };

    // 画图
    paint() {
        // 计算最终可偏移的偏移量
        const { transX, transY } = this.handleBorder();
        this.transSet = {
            x: transX,
            y: transY,
        };

        this.handleTransform();
    }

    // 处理边界,使图片不能拖动出边界
    handleBorder = () => {
        let transX, transY;
        // 拿到内容和最外层容器相对于浏览器窗口的距离
        const { top, left, right, bottom, height } = this.shell.getBoundingClientRect(),
            { outDomTop, outDomLeft, outDomRight, outDomBottom } = this.outDomCoordinate;
        // 处理y轴偏移量
        if (top < outDomTop && bottom > outDomBottom) {
            transY = Math.min(`${this.maxOffset.y}`, this.transSet.y);
        } else if (top > outDomTop && bottom < outDomBottom) {
            if (this.transSet.y > 0) {
                transY = Math.min(`${-this.maxOffset.y}`, this.transSet.y);
            } else {
                transY = Math.max(`${this.maxOffset.y}`, this.transSet.y);
            }
        } else if (top >= outDomTop && bottom > outDomBottom) {
            transY = Math.min(this.maxOffset.y, this.transSet.y);
        } else if (top < outDomTop && bottom <= outDomBottom) {
            transY = Math.max(`${-this.maxOffset.y}`, this.transSet.y);
        } else if (top == outDomTop && bottom == outDomBottom) {
            transY = 0;
        }
        // 处理x轴偏移量
        if ((left < outDomLeft && right > outDomRight) || (left >= outDomLeft && right > outDomRight)) {
            transX = Math.min(`${this.maxOffset.x}`, this.transSet.x);
        } else if (left < outDomLeft && right <= outDomRight) {
            transX = Math.max(`${-this.maxOffset.x}`, this.transSet.x);
        } else if (left == outDomLeft && right == outDomRight) {
            transX = 0;
        }
        return { transX, transY };
    };

    // 处理图像的偏移和缩放
    handleTransform = () => {
        this.shell.style.transform = `scale(${this.multiple}) translate(${this.transSet.x}px, ${this.transSet.y}px)`;
    };
}

let canvasContainer;

export default {
    name: 'convertImgIntoCanvas',
    components: {},
    data: () => ({
        fileList: [
            { name: 'food.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100' },
            { name: 'food2.jpeg', url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100' },
        ],
    }),
    computed: {},
    mounted() {
        // const imgUrl =
        //     'https://ts1.cn.mm.bing.net/th/id/R-C.d8dfd08893b58d08d74b38ad8870a48d?rik=9KBqff6Rai035Q&riu=http%3a%2f%2fstatic.cntonan.com%2fuploadfile%2f2019%2f0214%2f20190214104244pwm1xxsdikh.jpg&ehk=MOxI2n5nY44gO%2fKsNYWAuEBvcRwSmkRVNb4dTS6Gk%2bY%3d&risl=&pid=ImgRaw&r=0';
        // const imgWidth = 1125, // 图片原始宽
        //     imgHeight = 2436; // 图片原始高

        const imgUrl = 'https://img1.baidu.com/it/u=3861296666,2632273690&fm=253&fmt=auto&app=138&f=JPEG?w=667&h=500';
        const imgWidth = 667, // 图片原始宽
            imgHeight = 500; // 图片原始高
        // const imgWidth = 1280, // 图片原始宽
        //     imgHeight = 1707; // 图片原始高

        const { width, height, multiple } = this.handleShellWidthHeight(imgWidth, imgHeight);
        let img = {
            height,
            shellName: 'canvas-shell',
            outDomName: 'img-container',
        };
        canvasContainer = new Scene(img);

        let canvas = document.getElementById('canvas');
        let ctx = canvas.getContext('2d');

        let image = new Image(); // 创建一个<img>元素
        image.src = imgUrl; // 设置图片源地址
        image.onload = () => {
            ctx.drawImage(image, 0, 0, imgWidth, imgHeight);
        };

        canvas.width = imgWidth;
        canvas.height = imgHeight;
        canvas.style.transform = `scale(${multiple})`;
    },
    methods: {
        handleRemove(file, fileList) {
            console.log(file, fileList);
        },
        handlePreview(file) {
            console.log(file);
        },
        handleSuccess(res, file, fileList) {
            console.log(res, file, fileList);
        },
        handleExceed(files, fileList) {
            this.$message.warning(`当前限制选择 3 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
        },
        beforeRemove(file, fileList) {
            return this.$confirm(`确定移除 ${file.name}?`);
        },

        // 处理canvas外层的宽高以及canvas的缩放率(324,576为最外层容器固定宽高)
        handleShellWidthHeight(domWidth, domHeight) {
            let width = 324,
                multiple = countNum(324, domWidth, 'div', 6),
                height = countNum(domHeight, multiple, 'mul', 6);
            return { width, height, multiple };
        },
        zoom(type) {
            let _obj = {
                deltaY: type,
            };
            canvasContainer.onWheel(_obj);
        },
    },
};
</script>

<style lang="scss" scoped>
.container {
    width: 100vw;
    height: 100vh;

    .img-container {
        width: 324px;
        height: 576px;
        overflow: hidden;
        border: 1px solid black;
        display: flex;
        align-items: center;
        justify-content: center;
        position: relative;
        box-sizing: border-box;

        .canvas-shell {
            width: 324px;
            height: 576px;
            // transition: transform 0.2s cubic-bezier(0, 0, 0.25, 1) 0s;

            #canvas {
                transform-origin: left top;
            }
        }
    }
}
</style>


备注:countNum方法为处理浮点数的四则运算方法