canvas+cropper实现头像裁剪功能

        开发工作中,常常会遇到需要对图片进行二次处理的一些功能,常见的就有上传头像时,需要对选择的图片进行二次裁剪,包括截图时,也可以对截取的图片继续进行一些操作。常用到的工具网站切图编辑器大致就长这样。

但全用画布去绘画的话,难免工作量会很大。于是我引入cropper.js插件,配合canvas们就可以轻松实现图中的所有功能。以下是代码。

页面引入

<link rel="stylesheet" href="https://unpkg.com/cropperjs/dist/cropper.css">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
    <script src="https://unpkg.com/cropperjs/dist/cropper.js"></script>

html

<div class="container">
        <canvas id="panl-canvas" width="150" height="300" style="display: none;"></canvas>
        <div class="upload-box">
            <!-- 回显上传图片 -->
            <div class="upload-img-box">
                <img src="" alt="">
            </div>
            <div class="upload-btn">
                <button type="button" class="btn-box">choose image</button>
                <input id="select-box" type="file" style="display: none;" />
            </div>
        </div>
        <!-- 裁剪图片弹窗 -->
        <div class="module-cropper" style="display: none;">
            <div class="module-cropper-content">
                <div class="module-cropper-bg">
                    <!-- 包装图像或画布元素 -->
                    <div class="cropper-img-box">
                        <img id="cropperImg" src="" />
                    </div>
                </div>

            </div>
        </div>
    </div>
    <div class="module-cropper-btn">
        <button id="rotate90" onclick="rotateCropper(90)">90度旋转</button>
        <button id="rotateHor" onclick="flipCropperH()">水平翻转</button>
        <button id="rotateVer" onclick="flipCropperV()">垂直翻转</button>
        <button id="rotateFree" onclick="$('#panl-canvas').toggle()">自由旋转</button>
        <button id="cropFree" class="crop-btn" data-value="">自由裁剪</button>
        <button id="crop1_1" class="crop-btn">1:1</button>
        <button id="crop2_3" class="crop-btn">2:3</button>
        <button id="crop3_2" class="crop-btn">3:2</button>
        <button id="crop3_4" class="crop-btn">3:4</button>
        <button id="crop4_3" class="crop-btn">4:3</button>
        <button id="crop9_16" class="crop-btn">9:16</button>
        <button id="crop16_9" class="crop-btn">16:9</button>
        <button id="exportButton" onclick="cropperSucess()">导出</button>
    </div>

css

<style>
        * {
            margin: 0;
            padding: 0;
        }

        .container {
            width: 500px;
            height: 500px;
            margin: 50px auto;
            position: relative;
        }

        #panl-canvas {
            position: absolute;
            left: -200px;
            top: 0;
        }

        .upload-box {
            width: 100%;
            overflow: hidden;
            position: absolute;
            top: 40%;
            left: 50%;
            transform: translate(-50%, -50%);
        }

        .upload-img-box {
            width: 80%;
            margin: 20px auto;
            padding: 20px;
            box-sizing: border-box;
            border: 1px solid #ddd;
            border-radius: 5px;
        }

        .upload-img-box img {
            width: 100%;
        }

        .upload-btn {
            width: 80%;
            margin: 0 auto;
            overflow: hidden;
            text-align: center;
        }

        .btn-box {
            width: 120px;
            background: #2DCEC2;
            line-height: 35px;
            text-align: center;
            color: #fff;
            border-radius: 5px;
            border: none;
            outline-style: none;
        }

        .module-cropper {
            width: 100%;
            height: 100%;
            position: absolute;
            top: 0;
            left: 0;
            background: rgba(0, 0, 0, .8);
        }

        #cropperImg {
            max-width: 100%;
        }

        .module-cropper-content {
            width: 100%;
            height: 100%;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
        }

        .module-cropper-bg {
            width: 100%;
            height: 100%;
            overflow: hidden;
            position: relative;
        }

        .cropper-img-box {
            position: relative;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 100%;
            height: 100%;
        }

        .module-cropper-btn {
            width: 100%;
            display: flex;
            align-items: center;
            margin-top: 100px;
        }

        .module-cropper-btn button {
            width: 120px;
            height: 50px;
            background: #2DCEC2;
            line-height: 50px;
            text-align: center;
            color: #fff;
            border-radius: 5px;
            border: none;
            outline-style: none;
            margin-left: 20px;
        }
    </style>

js

<script>
        const image = document.getElementById('cropperImg');// 包装图像或画布元素
        let options = {
            aspectRatio: NaN, // 裁剪框的宽高比,默认NAN,可以随意改变裁剪框的宽高比
            viewMode: 0,  // 0,1,2,3
            dragMode: 'move', // 'crop': 可以产生一个新的裁剪框 'move': 只可以移动 'none': 什么也不处理
            // preview:".small",  // 添加额外的元素(容器)以供预览
            responsive: true, //在调整窗口大小的时候重新渲染cropper,默认为true
            restore: true, // 调整窗口大小后恢复裁剪的区域。
            checkCrossOrigin: true, //检查当前图像是否为跨域图像,默认为true
            modal: true, // 显示图片上方的黑色模态并在裁剪框下面,默认为true
            guides: true, // 显示在裁剪框里面的虚线,默认为true
            center: true, // 裁剪框在图片正中心,默认为true
            highlight: true, // 在裁剪框上方显示白色的区域,默认为true
            background: true, // 显示容器的网格背景(即马赛克背景),默认为true,若为false,这不显示
            autoCrop: true, // 当初始化时,显示裁剪框,改成false裁剪框消失需要你重绘裁剪区域,默认为true
            autoCropArea: 1, // 定义自动裁剪面积大小(百分比)和图片进行对比,默认为0.8
            movable: true, // 是否允许可以移动后面的图片,默认为true(但是如果dragMode为crop,由于和重绘裁剪框冲突,所以移动图片会失效)
            rotatable: true, // 是否允许旋转图像,默认为true
            scalable: true, // 是否允许缩放图像,默认为true
            zoomable: true, // 是否允许放大图像,默认为true
            zoomOnTouch: true, // 是否可以通过拖动触摸来放大图像,默认为true
            wheelZoomRatio: 0.1, // 用鼠标移动图像时,定义缩放比例,默认0.1
            cropBoxMovable: true, // 是否通过拖拽来移动剪裁框,默认为true
            cropBoxResizable: true, // 是否通过拖动来调整剪裁框的大小,默认为true
            toggleDragModeOnDblclick: true, // 当点击两次时可以在“crop”和“move”之间切换拖拽模式,默认为true
            crop: function (event) {
                // let zoom = calculateZoom(-(rotation / Math.PI) * 180)
                // console.log(zoom, 'zoomzoomzoomzoom');
                // cropper.zoomTo(zoom)
            }
        }
        let cropper = new Cropper(image, options); // 初始化cropper对象
        //选择图片
        $(".btn-box").click(function () {
            $('#select-box').click()
        })
        // input事件
        $('#select-box').on('change', function (e) {
            let file = e.target.files[0];
            let reader = new FileReader();
            reader.onload = function (evt) {
                let replaceSrc = evt.target.result;
                // 更换cropper的图片
                cropper.replace(replaceSrc, false);
            }
            reader.readAsDataURL(file);
            $(".module-cropper").show();
        })

        // 旋转图片
        function rotateCropper(deg) {
            cropper.rotate(deg);
        }

        // 水平翻转
        function flipCropperH() {
            cropper.scaleX(-cropper.getData().scaleX);
            console.log(cropper.getCanvasData(), 'cropper.getCanvasData()cropper.getCanvasData()');
        }

        // 垂直翻转
        function flipCropperV() {
            cropper.scaleY(-cropper.getData().scaleY);
        }

        // 图片选择完成
        function cropperSucess() {
            let baseSrc = cropper.getCroppedCanvas().toDataURL('image/jpeg', 0.7);
            $(".module-cropper").hide();
            $(".upload-img-box").find("img").attr("src", baseSrc)
            // 创建一个隐藏的链接元素
            const link = document.createElement('a');
            link.href = baseSrc;
            link.download = 'cropped_image.jpg';
            link.click();
            URL.revokeObjectURL(link.href);
        }

        //比例裁剪
        $('.crop-btn').on('click', function () {
            const str = $(this).attr('id');
            const match = str.match(/\d+/g);
            if (match && match.length >= 2) {
                const firstNumber = match[0]; // 第一个数字
                const secondNumber = match[1]; // 第二个数字
                console.log(firstNumber, secondNumber);
                const aspectRatio = firstNumber / secondNumber;
                cropper.setAspectRatio(aspectRatio);
            } else {
                cropper.setAspectRatio(NaN);
            }
        })


        //自由旋转
        const canvas = document.getElementById("panl-canvas")
        let height = canvas.height
        let width = canvas.width
        let graduateWidth = 10 // 刻度的长度

        let ctx = canvas.getContext('2d')
        let reqFrame = requestAnimationFrame || setTimeout
        let allRotate = 0 // 旋转弧度
        const circleRadius = 300 // 大圆半径
        const circleBorderDistance = 70 // 距离边上距离
        const circlePoint = {
            x: width - circleRadius - circleBorderDistance,
            y: height / 2
        }

        function calcPoint(r, a) {
            x1 = 0 + r * Math.cos(a) // 0表示圆心位置
            y1 = 0 + r * Math.sin(a)
            return [x1, y1]
        }

        function drawTick(ctx) {
            let maxDeg = 2 * Math.PI
            let tick = maxDeg / 18

            ctx.beginPath()
            ctx.lineCap = "round"
            for (let item = 0; item < 18; item++) {
                if (allRotate > 0 && [6, 7, 8].indexOf(item) !== -1) {
                    continue
                } else if (allRotate < 0 && [10, 11, 12].indexOf(item) !== -1) {
                    continue
                }
                ctx.save()
                ctx.translate(circlePoint.x, circlePoint.y);
                ctx.rotate(allRotate)
                let deg = tick * item
                let innerline = circleRadius - graduateWidth
                ctx.strokeStyle = '#aaa'
                let start = calcPoint(innerline, deg) // 刻度开始点
                let end = calcPoint(circleRadius, deg) // 刻度结束点

                ctx.moveTo(...start)
                ctx.lineTo(...end)
                ctx.restore()
            }
            ctx.stroke()


        }

        function drawText(ctx) {
            let maxDeg = 2 * Math.PI
            let tick = maxDeg / 18
            let outerline = height * 0.8 / 2
            const textPoint = { // 默认0的位置   
                x: circleRadius - 30,
                y: 4
            }
            ctx.beginPath()
            ctx.fillStyle = '#000';
            for (let item = 0; item < 18; item++) {
                ctx.save()
                ctx.translate(circlePoint.x, circlePoint.y);
                ctx.rotate(tick * item + allRotate)
                ctx.strokeStyle = '#000'
                let num = item * 20 > 180 ? item * 20 - 360 : item * 20
                if (allRotate >= 0) {
                    num = num === 180 ? -180 : num
                    num < 60 && (ctx.fillText(num, textPoint.x, textPoint.y))
                } else if (allRotate < 0) {
                    num > -60 && (ctx.fillText(num, textPoint.x, textPoint.y))
                }

                ctx.restore()
            }

            ctx.stroke()
        }

        function drawCircles(ctx) {

            ctx.beginPath();
            ctx.arc(circlePoint.x, circlePoint.y, circleRadius, 0, 2 * Math.PI); // 边框圆
            ctx.stroke();

            ctx.beginPath();
            ctx.arc(circlePoint.x + circleRadius, circlePoint.y, 5, 0, 2 * Math.PI); // 当前值的圆
            ctx.fill();
        }

        function initPanl(ctx, width, height) {
            ctx.clearRect(0, 0, width, height)
            drawTick(ctx) // 刻度
            drawText(ctx) // 数字
            drawCircles(ctx) // 两个圆
        }

        let lastRotateValue = 0; // 记录上一次的角度值

        canvas.addEventListener('mousedown', event => {
            let startX = event.clientX;
            let startY = event.clientY;

            const windowMouseMoveFun = e => {
                const distanceY = (e.clientY - startY) / 5; // 除5控制表盘滑动快慢
                const rotateValue = (allRotate / Math.PI) * 180;
                allRotate += distanceY * 2 * Math.PI / 360;
                allRotate = Math.min(Math.PI, Math.max(-Math.PI, allRotate)); // 限制角度范围在 -π 到 π 之间
                reqFrame(initPanl.bind(null, ctx, width, height));
                startY = e.clientY;

                // 计算角度差值
                const currentRotateValue = -(allRotate / Math.PI) * 180;
                const angleDifference = currentRotateValue - lastRotateValue;

                // 

                if (!image) return;
                rotation = allRotate; // 设置角度
                rotateCropper(angleDifference); // 应用角度变化
                let zoom = calculateZoom(-(rotation / Math.PI) * 180)
                // 1. 获取实际图像尺寸
                const imageData = cropper.getImageData();
                const actualWidth = imageData.naturalWidth;
                const actualHeight = imageData.naturalHeight;

                // 2. 获取显示尺寸(例如容器的宽度和高度)
                const containerWidth = cropper.getContainerData().width;
                const containerHeight = cropper.getContainerData().height;

                // 3. 根据实际尺寸和显示尺寸计算缩放比例
                const zoomRatioW = containerWidth / actualWidth;
                const zoomRatioH = containerHeight / actualHeight;

                const FinalZoom = zoomRatioW < zoomRatioH ? zoomRatioW : zoomRatioH
                // 4. 更新 Cropper 的缩放
                cropper.zoomTo(zoom * FinalZoom)
                lastRotateValue = currentRotateValue; // 更新上一次的角度值
            }

            const windowMouseUpFun = e => {
                window.removeEventListener('mousemove', windowMouseMoveFun);
                window.removeEventListener('mouseup', windowMouseUpFun);
            }

            window.addEventListener('mousemove', windowMouseMoveFun);
            window.addEventListener('mouseup', windowMouseUpFun);
        });




        initPanl(ctx, width, height)


        function calculateZoom(angle) {
            // 计算绝对值
            const absAngle = Math.abs(angle);
            // 将角度限制在0到90度之间
            const clampedAngle = Math.max(0, Math.min(absAngle, 180));
            const mappedAngle = clampedAngle <= 90 ? clampedAngle : 90 - (clampedAngle - 90);
            return mappedAngle / 90 + 1;
        }
    </script>

附上cropper.js官方地址首页 | Cropper.js

(第一次发表文章,欢迎吐槽,多有不足,一起进步)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值