图片马赛克是怎么实现?下面的案例告诉你。该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
- 图片上传与渲染(入口功能)
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 尺寸,确保后续处理与显示正常。
- 马赛克核心算法(技术核心)
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);
}
作用:实现 "模糊效果" 的核心,通过 "块平均色填充" 模拟马赛克,强度可通过滑块动态调整。
- 区域交互逻辑(用户体验核心)
通过状态变量(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;
}
- 结果输出逻辑(功能闭环)
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 实现前端图片生成,无需后端即可下载。
各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!