<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">ImageFilter · 图像滤镜效果</title>
<!-- Font Awesome for icons (optional but adds style) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0b0d15;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
.card {
max-width: 880px;
width: 100%;
background: #14171f;
border-radius: 2rem;
padding: 1.8rem 1.8rem 2rem;
box-shadow: 0 20px 40px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.03);
transition: box-shadow 0.2s;
}
h1 {
font-size: 1.8rem;
font-weight: 500;
letter-spacing: -0.01em;
display: flex;
align-items: center;
gap: 0.6rem;
color: #eef2f6;
margin-bottom: 1.2rem;
border-bottom: 1px solid #2a2f3a;
padding-bottom: 0.75rem;
}
h1 i {
color: #5f7cff;
font-size: 1.6rem;
}
.main-layout {
display: flex;
flex-direction: row;
gap: 1.5rem;
flex-wrap: wrap;
}
.image-section {
flex: 2;
min-width: 260px;
}
.canvas-wrapper {
background: #0e1017;
border-radius: 1.2rem;
padding: 0.5rem;
box-shadow: inset 0 2px 6px rgba(0,0,0,0.6);
display: flex;
justify-content: center;
align-items: center;
border: 1px solid #2a2f3a;
}
#imageCanvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 1/1;
object-fit: contain;
background: #0a0c12;
border-radius: 0.8rem;
image-rendering: auto;
transition: filter 0.2s ease;
}
.controls-section {
flex: 1.2;
min-width: 200px;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.filter-group {
background: #1c212b;
border-radius: 1.2rem;
padding: 1.2rem 1rem 1rem;
border: 1px solid #2d3340;
}
.filter-group h3 {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #8f99b0;
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 6px;
}
.filter-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.filter-btn {
background: #262d3a;
border: none;
padding: 0.5rem 0.2rem;
border-radius: 0.8rem;
color: #d6deee;
font-size: 0.75rem;
font-weight: 450;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
transition: all 0.15s;
border: 1px solid transparent;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.filter-btn i {
font-size: 0.8rem;
color: #8a9ac0;
}
.filter-btn:hover {
background: #313b4b;
border-color: #5a6e92;
color: white;
transform: scale(0.97);
}
.filter-btn.active {
background: #3f4d6b;
border-color: #6f8cff;
box-shadow: 0 0 0 1px #5f7cff;
color: white;
}
.filter-btn.active i {
color: #b6c8ff;
}
.action-row {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
}
.action-btn {
background: #262d3a;
border: 1px solid #323a49;
padding: 0.6rem 0.9rem;
border-radius: 2rem;
color: #d6deee;
font-size: 0.8rem;
display: flex;
align-items: center;
gap: 0.4rem;
cursor: pointer;
transition: 0.15s;
flex: 1 0 auto;
justify-content: center;
}
.action-btn:hover {
background: #323d52;
border-color: #5f7cff;
color: white;
}
.action-btn i {
color: #7f92bb;
}
.upload-label {
background: #2d3648;
border: 1px dashed #5a6e92;
padding: 0.6rem 0.9rem;
border-radius: 2rem;
color: #d6deee;
font-size: 0.8rem;
display: flex;
align-items: center;
gap: 0.4rem;
cursor: pointer;
transition: 0.15s;
flex: 1 0 auto;
justify-content: center;
}
.upload-label:hover {
background: #384461;
border-color: #6f8cff;
color: white;
}
#fileInput {
display: none;
}
.footer-note {
margin-top: 1rem;
color: #5d6a82;
font-size: 0.7rem;
text-align: center;
border-top: 1px solid #232833;
padding-top: 1rem;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
@media (max-width: 600px) {
.card { padding: 1rem; }
.filter-grid { grid-template-columns: 1fr 1fr; }
}
</style>
</head>
<body>
<div class="card">
<h1>
<i class="fas fa-image"></i>
ImageFilter · 滤镜
</h1>
<div class="main-layout">
<!-- 图像展示区 -->
<div class="image-section">
<div class="canvas-wrapper">
<canvas id="imageCanvas" width="600" height="600"></canvas>
</div>
<!-- 辅助操作 -->
<div class="action-row" style="margin-top: 0.8rem;">
<label for="fileInput" class="upload-label">
<i class="fas fa-cloud-upload-alt"></i> 上传图片
</label>
<button class="action-btn" id="resetBtn"><i class="fas fa-undo-alt"></i> 重置</button>
<button class="action-btn" id="downloadBtn"><i class="fas fa-download"></i> 保存</button>
</div>
</div>
<!-- 控制区 -->
<div class="controls-section">
<div class="filter-group">
<h3><i class="fas fa-fill-drip"></i> 经典滤镜</h3>
<div class="filter-grid" id="filterGrid">
<button class="filter-btn active" data-filter="original"><i class="fas fa-circle"></i> 原图</button>
<button class="filter-btn" data-filter="grayscale"><i class="fas fa-adjust"></i> 灰度</button>
<button class="filter-btn" data-filter="sepia"><i class="fas fa-mug-hot"></i> 复古</button>
<button class="filter-btn" data-filter="invert"><i class="fas fa-circle-notch"></i> 反色</button>
<button class="filter-btn" data-filter="vintage"><i class="fas fa-camera-retro"></i> 怀旧</button>
<button class="filter-btn" data-filter="cool"><i class="fas fa-snowflake"></i> 冷色调</button>
<button class="filter-btn" data-filter="warm"><i class="fas fa-sun"></i> 暖色调</button>
<button class="filter-btn" data-filter="vivid"><i class="fas fa-fire"></i> 鲜艳</button>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 0.6rem;">
<button class="action-btn" id="randomFilterBtn" style="background: #2a3349;"><i class="fas fa-random"></i> 随机滤镜</button>
</div>
</div>
</div>
<div class="footer-note">
<span><i class="far fa-images"></i> 点击滤镜即时预览</span>
<span><i class="fas fa-mouse-pointer"></i> 支持 JPG / PNG</span>
</div>
</div>
<script>
(function() {
// DOM 元素
const canvas = document.getElementById('imageCanvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const fileInput = document.getElementById('fileInput');
const filterBtns = document.querySelectorAll('.filter-btn');
const resetBtn = document.getElementById('resetBtn');
const downloadBtn = document.getElementById('downloadBtn');
const randomFilterBtn = document.getElementById('randomFilterBtn');
// 状态
let currentFilter = 'original';
let originalImageData = null; // 存储原始图像数据 (用于重置)
let isImageLoaded = false;
// ---------- 默认图片 (内置渐变/几何,确保有图可看) ----------
function drawDefaultImage() {
const w = canvas.width, h = canvas.height;
ctx.clearRect(0, 0, w, h);
// 绘制渐变圆形 + 星形 + 条纹 (方便观察滤镜效果)
const grd = ctx.createRadialGradient(w*0.3, h*0.3, 10, w*0.5, h*0.5, w*0.6);
grd.addColorStop(0, '#ff7eb3');
grd.addColorStop(0.3, '#ff758c');
grd.addColorStop(0.7, '#5f7cff');
grd.addColorStop(1, '#2c2f4a');
ctx.fillStyle = grd;
ctx.fillRect(0, 0, w, h);
// 几何图形
ctx.beginPath();
ctx.arc(w*0.7, h*0.25, 45, 0, Math.PI*2);
ctx.fillStyle = '#ffdd77';
ctx.shadowColor = 'rgba(255,200,50,0.5)';
ctx.shadowBlur = 28;
ctx.fill();
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.arc(w*0.2, h*0.7, 35, 0, Math.PI*2);
ctx.fillStyle = '#6dd5a0';
ctx.shadowBlur = 20;
ctx.shadowColor = '#3e9c7a';
ctx.fill();
ctx.shadowBlur = 0;
// 条纹
ctx.fillStyle = '#ffffff30';
for (let i=0; i<6; i++) {
ctx.fillRect(i*28+10, h*0.8, 10, 30);
}
// 文字
ctx.font = 'bold 26px "Inter", system-ui, sans-serif';
ctx.fillStyle = '#f0f4ff';
ctx.shadowColor = '#00000060';
ctx.shadowBlur = 12;
ctx.fillText('滤镜', w*0.1, h*0.45);
ctx.shadowBlur = 0;
// 保存为原始数据
originalImageData = ctx.getImageData(0, 0, w, h);
isImageLoaded = true;
}
// ---------- 加载图片到 canvas ----------
function loadImageToCanvas(file) {
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
const w = canvas.width, h = canvas.height;
ctx.clearRect(0, 0, w, h);
// 绘制到canvas (保持比例,居中裁剪)
const imgRatio = img.width / img.height;
const canvasRatio = w / h;
let sx, sy, sWidth, sHeight;
if (imgRatio > canvasRatio) {
// 图片更宽:裁剪左右
sHeight = img.height;
sWidth = img.height * canvasRatio;
sx = (img.width - sWidth) / 2;
sy = 0;
} else {
sWidth = img.width;
sHeight = img.width / canvasRatio;
sx = 0;
sy = (img.height - sHeight) / 2;
}
ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, 0, w, h);
// 保存原始数据
originalImageData = ctx.getImageData(0, 0, w, h);
isImageLoaded = true;
// 应用当前滤镜 (默认为original)
applyFilter(currentFilter);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// ---------- 核心滤镜处理 ----------
function applyFilter(filterType) {
if (!originalImageData) {
// 如果没有原始数据 (极少情况) 重置默认
drawDefaultImage();
return;
}
// 复制原始数据 (避免累积极端修改)
const srcData = new Uint8ClampedArray(originalImageData.data);
const w = canvas.width, h = canvas.height;
const frame = new ImageData(srcData, w, h);
const output = ctx.createImageData(w, h);
const data = output.data;
// 根据滤镜类型处理像素
if (filterType === 'original') {
// 直接复制原始数据
output.data.set(originalImageData.data);
}
else if (filterType === 'grayscale') {
for (let i = 0; i < srcData.length; i += 4) {
const r = srcData[i], g = srcData[i+1], b = srcData[i+2];
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
data[i] = gray;
data[i+1] = gray;
data[i+2] = gray;
data[i+3] = srcData[i+3];
}
}
else if (filterType === 'sepia') {
for (let i = 0; i < srcData.length; i += 4) {
const r = srcData[i], g = srcData[i+1], b = srcData[i+2];
let tr = 0.393 * r + 0.769 * g + 0.189 * b;
let tg = 0.349 * r + 0.686 * g + 0.168 * b;
let tb = 0.272 * r + 0.534 * g + 0.131 * b;
data[i] = Math.min(255, tr);
data[i+1] = Math.min(255, tg);
data[i+2] = Math.min(255, tb);
data[i+3] = srcData[i+3];
}
}
else if (filterType === 'invert') {
for (let i = 0; i < srcData.length; i += 4) {
data[i] = 255 - srcData[i];
data[i+1] = 255 - srcData[i+1];
data[i+2] = 255 - srcData[i+2];
data[i+3] = srcData[i+3];
}
}
else if (filterType === 'vintage') {
// 复古暖黄 + 降低饱和度 + 对比略提升
for (let i = 0; i < srcData.length; i += 4) {
let r = srcData[i], g = srcData[i+1], b = srcData[i+2];
// 褪色 + 暖色
r = r * 0.9 + 25;
g = g * 0.8 + 15;
b = b * 0.6 + 10;
// 提高对比
r = (r > 128) ? Math.min(255, r*1.1) : r*0.9;
g = (g > 128) ? Math.min(255, g*1.05) : g*0.9;
b = (b > 128) ? Math.min(255, b*0.9) : b*0.85;
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));
data[i+3] = srcData[i+3];
}
}
else if (filterType === 'cool') {
// 冷色调:蓝色增强,红色减弱
for (let i = 0; i < srcData.length; i += 4) {
let r = srcData[i], g = srcData[i+1], b = srcData[i+2];
r = r * 0.7;
g = g * 0.85;
b = Math.min(255, b * 1.25 + 10);
data[i] = Math.min(255, r);
data[i+1] = Math.min(255, g);
data[i+2] = Math.min(255, b);
data[i+3] = srcData[i+3];
}
}
else if (filterType === 'warm') {
// 暖色调:红色增强,蓝色减弱
for (let i = 0; i < srcData.length; i += 4) {
let r = srcData[i], g = srcData[i+1], b = srcData[i+2];
r = Math.min(255, r * 1.2 + 10);
g = g * 1.02;
b = b * 0.7;
data[i] = Math.min(255, r);
data[i+1] = Math.min(255, g);
data[i+2] = Math.min(255, b);
data[i+3] = srcData[i+3];
}
}
else if (filterType === 'vivid') {
// 鲜艳:饱和度大增,对比增强
for (let i = 0; i < srcData.length; i += 4) {
let r = srcData[i], g = srcData[i+1], b = srcData[i+2];
const avg = (r + g + b) / 3;
const amount = 1.5;
r = avg + amount * (r - avg);
g = avg + amount * (g - avg);
b = avg + amount * (b - avg);
// 对比加一点
r = (r > 128) ? Math.min(255, r * 1.08) : r * 0.92;
g = (g > 128) ? Math.min(255, g * 1.08) : g * 0.92;
b = (b > 128) ? Math.min(255, b * 1.08) : b * 0.92;
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));
data[i+3] = srcData[i+3];
}
}
// 绘制到canvas
ctx.putImageData(output, 0, 0);
}
// ---------- 切换滤镜 (更新UI + 应用) ----------
function setFilter(filterType) {
currentFilter = filterType;
// 更新按钮状态
filterBtns.forEach(btn => {
const f = btn.dataset.filter;
btn.classList.toggle('active', f === filterType);
});
applyFilter(filterType);
}
// ---------- 重置为原始图像 ----------
function resetToOriginal() {
if (originalImageData) {
ctx.putImageData(originalImageData, 0, 0);
setFilter('original');
// 设置active状态
filterBtns.forEach(btn => btn.classList.toggle('active', btn.dataset.filter === 'original'));
} else {
drawDefaultImage();
setFilter('original');
}
}
// ---------- 随机滤镜 ----------
function applyRandomFilter() {
const filterList = ['grayscale', 'sepia', 'invert', 'vintage', 'cool', 'warm', 'vivid'];
// 排除 original 让体验更有趣, 但也可加入, 这里保留趣味性
const randomIdx = Math.floor(Math.random() * filterList.length);
const randomFilter = filterList[randomIdx];
setFilter(randomFilter);
}
// ---------- 下载处理后的图片 ----------
function downloadImage() {
// 使用 canvas 内容
const link = document.createElement('a');
link.download = `imagefilter_${currentFilter}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
}
// ---------- 事件绑定 ----------
// 文件上传
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
loadImageToCanvas(file);
// 切换回原图显示 (但loadImageToCanvas内部会应用当前currentFilter,我们要重置active)
// 在loadImageToCanvas最后会调用applyFilter(currentFilter),但此时originalImageData已经更新。
// 我们将currentFilter强制设为original 使显示原图
currentFilter = 'original';
filterBtns.forEach(btn => btn.classList.toggle('active', btn.dataset.filter === 'original'));
// 由于loadImageToCanvas里调用了applyFilter(currentFilter),但currentFilter刚改成original,所以显示原图
// 但注意:loadImageToCanvas中调用applyFilter在img.onload里,此时currentFilter已经变为original
// 需要保证顺序: 先设置currentFilter = 'original', 再触发loadImageToCanvas
// 但loadImageToCanvas是异步,在reader.onload里,所以先改currentFilter可行
// 但因loadImageToCanvas内部调用applyFilter时使用currentFilter,就是original。
// 但是为了让UI同步,手动再设一下class
// 不过由于可能存在异步,我们在loadImageToCanvas内部调用的applyFilter会再次设置,没问题。
// 但是也要考虑原始图片加载完成后应用滤镜,稳妥:手动调用setFilter('original') 但是会覆盖。
// 简单重新设置一下样式
setTimeout(() => {
setFilter('original');
}, 20);
}
});
// 滤镜按钮点击
filterBtns.forEach(btn => {
btn.addEventListener('click', function() {
const filter = this.dataset.filter;
setFilter(filter);
});
});
// 重置
resetBtn.addEventListener('click', resetToOriginal);
// 下载
downloadBtn.addEventListener('click', downloadImage);
// 随机滤镜
randomFilterBtn.addEventListener('click', applyRandomFilter);
// ---------- 初始化 (绘制默认图) ----------
drawDefaultImage();
setFilter('original');
})();
</script>
</body>
</html>
滤镜交互与操作说明
您可以通过点击滤镜按钮或上传图片,轻松探索不同视觉效果,其核心交互逻辑清晰直观。

- 滤镜选择与预览:页面提供了“灰度”、“复古”、“反色”等8种经典滤镜,您只需点击任意滤镜按钮,画布上的图像便会实时应用该效果,方便您快速对比和选择。
- 图像加载与重置:您可以通过“上传图片”按钮导入本地图片进行处理,若想恢复原始状态,点击“重置”按钮即可清除所有滤镜效果,回到图像最初的样子。
- 结果保存与随机探索:处理完成后,点击“保存”按钮可将当前滤镜效果的图像下载为PNG文件。“随机滤镜”功能为您提供了一种发现意外视觉效果的有趣方式。