<!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>
旋转矫正与交互操作
您可以通过多种方式精确调整图片角度,并实时查看效果。

- 角度控制:您可以直接在输入框中输入精确的角度值,或拖动滑块进行快速微调,页面还提供了“左旋90°”和“右旋90°”按钮,方便进行直角旋转。
- 核心流程:点击“选择图片”上传您的图片后,所有旋转操作都会在画布上实时渲染,您可以通过“归零”按钮快速重置角度。
- 结果导出:调整满意后,点击“下载矫正后图片”即可将当前旋转结果保存为PNG格式的图片文件。
优化建议: 您可以根据需要调整 maxDim 变量(第125行附近)的值,以控制画布显示的最大尺寸,适应不同分辨率的图片。
标签: 角度矫正
版权声明:除非特别标注,否则均为本站原创文章,转载时请以链接形式注明文章出处。