前端使用clip-path及canvas实现图片裁剪绘制的新技巧

201 阅读4分钟

本文采用原生js实现选择图片并进行裁剪,将裁剪后的图片使用canvas进行绘制。附效果图及源码。

一、 效果图

capture_20231214114725799.png

二、 源码

1.html部分

 <div class="img-box">
    <!-- 原图 -->
    <img class="img1" alt="" />
    <!-- 蒙层 -->
    <div class="mask" style="display: none;"></div>
    <!-- 被裁剪区域显示的图片 -->
    <img class="img2" alt="" />
    <!-- 裁剪区域 -->
    <div class="img-section-box" style="display: none;">
        <span data-pos="leftTop"></span>
        <span data-pos="topCenter"></span>
        <span data-pos="rightTop"></span>
        <span data-pos="rightCenter"></span>
        <span data-pos="rightBottom"></span>
        <span data-pos="bottomCenter"></span>
        <span data-pos="leftBottom"></span>
        <span data-pos="leftCenter"></span>
    </div>
    <!-- 提示 -->
    <p>请上传图片</p>
</div>

<!-- 处理按钮 -->
<div class="handle-btn">
    <button class="upload">上传图片</button>
    <button class="save">保存</button>
</div>

<input type="file" name="" id="file" style="display: none;">

<canvas id="canvas" width="300" height="300"></canvas>

2. css部分

    body,
    html {
        padding: 0;
        margin: 0;
    }

    .img-box {
        position: relative;
        display: flex;
        justify-content: center;
        align-items: center;
    }

    .img-box img {
        position: absolute;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        user-select: none;
    }

    .img-box .img-section-box {
        position: absolute;
        left: var(--l);
        top: var(--t);
        z-index: 10;
        pointer-events: none;
    }

    .img-box .img-section-box span {
        position: absolute;
        width: 8px;
        height: 8px;
        background-color: #fff;
        pointer-events: visible;
    }

    .img-section-box span:nth-child(1) {
        left: -4px;
        top: -4px;
        cursor: nwse-resize;
    }

    .img-section-box span:nth-child(2) {
        left: 50%;
        top: -4px;
        transform: translateX(-50%);
        cursor: ns-resize;
    }

    .img-section-box span:nth-child(3) {
        right: -4px;
        top: -4px;
        cursor: nesw-resize;
    }

    .img-section-box span:nth-child(4) {
        display: block;
        right: -4px;
        top: 50%;
        transform: translateY(-50%);
        cursor: ew-resize;
    }

    .img-section-box span:nth-child(5) {
        right: -4px;
        bottom: -4px;
        cursor: nwse-resize;
    }

    .img-section-box span:nth-child(6) {
        left: 50%;
        bottom: -4px;
        transform: translateX(-50%);
        cursor: ns-resize;
    }

    .img-section-box span:nth-child(7) {
        left: -4px;
        bottom: -4px;
        cursor: nesw-resize;
    }

    .img-section-box span:nth-child(8) {
        left: -4px;
        top: 50%;
        transform: translateY(-50%);
        cursor: ew-resize;
    }

    .img-box .mask {
        position: absolute;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.5);
    }

    .img-box .img2 {
        clip-path: inset(var(--t) var(--r) var(--b) var(--l));
        cursor: move;
        z-index: 9;
    }

3. js部分

    // 裁剪区域外层容器
    const imgBox = document.querySelector(".img-box");
    // 获取原图及被裁剪区域图片
    const imgs = imgBox.querySelectorAll("img");
    // canvas元素
    const canvas = document.querySelector("#canvas");
    // 保存按钮
    const saveBtn = document.querySelector(".save");
    // 上传按钮
    const uploadBtn = document.querySelector(".upload");
    // 上传input
    const uploadInput = document.querySelector("#file");
    // 被裁剪区域外层容器
    const imgSectionBox = document.querySelector(".img-section-box");
    // 获取伸缩点元素
    const stretchEl = [...imgSectionBox.children];
    // 获取蒙层元素
    const mask = document.querySelector(".mask");
    // 获取canvas上下文
    const context = canvas.getContext('2d');
    // 是否按下被裁剪区域元素标识
    let isDown = false;
    // 是否伸缩标识
    let isStrech = false;
    // 伸缩点处理标识
    let dataPos = "";
    // 按下被裁剪区域时当前的x轴位置
    let downX = 0;
    // 按下被裁剪区域时当前的y轴位置
    let downY = 0;
    // 被裁剪区域距离原图顶部距离
    let t = 0;
    // 被裁剪区域距离原图左侧距离
    let l = 0;
    // 被裁剪区域距离原图右侧距离(默认400px,位于最左侧)
    let r = 200;
    // 被裁剪区域距离原图底部距离(默认400px,位于最上面)
    let b = 200;
    // 被裁剪区域宽度(默认100px)
    let beClipW = 100;
    // 被裁剪区域高度(默认100px)
    let beClipH = 100;
    // 裁剪区域宽度(默认500px)
    const clipW = 300;
    // 裁剪区域高度(默认500px)
    const clipH = 300;
    // 上传图片的原始宽度
    let imgOriginalW = 0;
    // 上传图片的原始高度
    let imgOriginalH = 0;
    // 裁剪图片地址
    let clipImgUrl = "";


    // 设置被裁剪区域元素大小
    const setBeClipEl = () => {
        imgSectionBox.style.width = beClipW + 'px';
        imgSectionBox.style.height = beClipH + "px";
    }

    // 设置裁剪区域元素大小
    const setClipEl = () => {
        imgBox.style.setProperty('width', clipW + 'px');
        imgBox.style.setProperty('height', clipH + 'px');
    }

    // 设置被裁剪区域元素位置
    const setBeClipElPosition = () => {
        imgBox.style.setProperty('--t', t + 'px');
        imgBox.style.setProperty('--l', l + 'px');
        imgBox.style.setProperty('--b', b + 'px');
        imgBox.style.setProperty('--r', r + 'px');
    }

    // 初始化
    const init = () => {
        // 设置被裁剪区域元素大小
        setBeClipEl();

        // 设置裁剪区域元素大小
        setClipEl();

        // 设置被裁剪区域元素位置
        setBeClipElPosition();
    }

    init();

    // 被裁剪区域图片添加鼠标按下事件
    imgs[1].addEventListener("mousedown", (e) => {
        isDown = true;
        downX = e.offsetX - l;
        downY = e.offsetY - t;
    });

    document.body.addEventListener("mouseup", () => {
        // 复原裁剪区域按下标识
        isDown = false;
        // 复原伸缩点元素按下标识
        isStrech = false;
    });

    // 裁剪区域外层容器添加移动事件
    imgBox.addEventListener("mousemove", (e) => {
        if (!isDown && !isStrech) return;

        e.preventDefault();


        if (isDown) {
            // 被裁剪区域图片移动
            handleBeClipImgMove(e);
        } else if (isStrech) {
            // 伸缩点移动
            handleStrechPointMove(e);
        }
    });

    // 处理被裁剪区域图片按下移动
    const handleBeClipImgMove = (e) => {
        t = e.offsetY - downY;
        l = e.offsetX - downX;
        b = imgBox.offsetHeight + downY - (e.offsetY + beClipH);
        r = imgBox.offsetWidth + downX - (e.offsetX + beClipW);

        if (b > imgBox.offsetHeight - beClipH) {
            b = imgBox.offsetHeight - beClipH;
            t = 0;
        }
        if (r > imgBox.offsetWidth - beClipW) {
            r = imgBox.offsetWidth - beClipW;
            l = 0;
        }
        if (t > imgBox.offsetHeight - beClipH) {
            t = imgBox.offsetHeight - beClipH;
        }
        if (l > imgBox.offsetWidth - beClipW) {
            l = imgBox.offsetWidth - beClipW;
        }

        imgBox.style.setProperty('--t', t + 'px');
        imgBox.style.setProperty('--l', l + 'px');
        imgBox.style.setProperty('--b', b + 'px');
        imgBox.style.setProperty('--r', r + 'px');
    }

    // 处理伸缩点按下移动
    const handleStrechPointMove = (e) => {
        // 左上
        const handleLeftTop = () => {
            handleTopCenter();
            handleLeftCenter();
        }

        // 上中
        const handleTopCenter = () => {
            beClipH = imgBox.offsetHeight - e.clientY - b > imgBox.offsetHeight - b ? imgBox.offsetHeight - b : imgBox.offsetHeight - e.clientY - b;
            t = e.clientY < 0 ? 0 : e.clientY;
            imgBox.style.setProperty('--t', t + 'px');
        }

        // 右上
        const handleRightTop = () => {
            handleTopCenter();
            handleRightCenter();
        }

        // 下中
        const handleBottomCenter = () => {
            beClipH = e.clientY - t > imgBox.offsetHeight - t ? imgBox.offsetHeight - t : e.clientY - t;
            b = imgBox.offsetHeight - e.clientY < 0 ? 0 : imgBox.offsetHeight - e.clientY;
            imgBox.style.setProperty('--b', b + 'px');
        }

        // 右中
        const handleRightCenter = () => {
            beClipW = e.clientX - l > imgBox.offsetWidth - l ? imgBox.offsetWidth - l : e.clientX - l;
            r = imgBox.offsetWidth - e.clientX < 0 ? 0 : imgBox.offsetWidth - e.clientX;
            imgBox.style.setProperty('--r', r + 'px');
        }

        // 右下
        const handleRightBottom = () => {
            handleBottomCenter();
            handleRightCenter();
        }

        // 左下
        const handleLeftBottom = () => {
            handleBottomCenter();
            handleLeftCenter();
        }

        // 左中
        const handleLeftCenter = () => {
            beClipW = imgBox.offsetWidth - e.clientX - r > imgBox.offsetWidth - r ? imgBox.offsetWidth - r : imgBox.offsetWidth - e.clientX - r;
            l = e.clientX < 0 ? 0 : e.clientX;
            imgBox.style.setProperty('--l', l + 'px');

        }

        switch (dataPos) {
            case "leftTop":
                handleLeftTop();
                break;
            case "topCenter":
                handleTopCenter();
                break;
            case "rightTop":
                handleRightTop();
                break;
            case "rightCenter":
                handleRightCenter();
                break;
            case "rightBottom":
                handleRightBottom();
                break;
            case "bottomCenter":
                handleBottomCenter();
                break;
            case "leftBottom":
                handleLeftBottom();
                break;
            case "leftCenter":
                handleLeftCenter();
                break;
        }

        // 更新被裁剪区域元素宽高
        setBeClipEl();
    }
        
    // 给伸缩点元素添加按下事件
    stretchEl.forEach(item => {
        item.addEventListener("mousedown", (e) => {
            if (isStrech) return;
            isStrech = true;
            dataPos = e.target.dataset.pos;
        });
    });

    // 保存回调
    const saveCallback = () => {
        if (clipImgUrl === "") return alert("请上传图片后再试");
        // 根据原始图片与裁剪区域图片大小计算图片比例
        const left = imgOriginalW / clipW * l;
        const top = imgOriginalH / clipH * t;
        const sw = imgOriginalW / clipW * beClipW;
        const sh = imgOriginalH / clipH * beClipH;
        // 绘制被裁剪图片
        context.drawImage(imgs[0], left, top, sw, sh, 0, 0, 100, 100);
    }

    // 保存
    saveBtn.addEventListener("click", saveCallback);

    // 上传图片
    uploadBtn.addEventListener("click", () => {
        uploadInput.click();
    });

    // 图片选择触发回调
    uploadInput.addEventListener("change", (e) => {
        const file = e.target.files[0];
        const url = window.URL || window.webkitURL;
        clipImgUrl = url.createObjectURL(file);
        imgs[0].src = clipImgUrl;
        imgs[1].src = clipImgUrl;
        // 显示蒙层
        mask.style.display = "block";
        // 显示被裁剪区域外层容器
        imgSectionBox.style.display = "block";
        //创建Image对象
        const img = new Image();
        //创建Image的对象的url
        img.src = clipImgUrl;
        img.onload = function() {
            // 获取图片原始高度
            imgOriginalH = this.height;
            // 获取图片原始宽度
            imgOriginalW = this.width;
        }
    })