<!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行附近)的值,以控制画布显示的最大尺寸,适应不同分辨率的图片。
标签: 角度矫正
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播