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

295 lines
8.8 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" />
<title>图片黑白处理</title>
<style>
:root{
--bg-1:#E9F9E7; /* 淡绿色 */
--bg-2:#F5FBE7; /* 淡黄绿色 */
--fg:#0f5132; /* 深一点的绿色文字 */
--muted:#5c7a66;
--card:#ffffffcc;
--accent:#6cc870;
--accent-2:#90d47f;
--shadow: 0 8px 24px rgba(39, 115, 72, .15);
--radius: 18px;
}
html,body{
height:100%;
}
body{
margin:0;
font-family: -apple-system,BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, "Noto Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft Yahei", sans-serif;
color:var(--fg);
background: linear-gradient(160deg,var(--bg-1) 0%, var(--bg-2) 100%);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display:flex; align-items:stretch; justify-content:center;
}
.wrap{
width:min(100%, 760px);
padding:16px;
}
header{
text-align:center;
margin: 8px 0 14px;
}
h1{
margin:0 0 6px;
font-weight:800;
letter-spacing:.3px;
font-size: clamp(20px, 4.5vw, 28px);
}
.sub{
margin:0 auto;
max-width: 32em;
color:var(--muted);
font-size: clamp(12px, 3.4vw, 14px);
}.card{
background: var(--card);
backdrop-filter: blur(6px) saturate(120%);
border: 1px solid rgba(108,200,112,.18);
border-radius: var(--radius);
padding: 14px;
box-shadow: var(--shadow);
}
.controls{
display:grid;
grid-template-columns: 1fr;
gap:12px;
}
.row{
display:flex; align-items:center; gap:10px; flex-wrap:wrap;
}
label{
font-weight:600; font-size:14px;
}
input[type="file"]{
width:100%;
padding:12px;
border: 1px dashed #a8d7a3;
border-radius: 14px;
background: #ffffffb0;
}
input[type="range"]{
width:100%;
accent-color: var(--accent);
height: 28px;
}
output{ font-variant-numeric: tabular-nums; min-width:3ch; text-align:right; display:inline-block; }
.buttons{
display:grid; grid-template-columns: 1fr 1fr; gap:10px;
}
button{
appearance:none; border:0; cursor:pointer;
padding:12px 14px; border-radius: 14px; font-weight:700;
background: linear-gradient(180deg, var(--accent), var(--accent-2));
color:white; box-shadow: 0 6px 16px rgba(16,123,62,.25);
}
button.secondary{
background:#eaf8ea; color:#245b35; box-shadow:none; border:1px solid #cfe9ce;
}
button:disabled{ opacity:.5; cursor:not-allowed; }
.preview{
margin-top: 12px;
display:grid; gap:10px;
}
.canvas-wrap{
background: repeating-linear-gradient( 45deg, #f3fbf0, #f3fbf0 14px, #eef8ec 14px, #eef8ec 28px);
border:1px solid #dcefd7; border-radius: 14px; overflow:hidden;
display:flex; align-items:center; justify-content:center;
min-height: 240px;
}
canvas{ max-width:100%; height:auto; display:block; }
.placeholder{ color:#7da287; padding:20px; text-align:center; }
footer{
text-align:center; color:var(--muted); font-size:12px; margin:14px auto 6px;
}
/* 更偏向手机竖屏的合理布局 */
@media (min-width: 720px){
.controls{ grid-template-columns: 1.2fr 1fr; align-items:end; }
.buttons{ grid-template-columns: 1fr 1fr; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>图片黑白处理 · 轻柔绿意版</h1>
<p class="sub">上传一张图片,拖动滑块设置<span style="font-weight:600">黑白程度</span>0% 原图 → 100% 全黑白),一键下载处理结果。完全本地处理,无需联网。</p>
</header><section class="card">
<div class="controls">
<div>
<label for="file">选择图片</label>
<input id="file" type="file" accept="image/*" />
</div>
<div>
<div class="row" style="justify-content:space-between">
<label for="amount">黑白程度</label>
<div><output id="val">100</output>%</div>
</div>
<input id="amount" type="range" min="0" max="100" step="1" value="100" />
<div class="buttons">
<button id="download" disabled>下载处理后的图片</button>
<button id="reset" class="secondary" disabled>重置</button>
</div>
</div>
</div>
<div class="preview">
<div class="canvas-wrap">
<canvas id="canvas" aria-label="预览画布"></canvas>
<div id="ph" class="placeholder">⬆️ 请选择一张图片开始…</div>
</div>
</div>
</section>
<footer>© 黑白处理在您的设备本地完成 · 支持手机竖屏友好显示</footer>
</div><script>
(() => {
const fileInput = document.getElementById('file');
const amount = document.getElementById('amount');
const valOut = document.getElementById('val');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const downloadBtn = document.getElementById('download');
const resetBtn = document.getElementById('reset');
const placeholder = document.getElementById('ph');
let originalPixels = null; // Uint8ClampedArray当前画布的原始像素用于反复处理
let imgNatural = { w: 0, h: 0 };
let currentFileName = 'processed.png';
let rafId = null;
// 将图像绘制到画布,并根据屏幕宽度进行适度缩放,避免超大图导致内存压力
const MAX_DIM = 4096; // 上限,兼顾清晰度与移动端内存
function drawImageToCanvas(img) {
// 处理超大图片:在不改变比例的前提下收敛到 MAX_DIM 以内
let w = img.naturalWidth;
let h = img.naturalHeight;
const scale = Math.min(1, MAX_DIM / Math.max(w, h));
w = Math.round(w * scale);
h = Math.round(h * scale);
// 为了保证下载清晰度canvas 使用实际像素尺寸;显示层用 CSS 自适应
canvas.width = w;
canvas.height = h;
ctx.clearRect(0,0,w,h);
ctx.drawImage(img, 0, 0, w, h);
// 保存原始像素用于反复应用不同程度
originalPixels = ctx.getImageData(0,0,w,h);
imgNatural = { w, h };
}
function lerp(a,b,t){ return a + (b-a) * t; }
// 将原图按给定强度转为黑白(灰度)
function applyGrayscale(strength01){
if(!originalPixels) return;
const { data: src } = originalPixels;
const copy = new ImageData(new Uint8ClampedArray(src), imgNatural.w, imgNatural.h);
const out = copy.data;
// 加权灰度(符合 sRGB 感知权重)
for(let i=0; i<out.length; i+=4){
const r = src[i], g = src[i+1], b = src[i+2];
const y = 0.2126*r + 0.7152*g + 0.0722*b;
out[i] = lerp(r, y, strength01);
out[i+1] = lerp(g, y, strength01);
out[i+2] = lerp(b, y, strength01);
// 保留 alpha
}
ctx.putImageData(copy, 0, 0);
}
// 防抖 + rAF流畅响应滑杆
function scheduleRender(){
if(rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
const t = Number(amount.value) / 100; // 0~1
valOut.textContent = amount.value;
applyGrayscale(t);
});
}
// 处理文件加载
fileInput.addEventListener('change', async (e) => {
const file = e.target.files && e.target.files[0];
if(!file) return;
currentFileName = (file.name ? file.name.replace(/\.[^.]+$/, '') : 'processed') + '.png';
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
placeholder.style.display = 'none';
drawImageToCanvas(img);
// 初始按当前滑杆值处理
scheduleRender();
downloadBtn.disabled = false;
resetBtn.disabled = false;
URL.revokeObjectURL(url);
};
img.onerror = () => {
alert('无法加载该图片,请更换文件试试。');
URL.revokeObjectURL(url);
};
img.src = url;
});
amount.addEventListener('input', scheduleRender);
resetBtn.addEventListener('click', () => {
if(!originalPixels) return;
amount.value = 100;
scheduleRender();
});
// 下载当前画布
function downloadCanvas(filename){
// toBlob 在少数旧版浏览器可能不可用,做个兼容
if(canvas.toBlob){
canvas.toBlob((blob) => {
if(!blob){ fallback(); return; }
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(link.href), 5000);
}, 'image/png');
} else {
fallback();
}
function fallback(){
const dataURL = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = dataURL;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
}
}
downloadBtn.addEventListener('click', () => {
if(!originalPixels) return;
downloadCanvas(currentFileName);
});
})();
</script></body>
</html>