简单图片编辑工具

星期六, 1月 10, 2026 | 11分钟阅读 | 更新于 星期六, 1月 10, 2026

DOM_ZS

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

<!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>

© 2024 - 2026 我的梦境博客

🌱 Powered by Hugo with theme Dream.

关于我

👋 你好,我是DOM_zs!

这是我的个人博客。

📧 联系我:zhangsong192@proton.me

社交链接

© 2024 - 2026 我的梦境博客

🌱 Powered by Hugo with theme Dream.