图片旋转工具任意角度矫正

联启 电脑工具 15
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">图片旋转工具 · 任意角度矫正</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        body {
            background: #f2f5f9;
            font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            padding: 20px;
        }
        .card {
            max-width: 960px;
            width: 100%;
            background: white;
            border-radius: 32px;
            box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08), 0 8px 20px rgba(0, 0, 0, 0.06);
            padding: 28px 30px 35px;
            transition: all 0.2s ease;
        }
        h1 {
            font-size: 1.9rem;
            font-weight: 600;
            display: flex;
            align-items: center;
            gap: 10px;
            color: #0b1a33;
            letter-spacing: -0.02em;
            margin-bottom: 8px;
        }
        .sub {
            color: #5b6f87;
            margin-bottom: 24px;
            border-left: 4px solid #3b82f6;
            padding-left: 14px;
            font-weight: 450;
            font-size: 1rem;
            background: #f0f4fe;
            border-radius: 0 12px 12px 0;
            line-height: 1.4;
            padding: 10px 16px;
        }
        .toolbar {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            gap: 16px 20px;
            background: #f8fafc;
            padding: 16px 20px;
            border-radius: 40px;
            margin-bottom: 28px;
            border: 1px solid #e9edf3;
        }
        .angle-group {
            display: flex;
            align-items: center;
            gap: 10px;
            flex-wrap: wrap;
        }
        .angle-group label {
            font-weight: 500;
            color: #1e2a44;
            font-size: 0.95rem;
        }
        .angle-input {
            width: 80px;
            padding: 8px 6px;
            border: 1px solid #cdd7e6;
            border-radius: 40px;
            background: white;
            font-weight: 500;
            text-align: center;
            font-size: 1rem;
            transition: 0.2s;
        }
        .angle-input:focus {
            border-color: #3b82f6;
            outline: none;
            box-shadow: 0 0 0 3px rgba(59,130,246,0.25);
        }
        .slider {
            flex: 1;
            min-width: 140px;
            max-width: 280px;
            height: 6px;
            -webkit-appearance: none;
            background: linear-gradient(to right, #cbd5e1, #3b82f6 50%, #cbd5e1);
            border-radius: 10px;
            outline: none;
        }
        .slider::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 20px;
            height: 20px;
            background: white;
            border-radius: 50%;
            border: 2px solid #3b82f6;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
            cursor: pointer;
            transition: 0.1s;
        }
        .slider::-webkit-slider-thumb:hover {
            transform: scale(1.1);
            background: #f0f7ff;
        }
        .slider::-moz-range-thumb {
            width: 20px;
            height: 20px;
            background: white;
            border-radius: 50%;
            border: 2px solid #3b82f6;
            cursor: pointer;
        }
        .action-buttons {
            display: flex;
            gap: 12px;
            margin-left: auto;
            flex-wrap: wrap;
        }
        .btn {
            background: white;
            border: 1px solid #d0d9e8;
            padding: 8px 18px;
            border-radius: 40px;
            font-weight: 500;
            font-size: 0.9rem;
            color: #1a253b;
            cursor: pointer;
            transition: all 0.15s;
            display: inline-flex;
            align-items: center;
            gap: 6px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.02);
        }
        .btn-primary {
            background: #1d4ed8;
            border: 1px solid #1d4ed8;
            color: white;
            box-shadow: 0 6px 12px rgba(29,78,216,0.2);
        }
        .btn-primary:hover {
            background: #2563eb;
            border-color: #2563eb;
            transform: translateY(-1px);
            box-shadow: 0 10px 18px rgba(29,78,216,0.25);
        }
        .btn-outline {
            background: transparent;
            border: 1px solid #bcc7db;
        }
        .btn-outline:hover {
            background: #eef3fc;
            border-color: #8ca3c0;
        }
        .btn-success {
            background: #0f973d;
            border: 1px solid #0f973d;
            color: white;
            box-shadow: 0 4px 10px rgba(15,151,61,0.25);
        }
        .btn-success:hover {
            background: #16a34a;
            border-color: #16a34a;
            transform: scale(1.02);
        }
        .upload-area {
            display: flex;
            align-items: center;
            gap: 12px;
            margin-top: 6px;
            flex-wrap: wrap;
        }
        .file-label {
            background: #eef2f7;
            padding: 10px 24px;
            border-radius: 40px;
            font-weight: 500;
            color: #1e293b;
            cursor: pointer;
            border: 1px dashed #a0b8d6;
            transition: 0.2s;
            display: inline-block;
        }
        .file-label:hover {
            background: #e2eaf5;
            border-color: #3b82f6;
        }
        .file-label input {
            display: none;
        }
        .canvas-wrapper {
            position: relative;
            background: #ebeef5;
            border-radius: 28px;
            overflow: hidden;
            margin: 18px 0 22px;
            box-shadow: inset 0 4px 12px rgba(0,0,0,0.02);
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 380px;
            border: 1px solid #dce2ec;
        }
        #imageCanvas {
            display: block;
            width: 100%;
            height: auto;
            max-height: 70vh;
            object-fit: contain;
            background: repeating-conic-gradient(#f0f2f5 0% 25%, #ffffff 0% 50%) 0px 0px / 24px 24px;
            transition: box-shadow 0.1s;
            border-radius: 24px;
        }
        .status-tip {
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 8px;
            margin-top: 6px;
            color: #39506e;
            font-size: 0.9rem;
        }
        .coord-info {
            background: #f4f7fc;
            padding: 6px 16px;
            border-radius: 30px;
            font-weight: 450;
        }
        hr {
            margin: 18px 0 10px;
            border: none;
            border-top: 1px solid #e2e9f2;
        }
        .footer-note {
            display: flex;
            justify-content: flex-end;
            font-size: 0.85rem;
            color: #6f84a0;
        }
        @media (max-width: 640px) {
            .card { padding: 18px 16px; }
            .toolbar { flex-direction: column; align-items: stretch; gap: 12px; }
            .action-buttons { margin-left: unset; justify-content: flex-end; }
            .slider { max-width: 100%; }
        }
    </style>
</head>
<body>
<div class="card">
    <h1>
        <span>🔄 图片旋转</span>
        <span style="font-size: 1rem; font-weight: 400; color: #51718c; margin-left: auto;">任意角度矫正</span>
    </h1>
    <div class="sub">⚡ 拖动滑块 / 输入角度 · 实时预览 · 一键保存矫正结果</div>
    <!-- 工具栏 -->
    <div class="toolbar">
        <div class="angle-group">
            <label for="angleInput">角度</label>
            <input type="number" id="angleInput" class="angle-input" value="0" step="0.1" min="-360" max="360">
            <span style="color:#577394; font-size:0.9rem;">°</span>
        </div>
        <input type="range" id="angleSlider" class="slider" min="-180" max="180" value="0" step="0.1">
        <div class="action-buttons">
            <button class="btn btn-outline" id="resetBtn">↺ 归零</button>
            <button class="btn btn-primary" id="rotateLeftBtn">↺ 左旋 90°</button>
            <button class="btn btn-primary" id="rotateRightBtn">↻ 右旋 90°</button>
        </div>
    </div>
    <!-- 上传 + 下载 -->
    <div class="upload-area">
        <label for="fileInput" class="file-label">📁 选择图片</label>
        <input type="file" id="fileInput" accept="image/*">
        <button class="btn btn-success" id="downloadBtn">⬇ 下载矫正后图片</button>
        <span style="margin-left: auto; font-size:0.9rem; color:#3f5a7a;" id="fileNameDisplay">未选择文件</span>
    </div>
    <!-- 画布区 -->
    <div class="canvas-wrapper">
        <canvas id="imageCanvas" width="800" height="500"></canvas>
    </div>
    <!-- 状态栏 -->
    <div class="status-tip">
        <span>🖼️ 当前角度: <strong id="angleDisplay">0.0°</strong></span>
        <span class="coord-info" id="imageSizeDisplay">图片尺寸: -</span>
    </div>
    <hr />
    <div class="footer-note">
        <span>💡 拖动滑块微调 · 支持任意角度</span>
    </div>
</div>
<script>
    (function() {
        // DOM 引用
        const canvas = document.getElementById('imageCanvas');
        const ctx = canvas.getContext('2d');
        const fileInput = document.getElementById('fileInput');
        const fileNameDisplay = document.getElementById('fileNameDisplay');
        const angleInput = document.getElementById('angleInput');
        const angleSlider = document.getElementById('angleSlider');
        const angleDisplay = document.getElementById('angleDisplay');
        const imageSizeDisplay = document.getElementById('imageSizeDisplay');
        const resetBtn = document.getElementById('resetBtn');
        const rotateLeftBtn = document.getElementById('rotateLeftBtn');
        const rotateRightBtn = document.getElementById('rotateRightBtn');
        const downloadBtn = document.getElementById('downloadBtn');
        // 状态
        let sourceImage = null;          // 原始图片 (HTMLImageElement)
        let currentAngle = 0;           // 当前旋转角度 (度)
        let isInternalUpdate = false;   // 防止循环更新
        // ---------- 核心绘制函数 (任意角度) ----------
        function renderImage(angle) {
            if (!sourceImage) {
                // 没有图片时清空画布,显示提示信息
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                ctx.fillStyle = '#d7dee9';
                ctx.font = '18px system-ui, sans-serif';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText('请点击上方按钮选择一张图片', canvas.width/2, canvas.height/2);
                imageSizeDisplay.textContent = '图片尺寸: -';
                return;
            }
            const img = sourceImage;
            const angleRad = (angle * Math.PI) / 180;
            // 计算旋转后的画布尺寸 (保证完整包含图片)
            const imgW = img.width;
            const imgH = img.height;
            const cos = Math.abs(Math.cos(angleRad));
            const sin = Math.abs(Math.sin(angleRad));
            const newWidth = Math.ceil(imgW * cos + imgH * sin);
            const newHeight = Math.ceil(imgW * sin + imgH * cos);
            // 更新画布尺寸 (保留原比例,但不超过1200px)
            const maxDim = 1200;
            let finalW = newWidth;
            let finalH = newHeight;
            if (finalW > maxDim || finalH > maxDim) {
                const scale = maxDim / Math.max(finalW, finalH);
                finalW = Math.ceil(finalW * scale);
                finalH = Math.ceil(finalH * scale);
            }
            // 确保最小尺寸
            if (finalW < 80) finalW = 80;
            if (finalH < 80) finalH = 80;
            canvas.width = finalW;
            canvas.height = finalH;
            // 清空 + 透明棋盘背景 (CSS已提供背景,但canvas内也需要透明)
            ctx.clearRect(0, 0, finalW, finalH);
            // 移至中心并旋转
            ctx.save();
            ctx.translate(finalW / 2, finalH / 2);
            ctx.rotate(angleRad);
            // 绘制图片 (中心对齐)
            ctx.drawImage(img, -imgW / 2, -imgH / 2, imgW, imgH);
            ctx.restore();
            // 更新尺寸显示
            imageSizeDisplay.textContent = `原始 ${imgW}×${imgH}  · 显示 ${finalW}×${finalH}`;
        }
        // ---------- 根据角度更新所有UI组件 + 重绘 ----------
        function updateAngle(value, updateSlider = true, updateInput = true) {
            // 规范化角度到 -360~360 便于显示 (但保留精确值)
            let angle = parseFloat(value);
            if (isNaN(angle)) angle = 0;
            // 限制范围 (更友好的输入)
            if (angle < -360) angle = -360;
            if (angle > 360) angle = 360;
            currentAngle = angle;
            // 更新显示
            angleDisplay.textContent = angle.toFixed(1) + '°';
            // 避免循环
            isInternalUpdate = true;
            if (updateInput) {
                angleInput.value = angle.toFixed(1);
            }
            if (updateSlider) {
                angleSlider.value = angle;
            }
            isInternalUpdate = false;
            // 重新绘制
            renderImage(angle);
        }
        // ---------- 重置/归零 ----------
        function resetAngle() {
            updateAngle(0);
        }
        // ---------- 加载图片 ----------
        function loadImageFromFile(file) {
            if (!file || !file.type.startsWith('image/')) {
                alert('请选择有效的图片文件');
                return;
            }
            fileNameDisplay.textContent = file.name;
            const reader = new FileReader();
            reader.onload = function(e) {
                const img = new Image();
                img.onload = function() {
                    sourceImage = img;
                    // 重置角度为0 (但保留当前角度?通常加载图片后角度归零更符合直觉)
                    resetAngle();
                };
                img.onerror = function() {
                    alert('图片加载失败,请重试');
                };
                img.src = e.target.result;
            };
            reader.readAsDataURL(file);
        }
        // ---------- 初始化 (显示缺省状态) ----------
        function initEmpty() {
            sourceImage = null;
            resetAngle(); // 会触发render -> 显示提示
        }
        // ---------- 事件绑定 ----------
        // 1. 文件选择
        fileInput.addEventListener('change', function(e) {
            const file = e.target.files[0];
            if (file) {
                loadImageFromFile(file);
            }
            // 清空input,允许重复选择同一文件
            fileInput.value = '';
        });
        // 2. 滑块
        angleSlider.addEventListener('input', function() {
            if (isInternalUpdate) return;
            const val = parseFloat(this.value) || 0;
            updateAngle(val, true, true);
        });
        // 3. 数字输入框
        angleInput.addEventListener('input', function() {
            if (isInternalUpdate) return;
            let val = parseFloat(this.value);
            if (isNaN(val)) val = 0;
            // 限制输入范围避免极端值
            if (val > 360) val = 360;
            if (val < -360) val = -360;
            updateAngle(val, true, true);
        });
        // 4. 归零按钮
        resetBtn.addEventListener('click', resetAngle);
        // 5. 左旋90
        rotateLeftBtn.addEventListener('click', function() {
            const newAngle = currentAngle - 90;
            updateAngle(newAngle, true, true);
        });
        // 6. 右旋90
        rotateRightBtn.addEventListener('click', function() {
            const newAngle = currentAngle + 90;
            updateAngle(newAngle, true, true);
        });
        // 7. 下载
        downloadBtn.addEventListener('click', function() {
            if (!sourceImage) {
                alert('请先选择一张图片');
                return;
            }
            // 用当前canvas数据
            const link = document.createElement('a');
            link.download = `旋转矫正_${currentAngle.toFixed(1)}°.png`;
            link.href = canvas.toDataURL('image/png');
            link.click();
        });
        // 额外:拖拽图片到画布(增强体验)
        const canvasWrapper = document.querySelector('.canvas-wrapper');
        canvasWrapper.addEventListener('dragover', function(e) {
            e.preventDefault();
            this.style.borderColor = '#3b82f6';
            this.style.background = '#e8f0fe';
        });
        canvasWrapper.addEventListener('dragleave', function(e) {
            e.preventDefault();
            this.style.borderColor = '#dce2ec';
            this.style.background = '';
        });
        canvasWrapper.addEventListener('drop', function(e) {
            e.preventDefault();
            this.style.borderColor = '#dce2ec';
            this.style.background = '';
            const files = e.dataTransfer.files;
            if (files.length > 0 && files[0].type.startsWith('image/')) {
                loadImageFromFile(files[0]);
                // 更新文件名称
                fileNameDisplay.textContent = files[0].name;
            } else {
                alert('请拖入图片文件');
            }
        });
        // 8. 键盘快捷键 (可选)
        document.addEventListener('keydown', function(e) {
            if (e.key === 'r' || e.key === 'R') {
                if (!e.ctrlKey && !e.metaKey) {
                    resetAngle();
                    e.preventDefault();
                }
            }
        });
        // 启动: 显示空白状态
        initEmpty();
        // 处理窗口resize:重绘 (主要是画布自适应保留)
        let resizeTimer;
        window.addEventListener('resize', function() {
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(() => {
                if (sourceImage) {
                    renderImage(currentAngle);
                }
            }, 100);
        });
        console.log('🖼️ 图片旋转工具已启动 · 任意角度矫正');
    })();
</script>
</body>
</html>

旋转矫正与交互操作

您可以通过多种方式精确调整图片角度,并实时查看效果。

图片旋转工具任意角度矫正-第1张图片-电脑手机工具软件下载 - 免费实用工具合集 | 联启科技

  • 角度控制:您可以直接在输入框中输入精确的角度值,或拖动滑块进行快速微调,页面还提供了“左旋90°”和“右旋90°”按钮,方便进行直角旋转。
  • 核心流程:点击“选择图片”上传您的图片后,所有旋转操作都会在画布上实时渲染,您可以通过“归零”按钮快速重置角度。
  • 结果导出:调整满意后,点击“下载矫正后图片”即可将当前旋转结果保存为PNG格式的图片文件。

优化建议: 您可以根据需要调整 maxDim 变量(第125行附近)的值,以控制画布显示的最大尺寸,适应不同分辨率的图片。

标签: 角度矫正

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