HTML&CSS&JS:纯前端图片打码神器:自定义强度 + 区域缩放,无需安装

图片马赛克是怎么实现?下面的案例告诉你。该html文件是一款纯前端驱动的图片马赛克处理工具,无需后端依赖,支持从图片上传到结果下载的全流程操作,核心亮点是可视化选区控制(选择 / 移动 / 缩放)与实时马赛克效果调整,兼顾功能完整性与用户交互友好性。


大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。

演示效果

HTML&CSS

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片马赛克处理工具</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
        }

        body {
            background-color: #f8f8f8;
            color: #333;
            line-height: 1.6;
            padding: 20px;
            max-width: 800px;
            margin: 0 auto;
        }

        .container {
            background-color: #fff;
            border-radius: 12px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
            padding: 20px;
            margin-bottom: 20px;
        }

        h1 {
            font-size: 18px;
            font-weight: 600;
            margin-bottom: 15px;
            color: #1a1a1a;
            display: flex;
            align-items: center;
        }

        h1:before {
            content: "";
            display: inline-block;
            width: 4px;
            height: 16px;
            background-color: #07c160;
            border-radius: 2px;
            margin-right: 8px;
        }

        .upload-area {
            border: 2px dashed #e0e0e0;
            border-radius: 8px;
            padding: 40px 20px;
            text-align: center;
            cursor: pointer;
            transition: all 0.3s;
            margin-bottom: 20px;
        }

        .upload-area:hover {
            border-color: #07c160;
            background-color: #f9fff9;
        }

        .upload-area.active {
            border-color: #07c160;
            background-color: #f0f9f4;
        }

        .upload-icon {
            font-size: 48px;
            color: #07c160;
            margin-bottom: 10px;
        }

        .upload-text {
            color: #666;
            font-size: 14px;
        }

        .preview-container {
            display: none;
            margin-bottom: 20px;
            text-align: center;
        }

        .canvas-container {
            position: relative;
            display: inline-block;
            margin-bottom: 15px;
            border: 1px solid #e0e0e0;
            border-radius: 4px;
            overflow: hidden;
            cursor: crosshair;
        }

        canvas {
            display: block;
            max-width: 100%;
        }

        .selection-rect {
            position: absolute;
            border: 2px dashed #07c160;
            background-color: rgba(7, 193, 96, 0.1);
            pointer-events: none;
            display: none;
        }

        .mosaic-area {
            position: absolute;
            border: 2px solid #fa5151;
            background-color: rgba(250, 81, 81, 0.1);
            cursor: move;
            display: none;
        }

        .resize-handle {
            position: absolute;
            width: 12px;
            height: 12px;
            background-color: #fa5151;
            border: 2px solid #fff;
            border-radius: 2px;
            z-index: 10;
        }

        .resize-nw {
            top: -6px;
            left: -6px;
            cursor: nw-resize;
        }

        .resize-ne {
            top: -6px;
            right: -6px;
            cursor: ne-resize;
        }

        .resize-sw {
            bottom: -6px;
            left: -6px;
            cursor: sw-resize;
        }

        .resize-se {
            bottom: -6px;
            right: -6px;
            cursor: se-resize;
        }

        .tools {
            display: flex;
            justify-content: space-between;
            margin-bottom: 20px;
        }

        .tool-group {
            display: flex;
            gap: 10px;
        }

        .btn {
            padding: 8px 16px;
            border: none;
            border-radius: 6px;
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.2s;
        }

        .btn-primary {
            background-color: #07c160;
            color: white;
        }

        .btn-primary:hover {
            background-color: #06a451;
        }

        .btn-default {
            background-color: #f0f0f0;
            color: #333;
        }

        .btn-default:hover {
            background-color: #e0e0e0;
        }

        .btn-danger {
            background-color: #fa5151;
            color: white;
        }

        .btn-danger:hover {
            background-color: #e04040;
        }

        .slider-container {
            display: flex;
            align-items: center;
            margin-bottom: 20px;
        }

        .slider-label {
            margin-right: 10px;
            font-size: 14px;
            color: #666;
            min-width: 80px;
        }

        input[type="range"] {
            flex: 1;
            height: 4px;
            -webkit-appearance: none;
            background: #e0e0e0;
            border-radius: 2px;
            outline: none;
        }

        input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 18px;
            height: 18px;
            border-radius: 50%;
            background: #07c160;
            cursor: pointer;
        }

        .size-indicator {
            margin-left: 10px;
            font-size: 14px;
            color: #333;
            min-width: 40px;
            text-align: center;
        }

        .instructions {
            background-color: #f9f9f9;
            border-left: 3px solid #07c160;
            padding: 12px 15px;
            margin-top: 20px;
            font-size: 13px;
            color: #666;
        }

        .instructions h3 {
            font-size: 14px;
            margin-bottom: 5px;
            color: #333;
        }

        .instructions ul {
            padding-left: 20px;
            text-align: left;
        }

        .instructions li {
            margin-bottom: 5px;
        }

        .loading {
            display: none;
            text-align: center;
            padding: 20px;
            color: #666;
        }

        .spinner {
            border: 3px solid #f3f3f3;
            border-top: 3px solid #07c160;
            border-radius: 50%;
            width: 30px;
            height: 30px;
            animation: spin 1s linear infinite;
            margin: 0 auto 10px;
        }

        @keyframes spin {
            0% {
                transform: rotate(0deg);
            }

            100% {
                transform: rotate(360deg);
            }
        }

        .result-container {
            display: none;
            text-align: center;
            margin-top: 20px;
        }

        .download-btn {
            display: inline-block;
            padding: 10px 20px;
            background-color: #07c160;
            color: white;
            text-decoration: none;
            border-radius: 6px;
            font-weight: 500;
            transition: background-color 0.2s;
        }

        .download-btn:hover {
            background-color: #06a451;
        }

        .status {
            text-align: center;
            margin-top: 10px;
            font-size: 14px;
            color: #666;
        }

        .mode-indicator {
            display: inline-block;
            padding: 4px 8px;
            background-color: #07c160;
            color: white;
            border-radius: 4px;
            font-size: 12px;
            margin-left: 10px;
        }
    </style>
</head>

<body>
    <div class="container">
        <h1>图片马赛克处理工具</h1>

        <div class="upload-area" id="uploadArea">
            <div class="upload-icon">📁</div>
            <div class="upload-text">点击选择图片或拖拽图片到此处</div>
        </div>

        <input type="file" id="fileInput" accept="image/*" style="display: none;">

        <div class="preview-container" id="previewContainer">
            <div class="canvas-container">
                <canvas id="previewCanvas"></canvas>
                <div class="selection-rect" id="selectionRect"></div>
                <div class="mosaic-area" id="mosaicArea">
                    <div class="resize-handle resize-nw"></div>
                    <div class="resize-handle resize-ne"></div>
                    <div class="resize-handle resize-sw"></div>
                    <div class="resize-handle resize-se"></div>
                </div>
            </div>

            <div class="status" id="statusInfo">
                当前模式: <span class="mode-indicator" id="modeIndicator">选择区域</span>
                <span id="statusText">按住鼠标左键并拖拽选择区域,松开后自动添加马赛克</span>
            </div>

            <div class="tools">
                <div class="tool-group">
                    <button class="btn btn-default" id="resetBtn">重置图片</button>
                </div>
                <div class="tool-group">
                    <button class="btn btn-danger" id="saveBtn">保存图片</button>
                </div>
            </div>

            <div class="slider-container">
                <div class="slider-label">马赛克强度:</div>
                <input type="range" id="mosaicSize" min="5" max="50" value="15">
                <div class="size-indicator" id="sizeValue">15px</div>
            </div>

            <div class="instructions">
                <h3>使用说明:</h3>
                <ul>
                    <li>按住鼠标左键并拖拽选择需要打马赛克的区域</li>
                    <li>松开鼠标后,系统会自动为选中区域添加马赛克</li>
                    <li>马赛克区域显示后,可以拖动区域调整位置或拖拽角落调整大小</li>
                    <li>调整马赛克强度滑块改变马赛克颗粒大小</li>
                    <li>点击"保存图片"下载处理后的图片</li>
                </ul>
            </div>
        </div>

        <div class="loading" id="loading">
            <div class="spinner"></div>
            <div>正在处理图片...</div>
        </div>

        <div class="result-container" id="resultContainer">
            <img id="resultImage" class="result-image" alt="处理后的图片">
            <a href="#" class="download-btn" id="downloadBtn">下载图片</a>
        </div>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function () {
            // 获取DOM元素
            const uploadArea = document.getElementById('uploadArea');
            const fileInput = document.getElementById('fileInput');
            const previewContainer = document.getElementById('previewContainer');
            const previewCanvas = document.getElementById('previewCanvas');
            const selectionRect = document.getElementById('selectionRect');
            const mosaicArea = document.getElementById('mosaicArea');
            const resetBtn = document.getElementById('resetBtn');
            const saveBtn = document.getElementById('saveBtn');
            const mosaicSize = document.getElementById('mosaicSize');
            const sizeValue = document.getElementById('sizeValue');
            const loading = document.getElementById('loading');
            const resultContainer = document.getElementById('resultContainer');
            const resultImage = document.getElementById('resultImage');
            const downloadBtn = document.getElementById('downloadBtn');
            const statusInfo = document.getElementById('statusInfo');
            const statusText = document.getElementById('statusText');
            const modeIndicator = document.getElementById('modeIndicator');

            // 获取Canvas上下文并设置性能优化
            const ctx = previewCanvas.getContext('2d', { willReadFrequently: true });

            // 初始化变量
            let originalImage = null;
            let isSelecting = false;
            let isResizing = false;
            let isMoving = false;
            let startX = 0;
            let startY = 0;
            let currentX = 0;
            let currentY = 0;
            let mosaicStrength = parseInt(mosaicSize.value);
            let mosaicRegions = [];
            let currentMode = 'selecting';
            let resizeDirection = '';
            let startLeft = 0, startTop = 0, startWidth = 0, startHeight = 0;

            // 更新马赛克强度显示
            mosaicSize.addEventListener('input', function () {
                mosaicStrength = parseInt(this.value);
                sizeValue.textContent = mosaicStrength + 'px';

                if (currentMode === 'adjusting') {
                    applyMosaicToCurrentArea();
                }
            });

            // 上传区域点击事件
            uploadArea.addEventListener('click', function () {
                fileInput.click();
            });

            // 拖拽事件
            uploadArea.addEventListener('dragover', function (e) {
                e.preventDefault();
                uploadArea.classList.add('active');
            });

            uploadArea.addEventListener('dragleave', function () {
                uploadArea.classList.remove('active');
            });

            uploadArea.addEventListener('drop', function (e) {
                e.preventDefault();
                uploadArea.classList.remove('active');

                if (e.dataTransfer.files.length) {
                    handleImageFile(e.dataTransfer.files[0]);
                }
            });

            // 文件选择事件
            fileInput.addEventListener('change', function () {
                if (this.files.length) {
                    handleImageFile(this.files[0]);
                }
            });

            // 处理图片文件
            function handleImageFile(file) {
                if (!file.type.match('image.*')) {
                    alert('请选择图片文件!');
                    return;
                }

                const reader = new FileReader();

                reader.onload = function (e) {
                    const img = new Image();
                    img.onload = function () {
                        originalImage = img;

                        // 设置Canvas尺寸
                        const maxWidth = Math.min(img.width, 600);
                        const scale = maxWidth / img.width;
                        previewCanvas.width = maxWidth;
                        previewCanvas.height = img.height * scale;

                        // 绘制图片到Canvas
                        ctx.drawImage(img, 0, 0, previewCanvas.width, previewCanvas.height);

                        // 显示预览区域
                        previewContainer.style.display = 'block';
                        resultContainer.style.display = 'none';

                        // 重置状态
                        resetMosaicArea();
                        setMode('selecting');
                    };
                    img.src = e.target.result;
                };

                reader.readAsDataURL(file);
            }

            // 设置当前模式
            function setMode(mode) {
                currentMode = mode;

                switch (mode) {
                    case 'selecting':
                        modeIndicator.textContent = '选择区域';
                        statusText.textContent = '按住鼠标左键并拖拽选择区域,松开后自动添加马赛克';
                        previewCanvas.style.cursor = 'crosshair';
                        mosaicArea.style.display = 'none';
                        break;
                    case 'adjusting':
                        modeIndicator.textContent = '调整区域';
                        statusText.textContent = '拖动区域调整位置,拖拽角落调整大小';
                        previewCanvas.style.cursor = 'default';
                        mosaicArea.style.display = 'block';
                        break;
                }
            }

            // 重置马赛克区域
            function resetMosaicArea() {
                mosaicArea.style.display = 'none';
                mosaicArea.style.left = '0px';
                mosaicArea.style.top = '0px';
                mosaicArea.style.width = '0px';
                mosaicArea.style.height = '0px';
            }

            // Canvas鼠标事件
            previewCanvas.addEventListener('mousedown', startSelection);
            previewCanvas.addEventListener('mousemove', updateSelection);
            previewCanvas.addEventListener('mouseup', applyMosaicToSelection);

            // 马赛克区域鼠标事件
            mosaicArea.addEventListener('mousedown', startMovingOrResizing);
            document.addEventListener('mousemove', handleMovingOrResizing);
            document.addEventListener('mouseup', stopMovingOrResizing);

            // 调整手柄事件 - 修复重点
            const resizeHandles = mosaicArea.querySelectorAll('.resize-handle');
            resizeHandles.forEach(handle => {
                handle.addEventListener('mousedown', function (e) {
                    e.stopPropagation();
                    e.preventDefault();
                    isResizing = true;

                    // 记录初始位置和尺寸
                    const rect = mosaicArea.getBoundingClientRect();
                    const canvasRect = previewCanvas.getBoundingClientRect();

                    startX = e.clientX;
                    startY = e.clientY;

                    startLeft = parseInt(mosaicArea.style.left) || 0;
                    startTop = parseInt(mosaicArea.style.top) || 0;
                    startWidth = parseInt(mosaicArea.style.width) || 0;
                    startHeight = parseInt(mosaicArea.style.height) || 0;

                    // 确定调整方向
                    resizeDirection = this.classList[1]; // nw, ne, sw, se
                });
            });

            // 开始选择区域
            function startSelection(e) {
                if (currentMode !== 'selecting') return;

                isSelecting = true;
                const rect = previewCanvas.getBoundingClientRect();
                startX = e.clientX - rect.left;
                startY = e.clientY - rect.top;

                // 初始化选择区域
                selectionRect.style.left = startX + 'px';
                selectionRect.style.top = startY + 'px';
                selectionRect.style.width = '0px';
                selectionRect.style.height = '0px';
                selectionRect.style.display = 'block';
            }

            // 更新选择区域
            function updateSelection(e) {
                if (!isSelecting) return;

                const rect = previewCanvas.getBoundingClientRect();
                currentX = e.clientX - rect.left;
                currentY = e.clientY - rect.top;

                // 更新选择区域显示
                const left = Math.min(startX, currentX);
                const top = Math.min(startY, currentY);
                const width = Math.abs(currentX - startX);
                const height = Math.abs(currentY - startY);

                selectionRect.style.left = left + 'px';
                selectionRect.style.top = top + 'px';
                selectionRect.style.width = width + 'px';
                selectionRect.style.height = height + 'px';
            }

            // 应用马赛克到选择区域
            function applyMosaicToSelection() {
                if (!isSelecting || currentMode !== 'selecting') return;

                isSelecting = false;

                // 隐藏选择框
                selectionRect.style.display = 'none';

                // 计算选择区域
                const left = Math.min(startX, currentX);
                const top = Math.min(startY, currentY);
                const width = Math.abs(currentX - startX);
                const height = Math.abs(currentY - startY);

                // 确保选择区域有效
                if (width < 5 || height < 5) {
                    statusText.textContent = '选择区域太小,请重新选择';
                    return;
                }

                // 显示马赛克区域
                mosaicArea.style.left = left + 'px';
                mosaicArea.style.top = top + 'px';
                mosaicArea.style.width = width + 'px';
                mosaicArea.style.height = height + 'px';

                // 应用马赛克
                applyMosaicToArea(left, top, width, height);

                // 切换到调整模式
                setMode('adjusting');
            }

            // 应用马赛克到指定区域
            function applyMosaicToArea(left, top, width, height) {
                // 确保坐标在Canvas范围内
                left = Math.max(0, Math.min(left, previewCanvas.width));
                top = Math.max(0, Math.min(top, previewCanvas.height));
                width = Math.max(5, Math.min(width, previewCanvas.width - left));
                height = Math.max(5, Math.min(height, previewCanvas.height - top));

                // 获取图像数据
                const imageData = ctx.getImageData(left, top, width, height);
                const data = imageData.data;

                // 应用马赛克效果
                for (let y = 0; y < height; y += mosaicStrength) {
                    for (let x = 0; x < width; x += mosaicStrength) {
                        // 计算当前块的像素索引
                        const pixelIndex = (y * width + x) * 4;

                        // 计算当前块的平均颜色
                        let r = 0, g = 0, b = 0, count = 0;

                        for (let dy = 0; dy < mosaicStrength && y + dy < height; dy++) {
                            for (let dx = 0; dx < mosaicStrength && x + dx < width; dx++) {
                                const idx = ((y + dy) * width + (x + dx)) * 4;
                                r += data[idx];
                                g += data[idx + 1];
                                b += data[idx + 2];
                                count++;
                            }
                        }

                        r = Math.floor(r / count);
                        g = Math.floor(g / count);
                        b = Math.floor(b / count);

                        // 将当前块的所有像素设置为平均颜色
                        for (let dy = 0; dy < mosaicStrength && y + dy < height; dy++) {
                            for (let dx = 0; dx < mosaicStrength && x + dx < width; dx++) {
                                const idx = ((y + dy) * width + (x + dx)) * 4;
                                data[idx] = r;
                                data[idx + 1] = g;
                                data[idx + 2] = b;
                            }
                        }
                    }
                }

                // 将处理后的图像数据放回Canvas
                ctx.putImageData(imageData, left, top);
            }

            // 应用马赛克到当前区域
            function applyMosaicToCurrentArea() {
                const left = parseInt(mosaicArea.style.left) || 0;
                const top = parseInt(mosaicArea.style.top) || 0;
                const width = parseInt(mosaicArea.style.width) || 0;
                const height = parseInt(mosaicArea.style.height) || 0;

                // 先恢复原始图像
                if (originalImage) {
                    ctx.drawImage(originalImage, 0, 0, previewCanvas.width, previewCanvas.height);

                    // 重新应用所有已确认的马赛克区域
                    mosaicRegions.forEach(region => {
                        applyMosaicToArea(region.left, region.top, region.width, region.height);
                    });
                }

                // 应用当前马赛克区域
                applyMosaicToArea(left, top, width, height);
            }

            // 开始移动或调整大小
            function startMovingOrResizing(e) {
                if (currentMode !== 'adjusting') return;

                // 检查是否点击了调整手柄
                if (e.target.classList.contains('resize-handle')) {
                    return; // 调整手柄的事件已经在上面处理
                }

                // 否则开始移动
                isMoving = true;
                const rect = mosaicArea.getBoundingClientRect();
                const canvasRect = previewCanvas.getBoundingClientRect();

                startX = e.clientX;
                startY = e.clientY;

                // 记录初始位置
                startLeft = parseInt(mosaicArea.style.left) || 0;
                startTop = parseInt(mosaicArea.style.top) || 0;
            }

            // 处理移动或调整大小 - 修复重点
            function handleMovingOrResizing(e) {
                if (isMoving) {
                    const canvasRect = previewCanvas.getBoundingClientRect();
                    const currentX = e.clientX;
                    const currentY = e.clientY;

                    const deltaX = currentX - startX;
                    const deltaY = currentY - startY;

                    // 计算新位置
                    let newLeft = startLeft + deltaX;
                    let newTop = startTop + deltaY;

                    // 限制在Canvas范围内
                    const width = parseInt(mosaicArea.style.width) || 0;
                    const height = parseInt(mosaicArea.style.height) || 0;

                    newLeft = Math.max(0, Math.min(newLeft, previewCanvas.width - width));
                    newTop = Math.max(0, Math.min(newTop, previewCanvas.height - height));

                    // 更新位置
                    mosaicArea.style.left = newLeft + 'px';
                    mosaicArea.style.top = newTop + 'px';

                    // 重新应用马赛克
                    applyMosaicToCurrentArea();
                } else if (isResizing) {
                    const currentX = e.clientX;
                    const currentY = e.clientY;

                    const deltaX = currentX - startX;
                    const deltaY = currentY - startY;

                    let newLeft = startLeft;
                    let newTop = startTop;
                    let newWidth = startWidth;
                    let newHeight = startHeight;

                    // 根据调整方向计算新尺寸和位置
                    switch (resizeDirection) {
                        case 'nw': // 左上角调整
                            newLeft = startLeft + deltaX;
                            newTop = startTop + deltaY;
                            newWidth = startWidth - deltaX;
                            newHeight = startHeight - deltaY;
                            break;
                        case 'ne': // 右上角调整
                            newTop = startTop + deltaY;
                            newWidth = startWidth + deltaX;
                            newHeight = startHeight - deltaY;
                            break;
                        case 'sw': // 左下角调整
                            newLeft = startLeft + deltaX;
                            newWidth = startWidth - deltaX;
                            newHeight = startHeight + deltaY;
                            break;
                        case 'se': // 右下角调整
                            newWidth = startWidth + deltaX;
                            newHeight = startHeight + deltaY;
                            break;
                    }

                    // 确保最小尺寸
                    const minSize = 20;
                    if (newWidth < minSize) {
                        if (resizeDirection === 'nw' || resizeDirection === 'sw') {
                            newLeft = startLeft + startWidth - minSize;
                        }
                        newWidth = minSize;
                    }

                    if (newHeight < minSize) {
                        if (resizeDirection === 'nw' || resizeDirection === 'ne') {
                            newTop = startTop + startHeight - minSize;
                        }
                        newHeight = minSize;
                    }

                    // 限制在Canvas范围内
                    newLeft = Math.max(0, newLeft);
                    newTop = Math.max(0, newTop);
                    newWidth = Math.min(newWidth, previewCanvas.width - newLeft);
                    newHeight = Math.min(newHeight, previewCanvas.height - newTop);

                    // 更新尺寸和位置
                    mosaicArea.style.left = newLeft + 'px';
                    mosaicArea.style.top = newTop + 'px';
                    mosaicArea.style.width = newWidth + 'px';
                    mosaicArea.style.height = newHeight + 'px';

                    // 重新应用马赛克
                    applyMosaicToCurrentArea();
                }
            }

            // 停止移动或调整大小
            function stopMovingOrResizing() {
                isMoving = false;
                isResizing = false;
            }

            // 重置按钮事件
            resetBtn.addEventListener('click', function () {
                if (originalImage) {
                    ctx.drawImage(originalImage, 0, 0, previewCanvas.width, previewCanvas.height);
                    resultContainer.style.display = 'none';
                    resetMosaicArea();
                    mosaicRegions = [];
                    setMode('selecting');
                }
            });

            // 保存按钮事件
            saveBtn.addEventListener('click', function () {
                loading.style.display = 'block';

                // 模拟处理时间
                setTimeout(function () {
                    loading.style.display = 'none';

                    // 创建下载链接
                    const dataURL = previewCanvas.toDataURL('image/png');
                    downloadBtn.href = dataURL;
                    downloadBtn.download = 'mosaic-image.png';

                    // 显示结果
                    resultImage.src = dataURL;
                    resultContainer.style.display = 'block';

                    statusText.textContent = '图片已处理完成!可以下载保存';
                }, 800);
            });
        });
    </script>
</body>

</html>

HTML

  • fileInput:图片上传核心:隐藏原生控件(style="display: none"),通过上传区点击触发,兼顾美观与功能;accept="image/*"限制仅选图片,避免无效文件
  • previewCanvas:技术核心载体:基于 HTML5 Canvas API 实现像素级马赛克处理(读取 / 修改图片像素),是工具的 "处理引擎";所有视觉交互(选区、马赛克)均基于此画布
  • selectionRect:选区引导:用户拖拽选择马赛克区域时显示(绿色虚线 + 半透明背景),直观反馈选择范围,避免用户 "盲选"
  • mosaicArea:已处理区标识:选中区域应用马赛克后显示(红色实线 + 半透明背景),与选择区颜色区分,明确 "待处理 / 已处理" 状态;内部含 4 个缩放手柄
  • resize-handle:区域调整关键:4 个角的手柄(nw/ne/sw/se),分别对应 "西北 / 东北 / 西南 / 东南" 方向缩放,解决 "马赛克区域大小不合适" 的问题,提升灵活性
  • mosaicSize:效果控制核心:滑块调节马赛克颗粒大小(5-50px),实时同步数值显示,满足不同模糊强度需求(如小颗粒模糊细节、大颗粒隐藏敏感信息)
  • downloadBtn:结果输出关键:将 Canvas 内容转为 dataURL 赋值给 href,通过 download 属性指定下载文件名(mosaic-image.png),实现前端直接下载

CSS

  • .canvas-container 布局基础:position: relative 为绝对定位的 selectionRect 和 mosaicArea 提供基准,确保选区 / 已处理区精准覆盖在画布上,避免错位
  • .upload-area:hover/.upload-area.active 交互反馈:hover 时边框变绿(#07c160)+ 背景浅绿,active 时背景加深,明确 "可点击 / 正拖拽" 状态,降低用户操作困惑
  • .selection-rect/.mosaic-area 状态区分:选择区用绿色虚线(暗示 "待操作"),已处理区用红色实线(暗示 "已确认"),通过颜色直观区分两个核心阶段,避免用户混淆
  • .btn 系列(.btn-primary/.btn-danger) 功能引导:按钮颜色语义化 ------ 绿色(保存 / 下载,核心操作)、红色(备用删除)、浅灰(重置,次要操作),用户无需阅读文字即可预判功能
  • .spinner+@keyframes spin 等待反馈:加载动画(3px 边框旋转),在保存图片时显示,避免用户因 "无反馈" 重复点击,提升操作安全感

JavaScript

  1. 图片上传与渲染(入口功能)
js 复制代码
// 1. 触发方式:点击上传区/拖拽文件
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('drop', (e) => { /* 处理拖拽文件 */ });

// 2. 核心处理:读取图片并适配Canvas尺寸
function handleImageFile(file) {
    const reader = new FileReader();
    reader.onload = (e) => {
        const img = new Image();
        img.onload = () => {
            originalImage = img; // 保存原图(用于重置)
            // 按比例缩放图片(最大宽度600px,避免超出屏幕)
            const scale = Math.min(600, img.width) / img.width;
            previewCanvas.width = img.width * scale;
            previewCanvas.height = img.height * scale;
            // 渲染图片到Canvas
            ctx.drawImage(img, 0, 0, previewCanvas.width, previewCanvas.height);
        };
        img.src = e.target.result; // 读取图片DataURL
    };
    reader.readAsDataURL(file);
}

作用:解决 "图片如何进入工具" 的问题,同时适配 Canvas 尺寸,确保后续处理与显示正常。

  1. 马赛克核心算法(技术核心)
js 复制代码
function applyMosaicToArea(left, top, width, height) {
    // 1. 读取选区像素数据(rgba格式,每个像素4个值:r/g/b/a)
    const imageData = ctx.getImageData(left, top, width, height);
    const data = imageData.data;

    // 2. 马赛克逻辑:按强度分割"像素块",计算块内平均颜色
    for (let y = 0; y < height; y += mosaicStrength) { // 纵向按强度步长循环
        for (let x = 0; x < width; x += mosaicStrength) { // 横向按强度步长循环
            // 2.1 计算块内平均颜色
            let r = 0, g = 0, b = 0, count = 0;
            for (let dy = 0; dy < mosaicStrength && y + dy < height; dy++) {
                for (let dx = 0; dx < mosaicStrength && x + dx < width; dx++) {
                    const idx = ((y + dy) * width + (x + dx)) * 4;
                    r += data[idx]; g += data[idx + 1]; b += data[idx + 2];
                    count++;
                }
            }
            r = Math.floor(r / count); g = Math.floor(g / count); b = Math.floor(b / count);

            // 2.2 用平均颜色填充整个块(实现马赛克模糊)
            for (let dy = 0; dy < mosaicStrength && y + dy < height; dy++) {
                for (let dx = 0; dx < mosaicStrength && x + dx < width; dx++) {
                    const idx = ((y + dy) * width + (x + dx)) * 4;
                    data[idx] = r; data[idx + 1] = g; data[idx + 2] = b;
                }
            }
        }
    }

    // 3. 将处理后的像素放回Canvas,显示马赛克效果
    ctx.putImageData(imageData, left, top);
}

作用:实现 "模糊效果" 的核心,通过 "块平均色填充" 模拟马赛克,强度可通过滑块动态调整。

  1. 区域交互逻辑(用户体验核心)

通过状态变量(isSelecting/isMoving/isResizing)避免交互冲突,支持 "选择 - 移动 - 缩放" 全流程:

选择区域:鼠标拖拽时记录坐标,显示 selectionRect,松开后调用 applyMosaicToArea 生成马赛克;

移动区域:点击 mosaicArea(非手柄)时触发 isMoving,鼠标移动时计算偏移量,更新区域位置并重新应用马赛克;

缩放区域:点击 resize-handle 时触发 isResizing,根据手柄方向(如 nw= 左上)调整区域尺寸,边界校验避免超出 Canvas。

关键代码片段(状态管理):

js 复制代码
// 避免交互冲突:同一时间仅允许一种操作
function handleMovingOrResizing(e) {
    if (isMoving) { /* 处理移动 */ }
    else if (isResizing) { /* 处理缩放 */ }
}
// 结束操作时重置状态
function stopMovingOrResizing() {
    isMoving = false;
    isResizing = false;
}
  1. 结果输出逻辑(功能闭环)
js 复制代码
saveBtn.addEventListener('click', () => {
    loading.style.display = 'block'; // 显示加载动画
    setTimeout(() => {
        // 将Canvas内容转为PNG格式的DataURL
        const dataURL = previewCanvas.toDataURL('image/png');
        // 赋值给下载链接,触发下载
        downloadBtn.href = dataURL;
        downloadBtn.download = 'mosaic-image.png';
        // 显示结果区,隐藏加载
        resultContainer.style.display = 'block';
        loading.style.display = 'none';
    }, 800); // 模拟处理延迟,提升用户感知
});

作用:完成 "处理 - 输出" 闭环,通过 toDataURL 实现前端图片生成,无需后端即可下载。


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

相关推荐
道可到3 小时前
35 岁程序员的绝地求生计划:你准备好了吗?
前端·后端·面试
道可到3 小时前
国内最难入职的 IT 公司排行:你敢挑战哪一家?
前端·后端·面试
jnpfsoft3 小时前
低代码应用菜单避坑指南:新建 / 删除 / 导入全流程,路由重复再也不怕!
前端·低代码
Keepreal4963 小时前
word文件预览实现
前端·javascript·react.js
郝开3 小时前
5. React中的组件:组件是什么;React定义组件
前端·javascript·react.js
我是天龙_绍3 小时前
uniapp 中的 #ifndef 条件编译
前端
斜向生3 小时前
【JavaScript正则表达式指南】——字符类(集合、范围、预定义字符类)和反向字符类详解
javascript
white-persist3 小时前
SQL 注入详解:从原理到实战
前端·网络·数据库·sql·安全·web安全·原型模式
FuckPatience3 小时前
电脑所有可用的网络接口
前端·javascript·vue.js