使用 HTML5 Canvas 实现用户自定义裁剪图片

在Web开发中,经常需要处理用户上传的图片,其中一个常见的需求是允许用户选择并裁剪图片。本文将介绍如何使用HTML、CSS和JavaScript实现一个简单的图片裁剪工具。

步骤概览

  1. 创建HTML结构,包含文件上传控件、裁剪前的图片显示区域,选择裁剪区域、Canvas和显示裁剪后图片的标签。
  2. uploadFile.onchange 中创建一个 Image 对象,并在 onload 事件中获取图片的实际尺寸(image.widthimage.height),用于计算并设置图片在裁剪容器中的显示尺寸,以保持图片的宽高比例。
  3. 在图片上实现裁剪功能,其实就是一个拖拽,鼠标按下可以拖动裁剪区域,松开就禁止拖拽。用户选择完裁剪区域后,我们先计算出裁剪框在原图片中的位置和尺寸,根据这些信息绘制裁剪后的图片到Canvas上。
  4. 根据裁剪框在原图片中的位置和尺寸与Canvas上图片尺寸根据比列关联起来。
  5. 将裁剪后的图片转换为Blob对象,并显示在页面上。
  6. 将Blob对象上传到服务器。

一.HTML和CSS结构

首先,我们需要一个HTML结构来容纳上传的图片和裁剪区域。以下是一个基本的HTML结构:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>

    <style>
        * {
            margin: 0;
        }

        body {
            color: #fff;
            background: rgba(0, 0, 0, 0.8);
        }

        .wrap {
            width: 500px;
            margin: 50px;
        }

        canvas {
            display: none;
            border: 1px solid red;
        }

        .upload {
            width: 150px;
            height: 30px;
            border: 1px solid;
            margin: 20px auto;
            position: relative;
        }

        .upload-file {
            position: absolute;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            opacity: 0;
            z-index: 1;
            cursor: pointer;
        }

        .upload-btn {
            position: absolute;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            text-align: center;
            line-height: 1.5;
            font-size: 20px;
            color: #fff;
            border-radius: 5px;
        }


        .clip-area {
            text-align: center;
        }

        .clip-wrap {
            display: none;
            width: 500px;
            height: 500px;
            background: #000;
            position: relative;
        }

        .clip-wrap .clip-img {
            position: absolute;
            left: 0;
            right: 0;
            bottom: 0;
            top: 0;
            margin: auto;
        }

        .clip-box {
            display: none;
            position: absolute;
            width: 300px;
            height: 300px;
            background: rgba(0, 0, 0, 0.5);
            box-sizing: border-box;
            border: 1px solid #fff;
        }


        .clip-after {
            text-align: center;
        }

        .clip-after h1 {
            margin: 20px 0;
        }

        .clip-after-img {
            width: 300px;
        }

        .save-btn {
            width: 100px;
            height: 30px;
            line-height: 30px;
            text-align: center;
            color: #fff;
            background: transparent;
            border: 1px solid #fff;
            cursor: pointer;
            margin-top: 20px;
        }
    </style>
</head>

<body>
    <div class="wrap">
        <div class="upload">
            <input type="file" class="upload-file">
            <div class="upload-btn">图片上传</div>
        </div>

        <!-- 显示上传的图片并选择裁剪区域 -->
        <div class="clip-area">
            <h1>裁剪区域</h1>
            <div class="clip-wrap">
                <img src="" class="clip-img" style="width: 300px;">
                <div class="clip-box"></div>
            </div>
        </div>


        <!-- 裁剪后的图片 -->
        <div class="clip-after">
            <h1> 裁剪后的图片</h1>
            <div class="clip-after-content">
                <img src="" class="clip-after-img">
            </div>
            <canvas id="canvas"></canvas>
            <button class="save-btn">确认</button>
        </div>

    </div>

</body>

</html>

二.JS逻辑

1、实现选择图片并把图片等比例显示出来

实现一个简单的选择图片,再创建了一个图片实例 image,然后使用 FileReader 对用户上传的图片进行读取,并将读取结果赋值给 image 的 src 属性。接着,通过获取 image 的宽度和高度,我们可以得知原始图片的尺寸。基于这些尺寸信息,在裁剪前先把图片等比例显示出来

 const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');
        canvas.width = 300;
        canvas.height = 300;
        const uploadFile = document.querySelector('.upload-file');
        const clipAfterImg = document.querySelector('.clip-after-img');
        let imageBlob;

        //上传图片
        uploadFile.onchange = (e) => {
            clipWrap.style.display = 'block';
            const fileData = e.target.files[0];
            const reader = new FileReader();
            const image = new Image();
            reader.readAsDataURL(fileData);
            // 异步读取文件内容,结果用data:url的字符串形式表示
            reader.onload = function (e) {
                const imgUrl = this.result;
                image.src = this.result;
                image.onload = function () {
                    // 计算图片绘制区域,确保图片保持比例
                    const aspectRatio = image.width / image.height;
                    computeSize(aspectRatio, imgUrl);
                };
            }
        }

        //计算上传图片要显示的尺寸
        function computeSize(aspectRatio, imgUrl) {
            let drawWidth, drawHeight;
            if (aspectRatio > 1) {
                // 图片更宽
                drawWidth = clipWrap.offsetWidth;
                drawHeight = drawWidth / aspectRatio;

            } else {
                // 图片更高
                drawHeight = clipWrap.offsetHeight;
                drawWidth = clipWrap.offsetHeight * aspectRatio;
            }
            clipImg.src = imgUrl
            clipImg.style.width = `${drawWidth}px`;
            clipImg.style.height = `${drawHeight}px`

        }

每次选择完图片后显示选择区域并默认居中。

 //裁剪选择块的位置居中
        function centerClipBox() {
            clipBox.style.left = `${(clipWrap.clientWidth - clipBox.clientWidth) / 2}px`;
            clipBox.style.top = `${(clipWrap.clientHeight - clipBox.clientHeight) / 2}px`;
            clipBox.style.display = 'block';
        }

2、实现拖拽功能,用拖拽选择区域来选择内容

只有在鼠标按下 mousedown 事件才允许拖拽,鼠标松开 mouseup 禁止拖拽,再鼠标移动事件 mousemove 中计算拖拽的位置。并添加范围限制。

// 拖拽选择裁剪区域
const clipWrap = document.querySelector('.clip-wrap');
const clipImg = document.querySelector('.clip-img');
const clipBox = document.querySelector('.clip-box');
let isTtrue = false;
let initX;
let initY;

//初始化裁剪选择块的位置
centerClipBox()
//裁剪选择块的位置居中
function centerClipBox() {
    clipBox.style.left = `${(clipWrap.clientWidth - clipBox.clientWidth) / 2}px`;
    clipBox.style.top = `${(clipWrap.clientHeight - clipBox.clientHeight) / 2}px`;
    clipBox.style.display = 'block';
}
clipBox.addEventListener('mousedown', (e) => {
    isTtrue = true;
    initX = e.clientX - clipWrap.offsetLeft - clipBox.offsetLeft;
    initY = e.clientY - clipWrap.offsetLeft - clipBox.offsetTop;
})

//鼠标移动选择裁剪区域,并添加节流优化
document.addEventListener('mousemove', (e) => {
    if (isTtrue) {
        moveX = e.clientX - initX;
        let moveY = e.clientY - initY;
        let maxX = clipImg.offsetWidth - clipBox.offsetWidth + clipImg.offsetLeft;
        let maxY = clipImg.offsetHeight - clipBox.offsetHeight + clipImg.offsetTop;
        let minX = clipImg.offsetLeft;
        let minY = clipImg.offsetTop;
        moveX = Math.max(minX, Math.min(moveX, maxX));
        moveY = Math.max(minY, Math.min(moveY, maxY));
        clipBox.style.left = moveX + 'px';
        clipBox.style.top = moveY + 'px';
    }
})
document.addEventListener('mouseup', () => {
    isTtrue = false;
});

 3、计算选择区域在图片上的位置和尺寸

drawImage函数绘制裁剪后的图片,并将其显示在页面上。首先获取裁剪框和图片的位置和尺寸信息,然后根据这些信息绘制裁剪后的图片到Canvas上。最后通过 canvas.toBlob方法将Canvas内容转换为Blob对象,创建图片URL并设置给裁剪后的图片元素。

function drawImage() {
    // 获取裁剪框和图片的位置和尺寸信息
    const clipRect = clipBox.getBoundingClientRect();
    const imgRect = clipImg.getBoundingClientRect();

    const scaleX = clipImg.naturalWidth / imgRect.width;
    const scaleY = clipImg.naturalHeight / imgRect.height;

    const cropX = (clipRect.left - imgRect.left) * scaleX;
    const cropY = (clipRect.top - imgRect.top) * scaleY;
    const cropWidth = clipBox.clientWidth * scaleX;
    const cropHeight = clipBox.clientHeight * scaleY;

    // 调整画布尺寸
    canvas.width = cropWidth;
    canvas.height = cropHeight;

    // 绘制裁剪后的图片
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(
        clipImg,
        cropX,
        cropY,
        cropWidth,
        cropHeight,
        0,
        0,
        canvas.width,
        canvas.height
    );

    // 将画布内容转换为Blob对象
    canvas.toBlob(function (blob) {
        // 创建图片URL并设置给裁剪后的图片元素
        const url = URL.createObjectURL(blob);
        clipAfterImg.src = url;
        imageBlob = blob;
    }, 'image/png');
}

通过 getBoundingClientRect () 获取图片和选择块的位置,再通过图片的原始尺寸和显示的尺寸算出宽高比,移动距离和 canvas 绘制的图片都乘以宽高比,算出从哪里开始裁剪和画布的尺寸大小。

5、再图片 onchange 和拖拽触发 mousemove 调用drawImage来绘制图片,并添加节流事件防止频繁触发。

//计算上传图片要显示的尺寸
function computeSize(aspectRatio, imgUrl) {
    let drawWidth, drawHeight;
    if (aspectRatio > 1) {
        // 图片更宽
        drawWidth = clipWrap.offsetWidth;
        drawHeight = drawWidth / aspectRatio;

    } else {
        // 图片更高
        drawHeight = clipWrap.offsetHeight;
        drawWidth = clipWrap.offsetHeight * aspectRatio;
    }
    clipImg.src = imgUrl
    clipImg.style.width = `${drawWidth}px`;
    clipImg.style.height = `${drawHeight}px`

    clipImg.onload = () => {
        // 在计算完大小后居中显示clipBox
        centerClipBox();
        drawImage()
    }
}

document.addEventListener('mousemove', throttle((e) => {
    if (isTtrue) {
        moveX = e.clientX - initX;
        let moveY = e.clientY - initY;
        let maxX = clipImg.offsetWidth - clipBox.offsetWidth + clipImg.offsetLeft;
        let maxY = clipImg.offsetHeight - clipBox.offsetHeight + clipImg.offsetTop;
        let minX = clipImg.offsetLeft;
        let minY = clipImg.offsetTop;
        moveX = Math.max(minX, Math.min(moveX, maxX));
        moveY = Math.max(minY, Math.min(moveY, maxY));
        clipBox.style.left = moveX + 'px';
        clipBox.style.top = moveY + 'px';

        //裁剪区域移动重新绘制图片
        drawImage();
    }
}, 50));

//    节流
function throttle(fn, delay = 300) {
    let timer;
    return function (...args) {

        if (timer) return;
        timer = setTimeout(() => {
            fn(...args);
            timer = null;
        }, delay);
    }
}

 

效果

全部代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>

    <style>
        * {
            margin: 0;
        }

        body {
            color: #fff;
            background: rgba(0, 0, 0, 0.8);
        }

        .wrap {
            width: 500px;
            margin: 50px;
        }

        canvas {
            display: none;
            border: 1px solid red;
        }

        .upload {
            width: 150px;
            height: 30px;
            border: 1px solid;
            margin: 20px auto;
            position: relative;
        }

        .upload-file {
            position: absolute;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            opacity: 0;
            z-index: 1;
            cursor: pointer;
        }

        .upload-btn {
            position: absolute;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            text-align: center;
            line-height: 1.5;
            font-size: 20px;
            color: #fff;
            border-radius: 5px;
        }


        .clip-area {
            text-align: center;
        }

        .clip-wrap {
            display: none;
            width: 500px;
            height: 500px;
            background: #000;
            position: relative;
        }

        .clip-wrap .clip-img {
            position: absolute;
            left: 0;
            right: 0;
            bottom: 0;
            top: 0;
            margin: auto;
        }

        .clip-box {
            display: none;
            position: absolute;
            width: 300px;
            height: 300px;
            background: rgba(0, 0, 0, 0.5);
            box-sizing: border-box;
            border: 1px solid #fff;
        }


        .clip-after {
            text-align: center;
        }

        .clip-after h1 {
            margin: 20px 0;
        }

        .clip-after-img {
            width: 300px;
        }

        .save-btn {
            width: 100px;
            height: 30px;
            line-height: 30px;
            text-align: center;
            color: #fff;
            background: transparent;
            border: 1px solid #fff;
            cursor: pointer;
            margin-top: 20px;
        }
    </style>
</head>

<body>
    <div class="wrap">
        <div class="upload">
            <input type="file" class="upload-file">
            <div class="upload-btn">图片上传</div>
        </div>

        <!-- 显示上传的图片并选择裁剪区域 -->
        <div class="clip-area">
            <h1>裁剪区域</h1>
            <div class="clip-wrap">
                <img src="" class="clip-img" style="width: 300px;">
                <div class="clip-box"></div>
            </div>
        </div>


        <!-- 裁剪后的图片 -->
        <div class="clip-after">
            <h1> 裁剪后的图片</h1>
            <div class="clip-after-content">
                <img src="" class="clip-after-img">
            </div>
            <canvas id="canvas"></canvas>
            <button class="save-btn">确认</button>
        </div>
    </div>
    <script>
        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');
        canvas.width = 300;
        canvas.height = 300;
        //上传按钮和裁剪前显示的图片
        const uploadFile = document.querySelector('.upload-file');
        const clipAfterImg = document.querySelector('.clip-after-img');
        //保存按钮盒图片
        const saveBtn = document.querySelector('.save-btn')
        let imageBlob;

        // 拖拽选择裁剪区域参数
        const clipWrap = document.querySelector('.clip-wrap');
        const clipImg = document.querySelector('.clip-img');
        const clipBox = document.querySelector('.clip-box');
        let isTtrue = false;
        let initX;
        let initY;
        //上传图片
        uploadFile.onchange = (e) => {
            clipWrap.style.display = 'block';
            const fileData = e.target.files[0];
            const reader = new FileReader();
            const image = new Image();
            reader.readAsDataURL(fileData);
            // 异步读取文件内容,结果用data:url的字符串形式表示
            reader.onload = function (e) {
                const imgUrl = this.result;
                image.src = this.result;
                image.onload = function () {
                    // 计算图片绘制区域,确保图片保持比例
                    const aspectRatio = image.width / image.height;
                    computeSize(aspectRatio, imgUrl);
                };
            }
        }

        //计算上传图片要显示的尺寸
        function computeSize(aspectRatio, imgUrl) {
            let drawWidth, drawHeight;
            if (aspectRatio > 1) {
                // 图片更宽
                drawWidth = clipWrap.offsetWidth;
                drawHeight = drawWidth / aspectRatio;

            } else {
                // 图片更高
                drawHeight = clipWrap.offsetHeight;
                drawWidth = clipWrap.offsetHeight * aspectRatio;
            }
            clipImg.src = imgUrl
            clipImg.style.width = `${drawWidth}px`;
            clipImg.style.height = `${drawHeight}px`

            clipImg.onload = () => {
                // 在计算完大小后居中显示clipBox
                centerClipBox();
                drawImage()
            }

        }


        // 拖拽选择裁剪区域
        //初始化裁剪选择块的位置
        centerClipBox()
        //裁剪选择块的位置居中
        function centerClipBox() {
            clipBox.style.left = `${(clipWrap.clientWidth - clipBox.clientWidth) / 2}px`;
            clipBox.style.top = `${(clipWrap.clientHeight - clipBox.clientHeight) / 2}px`;
            clipBox.style.display = 'block';
        }
        clipBox.addEventListener('mousedown', (e) => {
            isTtrue = true;
            initX = e.clientX - clipWrap.offsetLeft - clipBox.offsetLeft;
            initY = e.clientY - clipWrap.offsetLeft - clipBox.offsetTop;
        })

        //鼠标移动选择裁剪区域,并添加节流优化
        document.addEventListener('mousemove', throttle((e) => {
            if (isTtrue) {
                moveX = e.clientX - initX;
                let moveY = e.clientY - initY;
                let maxX = clipImg.offsetWidth - clipBox.offsetWidth + clipImg.offsetLeft;
                let maxY = clipImg.offsetHeight - clipBox.offsetHeight + clipImg.offsetTop;
                let minX = clipImg.offsetLeft;
                let minY = clipImg.offsetTop;
                moveX = Math.max(minX, Math.min(moveX, maxX));
                moveY = Math.max(minY, Math.min(moveY, maxY));
                clipBox.style.left = moveX + 'px';
                clipBox.style.top = moveY + 'px';

                //裁剪区域移动重新绘制图片
                drawImage()
            }
        }, 50))
        document.addEventListener('mouseup', () => {
            isTtrue = false;
        });

        //在canvas上绘制选择的区域并转为图片
        function drawImage() {
            const clipRect = clipBox.getBoundingClientRect();
            const imgRect = clipImg.getBoundingClientRect();

            const scaleX = clipImg.naturalWidth / imgRect.width;
            const scaleY = clipImg.naturalHeight / imgRect.height;

            const cropX = (clipRect.left - imgRect.left) * scaleX;
            const cropY = (clipRect.top - imgRect.top) * scaleY;
            const cropWidth = clipBox.clientWidth * scaleX;
            const cropHeight = clipBox.clientHeight * scaleY;

            // 调整画布尺寸
            canvas.width = cropWidth;
            canvas.height = cropHeight;

            // 清空画布
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            // 将画布剪裁为圆形区域
            ctx.beginPath();
            ctx.arc(canvas.width / 2, canvas.height / 2, canvas.width / 2, 0, Math.PI * 2);
            ctx.closePath();
            ctx.clip();

            // 绘制图片
            ctx.drawImage(
                clipImg,
                cropX,
                cropY,
                cropWidth,
                cropHeight,
                0,
                0,
                canvas.width,
                canvas.height
            );

            // 将画布内容转换为图片
            canvas.toBlob(function (blob) {
                const url = URL.createObjectURL(blob);
                clipAfterImg.src = url;
                imageBlob = blob;
            }, 'image/png');
        }

        //图片上传
        saveBtn.addEventListener('click', () => {
            if (imageBlob) {
                const formData = new FormData();
                formData.append('image', imageBlob, 'image.png');

                fetch('/upload', {
                    method: 'POST',
                    body: formData,
                })
                    .then(response => response.json())
                    .then(data => {
                        console.log('Success:', data);
                    })
                    .catch((error) => {
                        console.error('Error:', error);
                    });
            } else {
                console.error('No image available to upload.');
            }
        });

        //    节流
        function throttle(fn, delay = 300) {
            let timer;
            return function (...args) {

                if (timer) return;
                timer = setTimeout(() => {
                    fn(...args);
                    timer = null;
                }, delay);
            }
        }
    </script>
</body>

</html>

 

posted @ 2024-06-12 01:36  雪旭  阅读(217)  评论(0编辑  收藏  举报