简单图片编辑器,动态模糊、毛玻璃效果、多种格式导出、图片信息显示、水印添加和色彩调整功能、局部模糊功能、网格文字水印。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>简单图像编辑器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'Roboto', system-ui, -apple-system, sans-serif;
background: #0f172a;
height: 100vh;
overflow: hidden;
}
/* 主应用 - 全屏Flex布局 */
.app {
display: flex;
flex-direction: column;
height: 100vh;
background: #0f172a;
}
/* 头部 */
.header {
background: linear-gradient(135deg, #0f2c3b 0%, #1a4058 100%);
padding: 0.75rem 1.5rem;
color: white;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.header h1 {
font-size: 1.3rem;
display: flex;
align-items: center;
gap: 8px;
}
.header-badges {
display: flex;
gap: 12px;
font-size: 0.7rem;
}
.badge {
background: rgba(255,255,255,0.2);
padding: 4px 12px;
border-radius: 20px;
}
/* 主体 - 左右布局 */
.main {
display: flex;
flex: 1;
overflow: hidden;
gap: 0;
}
/* 左侧工具栏 - 紧凑设计 */
.toolbar {
width: 320px;
background: #f8fafc;
border-right: 1px solid #e2e8f0;
overflow-y: auto;
padding: 1rem;
flex-shrink: 0;
scrollbar-width: thin;
}
/* 右侧预览区 - 自适应缩放 */
.preview {
flex: 1;
background: #11181c;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
/* 画布容器 - 支持自动缩放 */
.canvas-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
overflow: auto;
position: relative;
}
.canvas-wrapper {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
canvas {
display: block;
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
border-radius: 12px;
background: #fff;
cursor: crosshair;
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
}
/* 信息栏 */
.info-bar {
background: #0f1a1f;
color: #94a3b8;
padding: 0.6rem 1.2rem;
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 0.7rem;
font-family: monospace;
border-top: 1px solid #2d3e48;
flex-shrink: 0;
}
.info-item {
background: #1e2a32;
padding: 4px 12px;
border-radius: 16px;
}
/* UI 组件样式 - 精简统一 */
.section {
background: white;
border-radius: 16px;
padding: 0.9rem;
margin-bottom: 0.9rem;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.section-title {
font-weight: 600;
margin-bottom: 10px;
color: #1e4663;
font-size: 0.8rem;
border-left: 3px solid #2c6e9e;
padding-left: 8px;
}
.slider-group {
margin-bottom: 12px;
}
.slider-group label {
display: flex;
justify-content: space-between;
font-size: 0.7rem;
margin-bottom: 5px;
color: #334155;
}
input[type="range"] {
width: 100%;
height: 4px;
border-radius: 4px;
background: #cbd5e1;
-webkit-appearance: none;
}
input[type="range"]:focus {
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: #2c6e9e;
cursor: pointer;
border: 2px solid white;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
select, input[type="text"], input[type="color"] {
width: 100%;
padding: 6px 10px;
border: 1px solid #cbd5e6;
border-radius: 10px;
font-size: 0.7rem;
background: white;
}
.btn {
background: white;
border: 1px solid #cbd5e6;
padding: 5px 12px;
border-radius: 20px;
cursor: pointer;
font-size: 0.7rem;
font-weight: 500;
transition: all 0.15s;
}
.btn-primary {
background: #2c6e9e;
color: white;
border: none;
}
.btn-primary:hover {
background: #1f557b;
}
.btn-sm {
padding: 4px 10px;
font-size: 0.65rem;
}
.btn-active {
background: #2c6e9e;
color: white;
border-color: #2c6e9e;
}
.flex-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 6px;
}
.upload-btn {
background: #2c6e9e;
color: white;
text-align: center;
padding: 8px;
border-radius: 30px;
cursor: pointer;
font-weight: 600;
font-size: 0.8rem;
transition: 0.2s;
}
.upload-btn:hover {
background: #1f557b;
}
.local-area {
background: #f1f5f9;
border-radius: 12px;
padding: 8px;
margin-top: 6px;
}
.value-highlight {
color: #2c6e9e;
font-weight: 600;
}
.status-text {
font-size: 0.65rem;
color: #475569;
margin-top: 5px;
}
hr {
margin: 8px 0;
border-color: #e2e8f0;
}
/* 滚动条美化 */
.toolbar::-webkit-scrollbar {
width: 5px;
}
.toolbar::-webkit-scrollbar-track {
background: #e2e8f0;
}
.toolbar::-webkit-scrollbar-thumb {
background: #94a3b8;
border-radius: 5px;
}
@media (max-width: 768px) {
.toolbar {
width: 280px;
}
.header h1 {
font-size: 1rem;
}
.info-bar {
font-size: 0.6rem;
gap: 8px;
}
}
</style>
</head>
<body>
<div class="app">
<div class="header">
<h1>🎨 图像编辑工作室</h1>
<div class="header-badges">
<span class="badge">✨ 局部模糊</span>
<span class="badge">🎯 网格水印</span>
<span class="badge">⚡ 实时预览</span>
</div>
</div>
<div class="main">
<!-- 左侧工具栏 -->
<div class="toolbar">
<!-- 上传区 -->
<div class="section">
<div class="upload-btn" id="uploadTrigger">
📸 上传图片 (WebP/PNG/JPG)
<input type="file" id="fileInput" accept="image/*" style="display:none">
</div>
<div class="flex-row">
<button class="btn btn-sm" id="resetAllBtn">🔄 重置全部</button>
</div>
<div id="imgInfo" style="font-size:0.65rem; margin-top:8px; color:#475569; text-align:center;"></div>
</div>
<!-- 全局模糊 -->
<div class="section">
<div class="section-title">🌀 全局模糊</div>
<select id="blurType">
<option value="gaussian">高斯模糊</option>
<option value="motion">动态模糊</option>
<option value="frosted">毛玻璃效果</option>
</select>
<div class="slider-group" style="margin-top:8px">
<label><span>强度</span><span id="blurVal" class="value-highlight">0</span></label>
<input type="range" id="blurIntensity" min="0" max="20" step="0.5" value="0">
</div>
<div id="motionAngleWrap" style="display:none;">
<label>动态角度</label>
<input type="range" id="motionAngle" min="0" max="360" step="1" value="45">
</div>
</div>
<!-- 局部模糊 -->
<div class="section">
<div class="section-title">🎯 局部模糊</div>
<div class="local-area">
<div class="flex-row">
<button class="btn btn-sm" id="enableLocalBtn">✏️ 启用</button>
<button class="btn btn-sm" id="clearLocalBtn">🗑️ 清除</button>
</div>
<div class="slider-group" style="margin-top:8px">
<label>模糊强度 <span id="localStrengthVal" class="value-highlight">6.0</span></label>
<input type="range" id="localStrength" min="0" max="20" step="0.5" value="6">
</div>
<div id="localStatus" class="status-text">💡 启用后点击拖拽选择区域</div>
</div>
</div>
<!-- 色彩调整 -->
<div class="section">
<div class="section-title">🎨 色彩调整</div>
<div class="slider-group"><label>亮度 <span id="brightnessVal">0</span></label><input type="range" id="brightness" min="-100" max="100" value="0"></div>
<div class="slider-group"><label>对比度 <span id="contrastVal">0</span></label><input type="range" id="contrast" min="-100" max="100" value="0"></div>
<div class="slider-group"><label>饱和度 <span id="saturationVal">0</span></label><input type="range" id="saturation" min="-100" max="100" value="0"></div>
</div>
<!-- 水印 -->
<div class="section">
<div class="section-title">💧 水印</div>
<select id="watermarkType">
<option value="none">无水印</option>
<option value="text">文字水印</option>
<option value="copyright">© 版权符号</option>
<option value="grid_text">网格+文字</option>
</select>
<div id="textWatermarkPanel" style="display:none; margin-top:8px">
<input type="text" id="watermarkText" placeholder="水印文字" value="Art">
<div class="flex-row" style="margin-top:5px">
<input type="color" id="watermarkColor" value="#ffffff" style="width:45px">
<input type="range" id="watermarkOpacity" min="0" max="100" value="55" style="flex:1">
<span style="font-size:11px">透明度%</span>
</div>
<select id="watermarkPos" style="margin-top:5px">
<option value="bottom-right">右下角</option>
<option value="bottom-left">左下角</option>
<option value="top-right">右上角</option>
<option value="top-left">左上角</option>
<option value="center">居中</option>
</select>
</div>
<div id="gridPanel" style="display:none; margin-top:8px">
<label>网格密度</label>
<input type="range" id="gridDensity" min="35" max="90" value="55">
<label style="margin-top:5px">网格文字</label>
<input type="text" id="gridText" value="✨">
</div>
</div>
<!-- 导出 -->
<div class="section">
<div class="section-title">💾 导出</div>
<select id="exportFormat">
<option value="image/webp">WebP</option>
<option value="image/png">PNG</option>
<option value="image/jpeg">JPEG</option>
</select>
<div id="jpegQualityWrap" style="display:none; margin-top:6px">
<label>质量 <span id="qualitySpan">92</span>%</label>
<input type="range" id="jpegQuality" min="10" max="100" value="92">
</div>
<button class="btn btn-primary" id="downloadBtn" style="width:100%; margin-top:10px;">⬇️ 导出图片</button>
</div>
</div>
<!-- 右侧预览区 - 自动缩放 -->
<div class="preview">
<div class="canvas-container" id="canvasContainer">
<div class="canvas-wrapper">
<canvas id="mainCanvas"></canvas>
</div>
</div>
<div class="info-bar">
<div class="info-item" id="infoResolution">📐 分辨率: --</div>
<div class="info-item" id="infoSize">📦 大小: --</div>
<div class="info-item" id="infoEffects">🎨 效果: 原始</div>
<div class="info-item" id="infoLocal">📍 局部: 未启用</div>
</div>
</div>
</div>
</div>
<script>
// ==================== 状态管理 ====================
let originalBitmap = null;
let imgW = 0, imgH = 0;
let originalFile = null;
let renderPending = false;
let isRendering = false;
// 参数
let params = {
blurType: 'gaussian',
blurIntensity: 0,
motionAngle: 45,
brightness: 0,
contrast: 0,
saturation: 0,
watermark: {
style: 'none',
text: 'Art',
color: '#ffffff',
opacity: 0.55,
position: 'bottom-right',
gridDensity: 55,
gridText: '✨'
},
local: {
enabled: false,
x: 0, y: 0, w: 0, h: 0,
strength: 6
}
};
// 交互状态
let isSelectingLocal = false;
let selectStart = { x: 0, y: 0 };
// DOM
const canvas = document.getElementById('mainCanvas');
const ctx = canvas.getContext('2d');
const fileInput = document.getElementById('fileInput');
const uploadTrigger = document.getElementById('uploadTrigger');
const resetBtn = document.getElementById('resetAllBtn');
const downloadBtn = document.getElementById('downloadBtn');
// 模糊相关
const blurType = document.getElementById('blurType');
const blurIntensity = document.getElementById('blurIntensity');
const blurVal = document.getElementById('blurVal');
const motionAngleWrap = document.getElementById('motionAngleWrap');
const motionAngle = document.getElementById('motionAngle');
// 色彩
const brightness = document.getElementById('brightness');
const contrast = document.getElementById('contrast');
const saturation = document.getElementById('saturation');
const brightnessVal = document.getElementById('brightnessVal');
const contrastVal = document.getElementById('contrastVal');
const saturationVal = document.getElementById('saturationVal');
// 局部模糊
const enableLocalBtn = document.getElementById('enableLocalBtn');
const clearLocalBtn = document.getElementById('clearLocalBtn');
const localStrength = document.getElementById('localStrength');
const localStrengthVal = document.getElementById('localStrengthVal');
const localStatus = document.getElementById('localStatus');
// 水印
const watermarkType = document.getElementById('watermarkType');
const textPanel = document.getElementById('textWatermarkPanel');
const gridPanel = document.getElementById('gridPanel');
const watermarkText = document.getElementById('watermarkText');
const watermarkColor = document.getElementById('watermarkColor');
const watermarkOpacity = document.getElementById('watermarkOpacity');
const watermarkPos = document.getElementById('watermarkPos');
const gridDensity = document.getElementById('gridDensity');
const gridText = document.getElementById('gridText');
// 导出
const exportFormat = document.getElementById('exportFormat');
const jpegQualityWrap = document.getElementById('jpegQualityWrap');
const jpegQuality = document.getElementById('jpegQuality');
const qualitySpan = document.getElementById('qualitySpan');
// 信息栏
const infoResolution = document.getElementById('infoResolution');
const infoSize = document.getElementById('infoSize');
const infoEffects = document.getElementById('infoEffects');
const infoLocal = document.getElementById('infoLocal');
const imgInfoDiv = document.getElementById('imgInfo');
// ==================== 核心渲染 ====================
function updateInfoUI() {
if (originalBitmap && imgW > 0) {
let sizeStr = '';
if (originalFile) {
const bytes = originalFile.size;
if (bytes < 1024) sizeStr = bytes + ' B';
else if (bytes < 1048576) sizeStr = (bytes/1024).toFixed(1) + ' KB';
else sizeStr = (bytes/1048576).toFixed(1) + ' MB';
imgInfoDiv.innerHTML = `📷 ${imgW}×${imgH} | 💾 ${sizeStr}`;
infoSize.innerHTML = `📦 ${sizeStr}`;
} else {
imgInfoDiv.innerHTML = `📷 ${imgW}×${imgH}`;
infoSize.innerHTML = `📦 --`;
}
infoResolution.innerHTML = `📐 ${imgW}×${imgH}`;
} else {
imgInfoDiv.innerHTML = '等待上传图片';
infoResolution.innerHTML = `📐 分辨率: --`;
infoSize.innerHTML = `📦 大小: --`;
}
}
// 色彩调整
function applyColorAdjust(ctx, w, h) {
const imgData = ctx.getImageData(0, 0, w, h);
const data = imgData.data;
const bVal = params.brightness / 100;
const cVal = params.contrast / 100;
const sVal = params.saturation / 100;
const contrastFactor = cVal !== 0 ? (259 * (cVal + 255)) / (255 * (259 - cVal)) : 1;
for (let i = 0; i < data.length; i += 4) {
let r = data[i], g = data[i+1], b = data[i+2];
if (cVal !== 0) {
r = contrastFactor * (r - 128) + 128;
g = contrastFactor * (g - 128) + 128;
b = contrastFactor * (b - 128) + 128;
}
if (bVal !== 0) {
r += bVal * 255;
g += bVal * 255;
b += bVal * 255;
}
if (sVal !== 0) {
const gray = 0.2989 * r + 0.5870 * g + 0.1140 * b;
const sat = 1 + sVal;
r = gray + (r - gray) * sat;
g = gray + (g - gray) * sat;
b = gray + (b - gray) * sat;
}
data[i] = Math.min(255, Math.max(0, r));
data[i+1] = Math.min(255, Math.max(0, g));
data[i+2] = Math.min(255, Math.max(0, b));
}
ctx.putImageData(imgData, 0, 0);
}
// 全局模糊
function applyGlobalBlur(ctx, w, h) {
const intensity = params.blurIntensity;
if (intensity <= 0) return;
const type = params.blurType;
if (type === 'gaussian') {
const off = document.createElement('canvas');
off.width = w; off.height = h;
const offCtx = off.getContext('2d');
offCtx.drawImage(canvas, 0, 0);
offCtx.filter = `blur(${intensity}px)`;
offCtx.drawImage(off, 0, 0);
ctx.drawImage(off, 0, 0);
} else if (type === 'motion') {
const rad = (params.motionAngle * Math.PI) / 180;
const steps = Math.max(4, Math.floor(intensity * 1.2));
const dx = Math.cos(rad) * intensity * 0.7;
const dy = Math.sin(rad) * intensity * 0.7;
const temp = document.createElement('canvas');
temp.width = w; temp.height = h;
const tCtx = temp.getContext('2d');
tCtx.drawImage(canvas, 0, 0);
ctx.clearRect(0, 0, w, h);
for (let i = 0; i <= steps; i++) {
ctx.globalAlpha = 1 / (steps + 1);
ctx.drawImage(temp, (i/steps) * dx, (i/steps) * dy);
}
ctx.globalAlpha = 1;
} else if (type === 'frosted') {
const off = document.createElement('canvas');
off.width = w; off.height = h;
const offCtx = off.getContext('2d');
offCtx.drawImage(canvas, 0, 0);
offCtx.filter = `blur(${intensity * 0.8}px)`;
offCtx.drawImage(off, 0, 0);
const data = offCtx.getImageData(0, 0, w, h);
const noise = intensity * 0.5;
for (let i = 0; i < data.data.length; i += 4) {
data.data[i] += (Math.random() - 0.5) * noise;
data.data[i+1] += (Math.random() - 0.5) * noise;
data.data[i+2] += (Math.random() - 0.5) * noise;
}
ctx.putImageData(data, 0, 0);
}
}
// 局部模糊
function applyLocalBlur(ctx, w, h) {
const { enabled, x, y, w: rw, h: rh, strength } = params.local;
if (!enabled || strength <= 0 || rw <= 0 || rh <= 0) return;
if (x < 0 || y < 0 || x + rw > w || y + rh > h) return;
const region = document.createElement('canvas');
region.width = rw; region.height = rh;
const rCtx = region.getContext('2d');
rCtx.drawImage(canvas, x, y, rw, rh, 0, 0, rw, rh);
rCtx.filter = `blur(${strength}px)`;
rCtx.drawImage(region, 0, 0);
ctx.drawImage(region, x, y, rw, rh);
}
// 水印
function applyWatermark(ctx, w, h) {
const style = params.watermark.style;
if (style === 'none') return;
if (style === 'text' || style === 'copyright') {
let text = style === 'copyright' ? '© ' + (params.watermark.text || 'Art') : params.watermark.text;
const fontSize = Math.max(16, Math.min(w, h) * 0.045);
ctx.font = `600 ${fontSize}px "Segoe UI"`;
ctx.fillStyle = params.watermark.color;
ctx.globalAlpha = params.watermark.opacity;
const metrics = ctx.measureText(text);
const tw = metrics.width;
let x = w - tw - 20, y = h - 20;
const pos = params.watermark.position;
if (pos === 'bottom-right') { x = w - tw - 20; y = h - 20; }
else if (pos === 'bottom-left') { x = 20; y = h - 20; }
else if (pos === 'top-right') { x = w - tw - 20; y = 40; }
else if (pos === 'top-left') { x = 20; y = 40; }
else if (pos === 'center') { x = (w - tw)/2; y = h/2 + fontSize/3; }
ctx.fillText(text, x, y);
ctx.globalAlpha = 1;
} else if (style === 'grid_text') {
const step = Math.max(35, params.watermark.gridDensity);
const txt = params.watermark.gridText || "✦";
ctx.globalAlpha = 0.32;
ctx.fillStyle = params.watermark.color;
ctx.font = `14px "Segoe UI"`;
ctx.textAlign = "center";
for (let x = step/2; x < w; x += step) {
for (let y = step/2; y < h; y += step) {
ctx.fillText(txt, x, y);
}
}
ctx.textAlign = "left";
ctx.globalAlpha = 1;
}
}
// 渲染主函数
function render() {
if (!originalBitmap) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
return;
}
if (isRendering) {
renderPending = true;
return;
}
isRendering = true;
try {
const w = imgW, h = imgH;
canvas.width = w;
canvas.height = h;
ctx.drawImage(originalBitmap, 0, 0, w, h);
if (params.brightness !== 0 || params.contrast !== 0 || params.saturation !== 0) {
applyColorAdjust(ctx, w, h);
}
if (params.blurIntensity > 0) {
applyGlobalBlur(ctx, w, h);
}
if (params.local.enabled && params.local.w > 0 && params.local.h > 0) {
applyLocalBlur(ctx, w, h);
}
if (params.watermark.style !== 'none') {
applyWatermark(ctx, w, h);
}
// 更新效果状态
let effects = [];
if (params.blurIntensity > 0) effects.push(`${params.blurType === 'gaussian' ? '高斯' : params.blurType === 'motion' ? '动态' : '毛玻璃'}${params.blurIntensity.toFixed(1)}`);
if (params.brightness !== 0 || params.contrast !== 0 || params.saturation !== 0) effects.push('调色');
if (params.local.enabled && params.local.w > 0) effects.push('局部模糊');
if (params.watermark.style !== 'none') effects.push('水印');
infoEffects.innerHTML = `🎨 效果: ${effects.length ? effects.join('+') : '原始'}`;
infoLocal.innerHTML = `📍 局部: ${params.local.enabled && params.local.w > 0 ? `${Math.round(params.local.w)}×${Math.round(params.local.h)}` : '未启用'}`;
} catch(e) { console.warn(e); }
finally {
isRendering = false;
if (renderPending) {
renderPending = false;
render();
}
}
}
function scheduleRender() {
if (originalBitmap) render();
}
// ==================== 局部模糊交互 ====================
function initLocalSelection() {
let drawing = false;
let start = { x: 0, y: 0 };
const getCanvasCoord = (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
let cx, cy;
if (e.touches) {
cx = e.touches[0].clientX;
cy = e.touches[0].clientY;
} else {
cx = e.clientX;
cy = e.clientY;
}
let x = (cx - rect.left) * scaleX;
let y = (cy - rect.top) * scaleY;
x = Math.min(Math.max(0, x), canvas.width);
y = Math.min(Math.max(0, y), canvas.height);
return { x, y };
};
const onStart = (e) => {
if (!params.local.enabled) return;
drawing = true;
start = getCanvasCoord(e);
e.preventDefault();
};
const onMove = (e) => {
if (!drawing || !params.local.enabled) return;
const current = getCanvasCoord(e);
// 临时显示选择框
scheduleRender();
setTimeout(() => {
if (originalBitmap && params.local.enabled && drawing) {
ctx.save();
ctx.globalAlpha = 0.35;
ctx.fillStyle = '#3b82f6';
ctx.fillRect(start.x, start.y, current.x - start.x, current.y - start.y);
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.strokeRect(start.x, start.y, current.x - start.x, current.y - start.y);
ctx.restore();
}
}, 5);
};
const onEnd = (e) => {
if (!drawing || !params.local.enabled) {
drawing = false;
return;
}
const end = getCanvasCoord(e);
const x = Math.min(start.x, end.x);
const y = Math.min(start.y, end.y);
const w = Math.abs(end.x - start.x);
const h = Math.abs(end.y - start.y);
if (w > 10 && h > 10) {
params.local.x = x;
params.local.y = y;
params.local.w = w;
params.local.h = h;
localStatus.innerHTML = `✅ 区域: ${Math.round(w)}×${Math.round(h)} px | 强度: ${params.local.strength}`;
scheduleRender();
} else {
localStatus.innerHTML = `⚠️ 区域太小,请拖拽更大的范围`;
}
drawing = false;
};
canvas.addEventListener('mousedown', onStart);
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onEnd);
canvas.addEventListener('touchstart', onStart);
window.addEventListener('touchmove', onMove);
window.addEventListener('touchend', onEnd);
}
// ==================== 加载图片 ====================
async function loadImage(file) {
if (!file) return;
originalFile = file;
if (originalBitmap) originalBitmap.close();
try {
const bitmap = await createImageBitmap(file);
originalBitmap = bitmap;
imgW = bitmap.width;
imgH = bitmap.height;
updateInfoUI();
scheduleRender();
} catch(e) {
alert('图片加载失败: ' + e.message);
}
}
// ==================== 重置 ====================
function resetAll() {
blurIntensity.value = '0';
params.blurIntensity = 0;
blurVal.textContent = '0';
brightness.value = '0';
contrast.value = '0';
saturation.value = '0';
params.brightness = params.contrast = params.saturation = 0;
brightnessVal.textContent = '0';
contrastVal.textContent = '0';
saturationVal.textContent = '0';
params.local.enabled = false;
params.local.w = 0;
params.local.h = 0;
enableLocalBtn.classList.remove('btn-active');
localStatus.innerHTML = '💡 启用后点击拖拽选择区域';
params.watermark.style = 'none';
watermarkType.value = 'none';
textPanel.style.display = 'none';
gridPanel.style.display = 'none';
scheduleRender();
}
// ==================== 导出 ====================
function exportImage() {
if (!originalBitmap) { alert('请先上传图片'); return; }
let format = exportFormat.value;
let quality = 0.92;
if (format === 'image/jpeg') quality = parseInt(jpegQuality.value) / 100;
let mime = format;
if (mime === 'image/webp') {
const test = document.createElement('canvas');
test.width = 1;
if (!test.toDataURL('image/webp').includes('image/webp')) mime = 'image/png';
}
const link = document.createElement('a');
link.download = `edited_${Date.now()}.${mime.split('/')[1]}`;
link.href = canvas.toDataURL(mime, quality);
link.click();
}
// ==================== 事件绑定 ====================
function bindEvents() {
uploadTrigger.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => { if(e.target.files[0]) loadImage(e.target.files[0]); fileInput.value = ''; });
resetBtn.addEventListener('click', resetAll);
downloadBtn.addEventListener('click', exportImage);
blurType.addEventListener('change', (e) => { params.blurType = e.target.value; motionAngleWrap.style.display = params.blurType === 'motion' ? 'block' : 'none'; scheduleRender(); });
blurIntensity.addEventListener('input', (e) => { params.blurIntensity = parseFloat(e.target.value); blurVal.textContent = params.blurIntensity.toFixed(1); scheduleRender(); });
motionAngle.addEventListener('input', (e) => { params.motionAngle = parseInt(e.target.value); if(params.blurType === 'motion') scheduleRender(); });
brightness.addEventListener('input', (e) => { params.brightness = parseInt(e.target.value); brightnessVal.textContent = params.brightness; scheduleRender(); });
contrast.addEventListener('input', (e) => { params.contrast = parseInt(e.target.value); contrastVal.textContent = params.contrast; scheduleRender(); });
saturation.addEventListener('input', (e) => { params.saturation = parseInt(e.target.value); saturationVal.textContent = params.saturation; scheduleRender(); });
enableLocalBtn.addEventListener('click', () => { params.local.enabled = true; enableLocalBtn.classList.add('btn-active'); localStatus.innerHTML = '🎯 已启用,点击图片拖拽选择区域'; scheduleRender(); });
clearLocalBtn.addEventListener('click', () => { params.local.enabled = false; params.local.w = 0; params.local.h = 0; enableLocalBtn.classList.remove('btn-active'); localStatus.innerHTML = '💡 已清除局部区域'; scheduleRender(); });
localStrength.addEventListener('input', (e) => { params.local.strength = parseFloat(e.target.value); localStrengthVal.textContent = params.local.strength.toFixed(1); if(params.local.enabled && params.local.w > 0) scheduleRender(); });
watermarkType.addEventListener('change', (e) => {
params.watermark.style = e.target.value;
textPanel.style.display = (e.target.value === 'text' || e.target.value === 'copyright') ? 'block' : 'none';
gridPanel.style.display = e.target.value === 'grid_text' ? 'block' : 'none';
scheduleRender();
});
watermarkText.addEventListener('input', (e) => { params.watermark.text = e.target.value; scheduleRender(); });
watermarkColor.addEventListener('input', (e) => { params.watermark.color = e.target.value; scheduleRender(); });
watermarkOpacity.addEventListener('input', (e) => { params.watermark.opacity = parseInt(e.target.value)/100; scheduleRender(); });
watermarkPos.addEventListener('change', (e) => { params.watermark.position = e.target.value; scheduleRender(); });
gridDensity.addEventListener('input', (e) => { params.watermark.gridDensity = parseInt(e.target.value); scheduleRender(); });
gridText.addEventListener('input', (e) => { params.watermark.gridText = e.target.value; scheduleRender(); });
exportFormat.addEventListener('change', (e) => { jpegQualityWrap.style.display = e.target.value === 'image/jpeg' ? 'block' : 'none'; });
jpegQuality.addEventListener('input', (e) => { qualitySpan.textContent = e.target.value; });
}
// 初始化
initLocalSelection();
bindEvents();
updateInfoUI();
</script>
</body>
</html>
