Files
InfoGenie/InfoGenie-frontend/public/toolbox/图片圆角处理/index.html
2025-12-13 20:53:50 +08:00

380 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html><html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>图片圆角处理</title>
<style>
:root{
--bg-from:#eaf8e4; /* 淡绿色 */
--bg-to:#f5ffd8; /* 淡黄绿色 */
--card:#ffffffcc; /* 卡片半透明 */
--accent:#78c67e; /* 绿色主色 */
--accent-2:#99d78c; /* 次要 */
--text:#234; /* 深色文字 */
--muted:#5b6b63; /* 次级文字 */
--shadow:0 8px 28px rgba(34, 102, 60, 0.15);
--radius:18px;
}html, body {
height: 100%;
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Noto Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft Yahei", sans-serif;
color: var(--text);
background: linear-gradient(160deg, var(--bg-from), var(--bg-to));
}
.wrap {
min-height: 100%;
display: grid;
place-items: start center;
padding: 18px 14px 28px;
}
.card {
width: 100%;
max-width: 520px; /* 适配手机竖屏 */
background: var(--card);
backdrop-filter: blur(8px);
border-radius: 22px;
box-shadow: var(--shadow);
padding: 16px;
}
header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
header .dot {
width: 10px; height: 10px; border-radius: 999px; background: var(--accent);
box-shadow: 0 0 0 6px rgba(120,198,126,0.15);
}
h1 { font-size: 18px; margin: 0; font-weight: 700; }
p.sub { margin: 4px 0 10px; color: var(--muted); font-size: 13px; }
.uploader {
border: 1.5px dashed #a9d6ab;
border-radius: var(--radius);
background: #ffffffb3;
padding: 12px;
display: flex; flex-direction: column; gap: 10px; align-items: center; justify-content: center;
}
.uploader input[type=file] {
width: 100%;
border: none; outline: none; background: transparent;
}
.controls { margin-top: 12px; display: grid; gap: 12px; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.slider {
background: #ffffff;
border: 1px solid #e3f2e1;
border-radius: var(--radius);
padding: 10px;
box-shadow: 0 4px 14px rgba(0,0,0,0.05);
}
.slider label { display:flex; align-items:center; justify-content:space-between; gap:6px; font-size: 14px; font-weight:600; }
.slider output { font-variant-numeric: tabular-nums; color: var(--accent); min-width: 3ch; text-align: right; }
input[type=range] {
-webkit-appearance: none; appearance: none; width: 100%; height: 32px; background: transparent;
}
input[type=range]::-webkit-slider-runnable-track { height: 8px; background: linear-gradient(90deg, #cfeecf, #e9ffd4); border-radius: 999px; }
input[type=range]::-moz-range-track { height: 8px; background: linear-gradient(90deg, #cfeecf, #e9ffd4); border-radius: 999px; }
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none; width: 22px; height: 22px; border-radius: 50%; background: var(--accent);
border: 2px solid #fff; margin-top: -7px; box-shadow: 0 2px 8px rgba(37,106,63,.3);
}
input[type=range]::-moz-range-thumb { width: 22px; height: 22px; border-radius: 50%; background: var(--accent); border: 2px solid #fff; box-shadow: 0 2px 8px rgba(37,106,63,.3); }
.row { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.row .chk { display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--muted); }
.preview {
margin-top: 12px;
background: #ffffff;
border: 1px solid #e3f2e1;
border-radius: 24px;
overflow: hidden;
position: relative;
}
canvas { width: 100%; height: auto; display: block; background: repeating-conic-gradient(from 45deg, #f8fff1 0 10px, #f1ffe4 10px 20px); }
.actions { display:flex; gap:10px; margin-top: 12px; }
button, .btn {
appearance: none; border: none; cursor: pointer; font-weight: 700; letter-spacing: .2px; transition: transform .05s ease, box-shadow .2s ease, background .2s ease;
border-radius: 14px; padding: 12px 14px; box-shadow: 0 6px 18px rgba(120,198,126,.25);
}
.btn-primary { background: linear-gradient(180deg, var(--accent), var(--accent-2)); color: #fff; }
.btn-ghost { background: #ffffffb5; color: #2c4432; border: 1px solid #d9efda; }
button:active { transform: translateY(1px); }
footer { text-align:center; color: #6a7; font-size: 12px; margin-top: 10px; }
.hidden { display:none; }
</style>
</head>
<body>
<div class="wrap">
<div class="card" role="region" aria-label="图片圆角处理工具">
<header>
<div class="dot" aria-hidden="true"></div>
<h1>图片圆角处理(四角独立,最高至圆形)</h1>
</header>
<p class="sub">上传图片 → 调节四个角的圆角强度0100%)→ 预览并下载透明圆角 PNG。已针对手机竖屏优化。</p><div class="uploader" aria-label="上传图片">
<input id="file" type="file" accept="image/*" />
<small style="color:var(--muted);">支持 JPG / PNG / WebP 等常见格式</small>
</div>
<div class="controls">
<div class="row">
<label class="chk"><input type="checkbox" id="linkAll" checked /> 联动四角</label>
<button id="resetBtn" class="btn btn-ghost" type="button">重置</button>
</div>
<div class="grid-2">
<div class="slider">
<label>左上角 <output id="o_tl">20%</output></label>
<input id="r_tl" type="range" min="0" max="100" step="1" value="20" />
</div>
<div class="slider">
<label>右上角 <output id="o_tr">20%</output></label>
<input id="r_tr" type="range" min="0" max="100" step="1" value="20" />
</div>
<div class="slider">
<label>右下角 <output id="o_br">20%</output></label>
<input id="r_br" type="range" min="0" max="100" step="1" value="20" />
</div>
<div class="slider">
<label>左下角 <output id="o_bl">20%</output></label>
<input id="r_bl" type="range" min="0" max="100" step="1" value="20" />
</div>
</div>
</div>
<div class="preview" aria-live="polite">
<canvas id="previewCanvas" aria-label="预览画布"></canvas>
</div>
<div class="actions">
<button id="downloadBtn" class="btn btn-primary" type="button" disabled>下载处理后的 PNG</button>
<button id="fitBtn" class="btn btn-ghost" type="button" disabled>适配预览尺寸</button>
</div>
<footer>小贴士:将四个角都拉到 <b>100%</b>,在方形图片上会得到完全圆形效果。</footer>
</div>
</div> <!-- 脚本:处理圆角、预览与下载 --> <script>
const fileInput = document.getElementById('file');
const linkAll = document.getElementById('linkAll');
const previewCanvas = document.getElementById('previewCanvas');
const downloadBtn = document.getElementById('downloadBtn');
const fitBtn = document.getElementById('fitBtn');
const resetBtn = document.getElementById('resetBtn');
const sliders = {
tl: document.getElementById('r_tl'),
tr: document.getElementById('r_tr'),
br: document.getElementById('r_br'),
bl: document.getElementById('r_bl'),
};
const outputs = {
tl: document.getElementById('o_tl'),
tr: document.getElementById('o_tr'),
br: document.getElementById('o_br'),
bl: document.getElementById('o_bl'),
};
// 工作画布(按原图尺寸绘制,导出用)
const workCanvas = document.createElement('canvas');
const workCtx = workCanvas.getContext('2d');
const prevCtx = previewCanvas.getContext('2d');
let img = new Image();
let imageLoaded = false;
const state = {
percent: { tl: 20, tr: 20, br: 20, bl: 20 },
fitToPreview: false,
};
function clamp(v, min, max){ return Math.max(min, Math.min(max, v)); }
function updateOutputs(){
for(const k of ['tl','tr','br','bl']) outputs[k].textContent = state.percent[k] + '%';
}
function setAllPercents(v){ for(const k of ['tl','tr','br','bl']) state.percent[k] = v; updateSliders(); }
function updateSliders(){ for(const k in sliders) sliders[k].value = state.percent[k]; updateOutputs(); render(); }
// 根据百分比换算到像素半径(以较短边的一半为 100%
function percentToRadiusPx(p){
const base = Math.min(img.naturalWidth, img.naturalHeight) / 2; // 100% 对应的像素半径
return (clamp(p,0,100) / 100) * base;
}
// 绘制带四角独立圆角的路径
function roundedRectPath(ctx, x, y, w, h, r){
// 约束:每个角半径不能超过对应边长度的一半
const rTL = clamp(r.tl, 0, Math.min(w, h) / 2);
const rTR = clamp(r.tr, 0, Math.min(w, h) / 2);
const rBR = clamp(r.br, 0, Math.min(w, h) / 2);
const rBL = clamp(r.bl, 0, Math.min(w, h) / 2);
ctx.beginPath();
ctx.moveTo(x + rTL, y);
ctx.lineTo(x + w - rTR, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + rTR);
ctx.lineTo(x + w, y + h - rBR);
ctx.quadraticCurveTo(x + w, y + h, x + w - rBR, y + h);
ctx.lineTo(x + rBL, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - rBL);
ctx.lineTo(x, y + rTL);
ctx.quadraticCurveTo(x, y, x + rTL, y);
ctx.closePath();
}
// 渲染到工作画布(原尺寸)
function renderWork(){
if(!imageLoaded) return;
workCanvas.width = img.naturalWidth;
workCanvas.height = img.naturalHeight;
workCtx.clearRect(0,0,workCanvas.width, workCanvas.height);
workCtx.save();
const r = {
tl: percentToRadiusPx(state.percent.tl),
tr: percentToRadiusPx(state.percent.tr),
br: percentToRadiusPx(state.percent.br),
bl: percentToRadiusPx(state.percent.bl),
};
roundedRectPath(workCtx, 0, 0, workCanvas.width, workCanvas.height, r);
workCtx.clip();
workCtx.drawImage(img, 0, 0, workCanvas.width, workCanvas.height);
workCtx.restore();
}
// 渲染到预览画布(自适应容器宽度,保持清晰度)
function renderPreview(){
const container = previewCanvas.parentElement.getBoundingClientRect();
const targetW = Math.min(container.width, 1000);
const scale = targetW / workCanvas.width;
const dpr = window.devicePixelRatio || 1;
const canvasW = Math.round(targetW * dpr);
const canvasH = Math.round(workCanvas.height * scale * dpr);
previewCanvas.width = canvasW;
previewCanvas.height = canvasH;
previewCanvas.style.height = Math.round(canvasH / dpr) + 'px';
previewCanvas.style.width = Math.round(canvasW / dpr) + 'px';
prevCtx.clearRect(0,0,canvasW,canvasH);
prevCtx.imageSmoothingEnabled = true;
prevCtx.drawImage(workCanvas, 0, 0, canvasW, canvasH);
}
function render(){
if(!imageLoaded) return;
renderWork();
renderPreview();
}
// 事件:上传图片
fileInput.addEventListener('change', (e)=>{
const file = e.target.files && e.target.files[0];
if(!file) return;
const url = URL.createObjectURL(file);
const temp = new Image();
temp.onload = ()=>{
img = temp;
imageLoaded = true;
render();
downloadBtn.disabled = false;
fitBtn.disabled = false;
};
temp.onerror = ()=>{
alert('图片加载失败,请更换文件重试');
imageLoaded = false;
downloadBtn.disabled = true;
fitBtn.disabled = true;
};
temp.src = url;
});
// 事件:滑块变更
for(const key of Object.keys(sliders)){
sliders[key].addEventListener('input', (e)=>{
const val = parseInt(e.target.value, 10) || 0;
if(linkAll.checked){
setAllPercents(val);
}else{
state.percent[key] = val;
updateOutputs();
render();
}
});
}
// 重置
resetBtn.addEventListener('click', ()=>{
linkAll.checked = true;
setAllPercents(20);
});
// 适配预览尺寸(导出较小尺寸,便于快速分享)
fitBtn.addEventListener('click', ()=>{
if(!imageLoaded) return;
const container = previewCanvas.parentElement.getBoundingClientRect();
const targetW = Math.min(container.width, 1080); // 限制到 1080 宽
const scale = targetW / img.naturalWidth;
const targetH = Math.round(img.naturalHeight * scale);
// 临时缩放导出画布
const temp = document.createElement('canvas');
temp.width = Math.round(targetW);
temp.height = Math.round(targetH);
const tctx = temp.getContext('2d');
// 先把当前工作画布绘好
renderWork();
tctx.drawImage(workCanvas, 0, 0, temp.width, temp.height);
const a = document.createElement('a');
a.href = temp.toDataURL('image/png');
a.download = 'rounded-image-fit.png';
a.click();
});
// 下载原始尺寸 PNG
downloadBtn.addEventListener('click', ()=>{
if(!imageLoaded) return;
renderWork();
const a = document.createElement('a');
a.href = workCanvas.toDataURL('image/png');
a.download = 'rounded-image.png';
a.click();
});
// 初始输出数字
updateOutputs();
// 自适应:窗口尺寸变动时重绘预览
window.addEventListener('resize', ()=>{ if(imageLoaded) renderPreview(); });
// 支持 PWA 风:阻止 iOS 双击缩放(改善滑块体验)
let lastTouch = 0;
document.addEventListener('touchend', (e)=>{
const now = Date.now();
if(now - lastTouch <= 300){ e.preventDefault(); }
lastTouch = now;
}, {passive:false});
</script></body>
</html>