图片裁剪工具按比例裁剪

联启 电脑工具 18
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">图片裁剪工具 · 按比例裁剪</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        body {
            font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
            background: #f2f4f8;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            padding: 16px;
        }
        .card {
            background: white;
            border-radius: 24px;
            box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
            padding: 24px 28px 32px;
            max-width: 780px;
            width: 100%;
            transition: all 0.2s;
        }
        h1 {
            font-size: 1.6rem;
            font-weight: 600;
            letter-spacing: -0.01em;
            display: flex;
            align-items: center;
            gap: 8px;
            color: #1e293b;
            margin-bottom: 8px;
        }
        .sub {
            color: #64748b;
            font-size: 0.9rem;
            margin-bottom: 20px;
            border-left: 4px solid #3b82f6;
            padding-left: 14px;
            background: #f8fafc;
            border-radius: 0 8px 8px 0;
            line-height: 1.4;
        }
        .upload-area {
            display: flex;
            flex-wrap: wrap;
            gap: 16px;
            align-items: center;
            margin-bottom: 20px;
            background: #f9fafb;
            padding: 12px 16px;
            border-radius: 60px;
            border: 1px dashed #cbd5e1;
        }
        .upload-btn {
            background: #3b82f6;
            color: white;
            border: none;
            padding: 10px 22px;
            border-radius: 40px;
            font-weight: 500;
            font-size: 0.95rem;
            cursor: pointer;
            transition: 0.2s;
            display: inline-flex;
            align-items: center;
            gap: 8px;
            box-shadow: 0 4px 8px rgba(59, 130, 246, 0.2);
        }
        .upload-btn:hover {
            background: #2563eb;
            transform: scale(1.02);
        }
        .upload-btn:active {
            transform: scale(0.97);
        }
        .file-name {
            color: #334155;
            font-size: 0.95rem;
            font-weight: 450;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            max-width: 180px;
            flex: 1;
        }
        .ratio-control {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            gap: 12px 18px;
            margin-bottom: 24px;
            background: #f1f5f9;
            padding: 12px 18px;
            border-radius: 40px;
        }
        .ratio-group {
            display: flex;
            align-items: center;
            gap: 6px;
            flex-wrap: wrap;
        }
        .ratio-group label {
            font-size: 0.9rem;
            font-weight: 500;
            color: #1e293b;
            background: white;
            padding: 4px 14px;
            border-radius: 30px;
            border: 1px solid #e2e8f0;
            cursor: pointer;
            transition: all 0.15s;
            box-shadow: 0 1px 3px rgba(0,0,0,0.02);
        }
        .ratio-group input[type="radio"] {
            display: none;
        }
        .ratio-group input[type="radio"]:checked + label {
            background: #3b82f6;
            color: white;
            border-color: #3b82f6;
            box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
        }
        .custom-ratio {
            display: flex;
            align-items: center;
            gap: 6px;
            background: white;
            padding: 2px 8px 2px 14px;
            border-radius: 40px;
            border: 1px solid #e2e8f0;
        }
        .custom-ratio input {
            width: 52px;
            padding: 6px 4px;
            border: none;
            background: transparent;
            font-weight: 500;
            font-size: 0.9rem;
            text-align: center;
            outline: none;
            color: #0f172a;
        }
        .custom-ratio input:focus {
            background: #f0f4ff;
            border-radius: 20px;
        }
        .custom-ratio span {
            color: #64748b;
            font-weight: 400;
            font-size: 1.1rem;
        }
        .canvas-wrapper {
            position: relative;
            background: #0f172a;
            border-radius: 20px;
            overflow: hidden;
            margin-bottom: 18px;
            box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05);
        }
        #imageCanvas {
            display: block;
            width: 100%;
            height: auto;
            max-height: 460px;
            background: #1e293b;
            object-fit: contain;
            touch-action: none;
            cursor: crosshair;
        }
        .action-bar {
            display: flex;
            flex-wrap: wrap;
            justify-content: space-between;
            align-items: center;
            gap: 12px;
            margin-top: 6px;
        }
        .action-btn-group {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
        }
        .btn {
            background: white;
            border: 1px solid #d1d9e6;
            padding: 8px 20px;
            border-radius: 40px;
            font-weight: 500;
            color: #1e293b;
            font-size: 0.9rem;
            cursor: pointer;
            transition: 0.15s;
            display: inline-flex;
            align-items: center;
            gap: 6px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.02);
        }
        .btn-primary {
            background: #3b82f6;
            border: 1px solid #3b82f6;
            color: white;
            box-shadow: 0 4px 10px rgba(59, 130, 246, 0.3);
        }
        .btn-primary:hover {
            background: #2563eb;
            border-color: #2563eb;
        }
        .btn-primary:active {
            transform: scale(0.96);
        }
        .btn:active {
            transform: scale(0.96);
        }
        .btn:disabled {
            opacity: 0.5;
            pointer-events: none;
            filter: grayscale(0.3);
        }
        .download-hint {
            color: #475569;
            font-size: 0.85rem;
            background: #f1f5f9;
            padding: 4px 16px;
            border-radius: 30px;
            white-space: nowrap;
        }
        .footer-note {
            margin-top: 18px;
            text-align: center;
            font-size: 0.75rem;
            color: #94a3b8;
        }
        @media (max-width: 500px) {
            .card { padding: 16px; }
            .upload-area { flex-direction: column; align-items: stretch; border-radius: 28px; }
            .file-name { max-width: 100%; }
            .ratio-control { flex-direction: column; align-items: stretch; gap: 8px; }
            .action-bar { flex-direction: column; align-items: stretch; }
            .action-btn-group { justify-content: center; }
            .download-hint { text-align: center; }
        }
    </style>
</head>
<body>
<div class="card">
    <h1>✂️ 比例裁剪</h1>
    <div class="sub">拖拽鼠标 / 手指移动 · 自动保持宽高比</div>
    <!-- 上传 & 比例 -->
    <div class="upload-area">
        <label for="imageUpload" class="upload-btn">📁 选择图片</label>
        <input type="file" id="imageUpload" accept="image/png, image/jpeg, image/webp, image/bmp" style="display:none;">
        <span class="file-name" id="fileName">未选择文件</span>
    </div>
    <div class="ratio-control">
        <div class="ratio-group">
            <input type="radio" name="ratio" id="ratioFree" value="free" checked>
            <label for="ratioFree">🔄 自由</label>
            <input type="radio" name="ratio" id="ratio1_1" value="1:1">
            <label for="ratio1_1">1:1</label>
            <input type="radio" name="ratio" id="ratio4_3" value="4:3">
            <label for="ratio4_3">4:3</label>
            <input type="radio" name="ratio" id="ratio16_9" value="16:9">
            <label for="ratio16_9">16:9</label>
            <input type="radio" name="ratio" id="ratio3_2" value="3:2">
            <label for="ratio3_2">3:2</label>
        </div>
        <div class="custom-ratio">
            <span>✏️</span>
            <input type="number" id="ratioW" value="4" min="0.1" step="0.1" placeholder="宽">
            <span>:</span>
            <input type="number" id="ratioH" value="3" min="0.1" step="0.1" placeholder="高">
        </div>
    </div>
    <!-- Canvas 容器 -->
    <div class="canvas-wrapper">
        <canvas id="imageCanvas" width="640" height="400"></canvas>
    </div>
    <!-- 操作栏 -->
    <div class="action-bar">
        <div class="action-btn-group">
            <button class="btn" id="resetBtn">↺ 重置</button>
            <button class="btn btn-primary" id="cropBtn">✂️ 裁剪并下载</button>
        </div>
        <span class="download-hint">⬇️ 点击裁剪自动保存</span>
    </div>
    <div class="footer-note">拖拽选区 · 支持触屏操作</div>
</div>
<script>
    (function() {
        // DOM 元素
        const canvas = document.getElementById('imageCanvas');
        const ctx = canvas.getContext('2d');
        const fileInput = document.getElementById('imageUpload');
        const fileName = document.getElementById('fileName');
        const resetBtn = document.getElementById('resetBtn');
        const cropBtn = document.getElementById('cropBtn');
        // 比例单选
        const ratioRadios = document.querySelectorAll('input[name="ratio"]');
        const ratioWInput = document.getElementById('ratioW');
        const ratioHInput = document.getElementById('ratioH');
        // 状态
        let image = null;                // 原始图片 (HTMLImageElement)
        let imageLoaded = false;
        // 选区 (归一化 0~1 相对于画布显示区域)
        let sel = { x: 0.1, y: 0.1, w: 0.8, h: 0.8 };
        // 拖拽状态
        let isDragging = false;
        let dragType = ''; // 'move' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' | 'edge'
        let dragStart = { x: 0, y: 0 };
        let dragSel = { x: 0, y: 0, w: 0, h: 0 };
        // 画布尺寸 (显示尺寸)
        let canvasDisplayWidth = 0;
        let canvasDisplayHeight = 0;
        // 初次重置选区到合理位置
        function resetSelection() {
            if (!imageLoaded || !image) {
                // 默认选区
                sel.x = 0.05; sel.y = 0.05; sel.w = 0.9; sel.h = 0.9;
                drawCanvas();
                return;
            }
            // 根据图片比例调整初始选区 (占满 85% 区域)
            const imgAspect = image.naturalWidth / image.naturalHeight;
            const dispAspect = canvasDisplayWidth / canvasDisplayHeight;
            let w, h;
            if (imgAspect > dispAspect) {
                w = 0.9;
                h = w / (imgAspect / dispAspect);
            } else {
                h = 0.9;
                w = h * (imgAspect / dispAspect);
            }
            // 居中
            sel.x = (1 - w) / 2;
            sel.y = (1 - h) / 2;
            sel.w = Math.min(w, 0.95);
            sel.h = Math.min(h, 0.95);
            clampSelection();
            drawCanvas();
        }
        // 将选区限制在画布内
        function clampSelection() {
            if (sel.x < 0) sel.x = 0;
            if (sel.y < 0) sel.y = 0;
            if (sel.x + sel.w > 1) sel.w = 1 - sel.x;
            if (sel.y + sel.h > 1) sel.h = 1 - sel.y;
            if (sel.w < 0.01) sel.w = 0.01;
            if (sel.h < 0.01) sel.h = 0.01;
        }
        // 获取当前比例约束 (返回值: [wRatio, hRatio] 或 null 表示自由)
        function getCurrentRatio() {
            for (const radio of ratioRadios) {
                if (radio.checked) {
                    const val = radio.value;
                    if (val === 'free') return null;
                    if (val === '1:1') return [1, 1];
                    if (val === '4:3') return [4, 3];
                    if (val === '16:9') return [16, 9];
                    if (val === '3:2') return [3, 2];
                }
            }
            // 自定义 (如果自由未选中则读取自定义)
            const w = parseFloat(ratioWInput.value);
            const h = parseFloat(ratioHInput.value);
            if (isNaN(w) || isNaN(h) || w <= 0 || h <= 0) return null;
            return [w, h];
        }
        // 应用比例约束 (修改 sel 原地)
        function applyRatioConstraint() {
            const ratio = getCurrentRatio();
            if (!ratio) return; // 自由模式
            const [rw, rh] = ratio;
            const currentAspect = sel.w / sel.h;
            const targetAspect = rw / rh;
            // 以中心为锚点调整宽/高
            const cx = sel.x + sel.w / 2;
            const cy = sel.y + sel.h / 2;
            let newW, newH;
            if (currentAspect > targetAspect) {
                // 过宽: 缩小宽度
                newW = sel.h * targetAspect;
                newH = sel.h;
            } else {
                // 过高: 缩小高度
                newH = sel.w / targetAspect;
                newW = sel.w;
            }
            sel.w = Math.min(newW, 1);
            sel.h = Math.min(newH, 1);
            sel.x = Math.max(0, Math.min(1 - sel.w, cx - sel.w / 2));
            sel.y = Math.max(0, Math.min(1 - sel.h, cy - sel.h / 2));
            clampSelection();
        }
        // ---------- 绘图 ----------
        function drawCanvas() {
            const rect = canvas.getBoundingClientRect();
            canvasDisplayWidth = rect.width;
            canvasDisplayHeight = rect.height;
            // 设置画布实际分辨率 (2倍缩放提升清晰度)
            const dpr = window.devicePixelRatio || 1;
            const drawW = canvasDisplayWidth * dpr;
            const drawH = canvasDisplayHeight * dpr;
            if (canvas.width !== drawW || canvas.height !== drawH) {
                canvas.width = drawW;
                canvas.height = drawH;
            }
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            // 背景 (暗色)
            ctx.fillStyle = '#1e293b';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            if (!imageLoaded || !image) {
                // 显示提示文字
                ctx.fillStyle = '#94a3b8';
                ctx.font = `${18 * dpr}px system-ui, sans-serif`;
                ctx.textAlign = 'center';
                ctx.fillText('请选择一张图片', canvas.width/2, canvas.height/2 - 10);
                return;
            }
            // 绘制图片 (保持宽高比适应画布)
            const imgW = image.naturalWidth;
            const imgH = image.naturalHeight;
            const cw = canvas.width, ch = canvas.height;
            const scale = Math.min(cw / imgW, ch / imgH);
            const offsetX = (cw - imgW * scale) / 2;
            const offsetY = (ch - imgH * scale) / 2;
            ctx.drawImage(image, offsetX, offsetY, imgW * scale, imgH * scale);
            // 绘制遮罩 + 选区
            // 选区坐标转换为 canvas 像素
            const sx = offsetX + sel.x * (imgW * scale);
            const sy = offsetY + sel.y * (imgH * scale);
            const sw = sel.w * (imgW * scale);
            const sh = sel.h * (imgH * scale);
            // 遮罩 (半透明黑)
            ctx.fillStyle = 'rgba(0,0,0,0.45)';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            // 擦出选区 (使用 'destination-out' 需要复合模式,改用 clearRect 但需保留背景? 更简单:使用globalCompositeOperation)
            ctx.save();
            ctx.globalCompositeOperation = 'destination-out';
            ctx.fillStyle = 'rgba(0,0,0,1)';
            ctx.fillRect(sx, sy, sw, sh);
            ctx.restore();
            // 绘制亮色边框
            ctx.strokeStyle = '#ffffff';
            ctx.lineWidth = 2 * dpr;
            ctx.setLineDash([]);
            ctx.strokeRect(sx, sy, sw, sh);
            // 绘制四个角柄
            const handleSize = 10 * dpr;
            const handles = [
                [sx, sy, 'nw'], [sx+sw, sy, 'ne'], 
                [sx, sy+sh, 'sw'], [sx+sw, sy+sh, 'se']
            ];
            ctx.fillStyle = '#3b82f6';
            ctx.strokeStyle = 'white';
            ctx.lineWidth = 1.5 * dpr;
            for (let [hx, hy] of handles) {
                ctx.fillRect(hx - handleSize/2, hy - handleSize/2, handleSize, handleSize);
                ctx.strokeRect(hx - handleSize/2, hy - handleSize/2, handleSize, handleSize);
            }
            // 如果图片加载完成,更新文件名字
        }
        // 重绘 & 尺寸自适应
        function redraw() {
            if (!canvasDisplayWidth || !canvasDisplayHeight) {
                const rect = canvas.getBoundingClientRect();
                canvasDisplayWidth = rect.width;
                canvasDisplayHeight = rect.height;
            }
            drawCanvas();
        }
        // ---------- 图片加载 ----------
        function loadImage(file) {
            if (!file) return;
            const reader = new FileReader();
            reader.onload = (e) => {
                const img = new Image();
                img.onload = () => {
                    image = img;
                    imageLoaded = true;
                    fileName.textContent = file.name;
                    // 重置选区
                    resetSelection();
                    redraw();
                };
                img.onerror = () => {
                    alert('图片加载失败,请选择有效图片文件。');
                };
                img.src = e.target.result;
            };
            reader.readAsDataURL(file);
        }
        fileInput.addEventListener('change', (e) => {
            if (e.target.files.length > 0) {
                loadImage(e.target.files[0]);
            }
        });
        // ---------- 鼠标/触屏 交互 ----------
        function getCanvasCoords(e) {
            const rect = canvas.getBoundingClientRect();
            let clientX, clientY;
            if (e.touches) {
                clientX = e.touches[0].clientX;
                clientY = e.touches[0].clientY;
                e.preventDefault();
            } else {
                clientX = e.clientX;
                clientY = e.clientY;
            }
            return {
                x: (clientX - rect.left) / rect.width,
                y: (clientY - rect.top) / rect.height
            };
        }
        function handlePointerDown(e) {
            if (!imageLoaded) return;
            const pos = getCanvasCoords(e);
            const px = pos.x, py = pos.y;
            // 检查是否在角柄 (像素判断需要转换)
            const rect = canvas.getBoundingClientRect();
            const scaleX = canvas.width / rect.width;
            const scaleY = canvas.height / rect.height;
            const offX = (canvas.width - image.naturalWidth * Math.min(canvas.width/image.naturalWidth, canvas.height/image.naturalHeight)) / 2;
            const offY = (canvas.height - image.naturalHeight * Math.min(canvas.width/image.naturalWidth, canvas.height/image.naturalHeight)) / 2;
            const imgScale = Math.min(canvas.width / image.naturalWidth, canvas.height / image.naturalHeight);
            // 计算手柄位置 (像素)
            const sx = offX + sel.x * (image.naturalWidth * imgScale);
            const sy = offY + sel.y * (image.naturalHeight * imgScale);
            const sw = sel.w * (image.naturalWidth * imgScale);
            const sh = sel.h * (image.naturalHeight * imgScale);
            const handleSize = 10 * (window.devicePixelRatio || 1);
            const cx = (clientXFromEvent(e) - rect.left) * (canvas.width / rect.width);
            const cy = (clientYFromEvent(e) - rect.top) * (canvas.height / rect.height);
            function clientXFromEvent(ev) {
                return ev.touches ? ev.touches[0].clientX : ev.clientX;
            }
            function clientYFromEvent(ev) {
                return ev.touches ? ev.touches[0].clientY : ev.clientY;
            }
            // 检查角手柄 (按像素)
            const corners = [
                { x: sx, y: sy, type: 'topLeft' },
                { x: sx+sw, y: sy, type: 'topRight' },
                { x: sx, y: sy+sh, type: 'bottomLeft' },
                { x: sx+sw, y: sy+sh, type: 'bottomRight' }
            ];
            for (const corner of corners) {
                if (Math.abs(cx - corner.x) < handleSize && Math.abs(cy - corner.y) < handleSize) {
                    isDragging = true;
                    dragType = corner.type;
                    dragStart = { x: px, y: py };
                    dragSel = { ...sel };
                    return;
                }
            }
            // 检查是否在选区内 (移动)
            if (px >= sel.x && px <= sel.x + sel.w && py >= sel.y && py <= sel.y + sel.h) {
                isDragging = true;
                dragType = 'move';
                dragStart = { x: px, y: py };
                dragSel = { ...sel };
                return;
            }
            // 点击外部: 重新选区 (可选)
            // 为了简化,点击外部不重置,但可做备用
        }
        function handlePointerMove(e) {
            if (!isDragging || !imageLoaded) return;
            e.preventDefault();
            const pos = getCanvasCoords(e);
            const dx = pos.x - dragStart.x;
            const dy = pos.y - dragStart.y;
            const newSel = { ...dragSel };
            if (dragType === 'move') {
                newSel.x = dragSel.x + dx;
                newSel.y = dragSel.y + dy;
                // 边界限制
                if (newSel.x < 0) newSel.x = 0;
                if (newSel.y < 0) newSel.y = 0;
                if (newSel.x + newSel.w > 1) newSel.x = 1 - newSel.w;
                if (newSel.y + newSel.h > 1) newSel.y = 1 - newSel.h;
                sel = newSel;
                clampSelection();
                drawCanvas();
                return;
            }
            // 角缩放
            const ratio = getCurrentRatio();
            let rw = ratio ? ratio[0] : null;
            let rh = ratio ? ratio[1] : null;
            const processCorner = (fixedX, fixedY, freeX, freeY) => {
                let rawW, rawH;
                if (freeX) {
                    rawW = dragSel.w + dx;
                } else {
                    rawW = dragSel.w - dx;
                }
                if (freeY) {
                    rawH = dragSel.h + dy;
                } else {
                    rawH = dragSel.h - dy;
                }
                if (rawW < 0.01) rawW = 0.01;
                if (rawH < 0.01) rawH = 0.01;
                let newW = rawW, newH = rawH;
                if (rw && rh) {
                    // 锁定比例: 根据拖拽方向决定以宽为准还是高为准
                    const targetAspect = rw / rh;
                    // 尝试以宽为准
                    let tryW = rawW;
                    let tryH = rawW / targetAspect;
                    if (tryH > 0 && tryH < 1.5) {
                        newW = tryW; newH = tryH;
                    } else {
                        newH = rawH;
                        newW = rawH * targetAspect;
                    }
                }
                // 计算固定点
                let newX = fixedX ? dragSel.x : (dragSel.x + dragSel.w - newW);
                let newY = fixedY ? dragSel.y : (dragSel.y + dragSel.h - newH);
                // 边界修正
                if (newX < 0) { newX = 0; if (rw && rh) newW = Math.min(newW, 1); }
                if (newY < 0) { newY = 0; if (rw && rh) newH = Math.min(newH, 1); }
                if (newX + newW > 1) { newX = 1 - newW; }
                if (newY + newH > 1) { newY = 1 - newH; }
                sel.x = newX; sel.y = newY; sel.w = newW; sel.h = newH;
                clampSelection();
            };
            switch (dragType) {
                case 'topLeft': processCorner(true, true, false, false); break;
                case 'topRight': processCorner(false, true, true, false); break;
                case 'bottomLeft': processCorner(true, false, false, true); break;
                case 'bottomRight': processCorner(false, false, true, true); break;
                default: break;
            }
            drawCanvas();
        }
        function handlePointerUp(e) {
            if (isDragging) {
                isDragging = false;
                dragType = '';
                // 保证比例约束 (如果是自由则跳过)
                applyRatioConstraint();
                drawCanvas();
            }
        }
        // 注册事件
        canvas.addEventListener('mousedown', handlePointerDown);
        window.addEventListener('mousemove', handlePointerMove);
        window.addEventListener('mouseup', handlePointerUp);
        canvas.addEventListener('touchstart', handlePointerDown, { passive: false });
        window.addEventListener('touchmove', handlePointerMove, { passive: false });
        window.addEventListener('touchend', handlePointerUp);
        // 比例切换重绘
        for (const radio of ratioRadios) {
            radio.addEventListener('change', () => {
                if (imageLoaded) {
                    applyRatioConstraint();
                    drawCanvas();
                }
            });
        }
        ratioWInput.addEventListener('input', () => {
            if (imageLoaded) {
                // 如果自由模式,不强制;但若自定义比例,需要检查
                const ratio = getCurrentRatio();
                if (ratio) {
                    applyRatioConstraint();
                    drawCanvas();
                }
            }
        });
        ratioHInput.addEventListener('input', () => {
            if (imageLoaded) {
                const ratio = getCurrentRatio();
                if (ratio) {
                    applyRatioConstraint();
                    drawCanvas();
                }
            }
        });
        // 重置
        resetBtn.addEventListener('click', () => {
            if (imageLoaded) {
                resetSelection();
                applyRatioConstraint();
                drawCanvas();
            } else {
                sel = { x: 0.05, y: 0.05, w: 0.9, h: 0.9 };
                drawCanvas();
            }
        });
        // 裁剪下载
        cropBtn.addEventListener('click', () => {
            if (!imageLoaded || !image) {
                alert('请先加载一张图片');
                return;
            }
            // 计算裁剪区域 (基于原始图片)
            const imgW = image.naturalWidth;
            const imgH = image.naturalHeight;
            // 计算画布中图片的绘制区域 (同drawCanvas)
            const cw = canvas.width, ch = canvas.height;
            const scale = Math.min(cw / imgW, ch / imgH);
            const offsetX = (cw - imgW * scale) / 2;
            const offsetY = (ch - imgH * scale) / 2;
            // 选区在原始图片上的像素坐标
            const cropX = Math.round((sel.x * (imgW * scale)) / scale);
            const cropY = Math.round((sel.y * (imgH * scale)) / scale);
            const cropW = Math.round((sel.w * (imgW * scale)) / scale);
            const cropH = Math.round((sel.h * (imgH * scale)) / scale);
            // 边界保护
            const safeX = Math.max(0, Math.min(cropX, imgW - 1));
            const safeY = Math.max(0, Math.min(cropY, imgH - 1));
            const safeW = Math.min(cropW, imgW - safeX);
            const safeH = Math.min(cropH, imgH - safeY);
            if (safeW < 2 || safeH < 2) {
                alert('选区太小,请调整裁剪区域');
                return;
            }
            // 创建临时canvas
            const tempCanvas = document.createElement('canvas');
            tempCanvas.width = safeW;
            tempCanvas.height = safeH;
            const tempCtx = tempCanvas.getContext('2d');
            tempCtx.drawImage(image, safeX, safeY, safeW, safeH, 0, 0, safeW, safeH);
            // 下载
            const link = document.createElement('a');
            link.download = 'cropped_image.png';
            link.href = tempCanvas.toDataURL('image/png');
            link.click();
        });
        // 窗口变化自适应
        let resizeTimer;
        window.addEventListener('resize', () => {
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(() => {
                if (imageLoaded) redraw();
                else drawCanvas();
            }, 100);
        });
        // 初始绘制
        drawCanvas();
        // 如果有默认图片? 无
        // 设置默认自定义比例为4:3
        ratioWInput.value = 4;
        ratioHInput.value = 3;
    })();
</script>
</body>
</html>

功能与操作方法

您可以通过简单的拖拽和选择,快速实现图片的精确裁剪,其核心操作流程如下:

图片裁剪工具按比例裁剪-第1张图片-电脑手机工具软件下载 - 免费实用工具合集 | 联启科技

  1. 加载图片:点击“选择图片”按钮上传您需要裁剪的图片。
  2. 设置比例:您可以在上方选择预设比例(如1:1、4:3)或自定义宽高比,选区将自动遵循所选比例。
  3. 调整选区:在图片上拖拽鼠标或手指来移动选区,或拖拽选区的四个角来调整大小,选区会始终保持您设定的宽高比。
  4. 完成裁剪:调整满意后,点击“裁剪并下载”按钮,系统将按照选区范围生成并保存裁剪后的图片。

标签: 比例裁剪

抱歉,评论功能暂时关闭!