效果图:(知识点和完整代码在最后面)
完整代码:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>【前端实例代码】用HTML、CSS和JavaScript创建一个简易图片编辑器(实现图片的亮度、饱和度、灰度、颜色反转、图片旋转镜面翻转等滤镜效果)</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/css/boxicons.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css"/>
<style>
/* Import Google font - Poppins */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
body {
display: flex;
padding: 10px;
min-height: 100vh;
align-items: center;
justify-content: center;
background: #E3F2FD;
background: linear-gradient(135deg, #4AB1FF, #2D5CFE);
}
.container {
width: 850px;
padding: 30px 35px 35px;
background: #fff;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.container.disable .editor-panel,
.container.disable .controls .reset-filter,
.container.disable .controls .save-img {
opacity: 0.5;
pointer-events: none;
}
.container h2 {
margin-top: -8px;
font-size: 22px;
font-weight: 500;
}
.container .wrapper {
display: flex;
margin: 20px 0;
min-height: 335px;
}
.wrapper .editor-panel {
padding: 15px 20px;
width: 280px;
border-radius: 5px;
border: 1px solid #ccc;
}
.editor-panel .title {
display: block;
font-size: 16px;
margin-bottom: 12px;
}
.editor-panel .options, .controls {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.editor-panel button {
outline: none;
height: 40px;
font-size: 14px;
color: #6C757D;
background: #fff;
border-radius: 3px;
margin-bottom: 8px;
border: 1px solid #aaa;
}
.editor-panel .filter button {
width: calc(100% / 2 - 4px);
}
.editor-panel button:hover {
background: #f5f5f5;
}
.filter button.active {
color: #fff;
border-color: #5372F0;
background: #5372F0;
}
.filter .slider {
margin-top: 12px;
}
.filter .slider .filter-info {
display: flex;
color: #464646;
font-size: 14px;
justify-content: space-between;
}
.filter .slider input {
width: 100%;
height: 5px;
accent-color: #5372F0;
}
.editor-panel .rotate {
margin-top: 17px;
}
.editor-panel .rotate button {
display: flex;
align-items: center;
justify-content: center;
width: calc(100% / 4 - 3px);
}
.rotate .options button:nth-child(3),
.rotate .options button:nth-child(4) {
font-size: 18px;
}
.rotate .options button:active {
color: #fff;
background: #5372F0;
border-color: #5372F0;
}
.wrapper .preview-img {
flex-grow: 1;
display: flex;
overflow: hidden;
margin-left: 20px;
border-radius: 5px;
align-items: center;
justify-content: center;
}
.preview-img img {
max-width: 490px;
max-height: 335px;
width: 100%;
height: 100%;
object-fit: contain;
}
.controls button {
padding: 11px 20px;
font-size: 14px;
border-radius: 3px;
outline: none;
color: #fff;
cursor: pointer;
background: none;
transition: all 0.3s ease;
text-transform: uppercase;
}
.controls .reset-filter {
color: #6C757D;
border: 1px solid #6C757D;
}
.controls .reset-filter:hover {
color: #fff;
background: #6C757D;
}
.controls .choose-img {
background: #6C757D;
border: 1px solid #6C757D;
}
.controls .save-img {
margin-left: 5px;
background: #5372F0;
border: 1px solid #5372F0;
}
@media screen and (max-width: 760px) {
.container {
padding: 25px;
}
.container .wrapper {
flex-wrap: wrap-reverse;
}
.wrapper .editor-panel {
width: 100%;
}
.wrapper .preview-img {
width: 100%;
margin: 0 0 15px;
}
}
@media screen and (max-width: 500px) {
.controls button {
width: 100%;
margin-bottom: 10px;
}
.controls .row {
width: 100%;
}
.controls .row .save-img {
margin-left: 0px;
}
}
</style>
</head>
<body>
<div class="container disable">
<h2>简易图片编辑器</h2>
<div class="wrapper">
<div class="editor-panel">
<div class="filter">
<label class="title">滤镜</label>
<div class="options">
<button id="brightness" class="active">亮度</button>
<button id="saturation">饱和度</button>
<button id="inversion">反转</button>
<button id="grayscale">灰度</button>
</div>
<div class="slider">
<div class="filter-info">
<p class="name">明亮度</p>
<p class="value">100%</p>
</div>
<input type="range" value="100" min="0" max="200">
</div>
</div>
<div class="rotate">
<label class="title">旋转和翻转</label>
<div class="options">
<button id="left"><i class="fa-solid fa-rotate-left"></i></button>
<button id="right"><i class="fa-solid fa-rotate-right"></i></button>
<button id="horizontal"><i class='bx bx-reflect-vertical'></i></button>
<button id="vertical"><i class='bx bx-reflect-horizontal'></i></button>
</div>
</div>
</div>
<div class="preview-img">
<img src="https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg" alt="preview-img">
</div>
</div>
<div class="controls">
<button class="reset-filter">重置</button>
<div class="row">
<input type="file" class="file-input" accept="image/*" hidden>
<button class="choose-img">选择图片</button>
<button class="save-img">保存图片</button>
</div>
</div>
</div>
<script>
const fileInput = document.querySelector(".file-input"),
filterOptions = document.querySelectorAll(".filter button"),
filterName = document.querySelector(".filter-info .name"),
filterValue = document.querySelector(".filter-info .value"),
filterSlider = document.querySelector(".slider input"),
rotateOptions = document.querySelectorAll(".rotate button"),
previewImg = document.querySelector(".preview-img img"),
resetFilterBtn = document.querySelector(".reset-filter"),
chooseImgBtn = document.querySelector(".choose-img"),
saveImgBtn = document.querySelector(".save-img");
let brightness = "100", saturation = "100", inversion = "0", grayscale = "0";
let rotate = 0, flipHorizontal = 1, flipVertical = 1;
const loadImage = () => {
let file = fileInput.files[0];
if (!file) return;
previewImg.src = URL.createObjectURL(file);
previewImg.addEventListener("load", () => {
resetFilterBtn.click();
document.querySelector(".container").classList.remove("disable");
});
}
const applyFilter = () => {
previewImg.style.transform = `rotate(${rotate}deg) scale(${flipHorizontal}, ${flipVertical})`;
previewImg.style.filter = `brightness(${brightness}%) saturate(${saturation}%) invert(${inversion}%) grayscale(${grayscale}%)`;
}
filterOptions.forEach(option => {
option.addEventListener("click", () => {
document.querySelector(".active").classList.remove("active");
option.classList.add("active");
filterName.innerText = option.innerText;
if (option.id === "brightness") {
filterSlider.max = "200";
filterSlider.value = brightness;
filterValue.innerText = `${brightness}%`;
} else if (option.id === "saturation") {
filterSlider.max = "200";
filterSlider.value = saturation;
filterValue.innerText = `${saturation}%`
} else if (option.id === "inversion") {
filterSlider.max = "100";
filterSlider.value = inversion;
filterValue.innerText = `${inversion}%`;
} else {
filterSlider.max = "100";
filterSlider.value = grayscale;
filterValue.innerText = `${grayscale}%`;
}
});
});
const updateFilter = () => {
filterValue.innerText = `${filterSlider.value}%`;
const selectedFilter = document.querySelector(".filter .active");
if (selectedFilter.id === "brightness") {
brightness = filterSlider.value;
} else if (selectedFilter.id === "saturation") {
saturation = filterSlider.value;
} else if (selectedFilter.id === "inversion") {
inversion = filterSlider.value;
} else {
grayscale = filterSlider.value;
}
applyFilter();
}
rotateOptions.forEach(option => {
option.addEventListener("click", () => {
if (option.id === "left") {
rotate -= 90;
} else if (option.id === "right") {
rotate += 90;
} else if (option.id === "horizontal") {
flipHorizontal = flipHorizontal === 1 ? -1 : 1;
} else {
flipVertical = flipVertical === 1 ? -1 : 1;
}
applyFilter();
});
});
const resetFilter = () => {
brightness = "100";
saturation = "100";
inversion = "0";
grayscale = "0";
rotate = 0;
flipHorizontal = 1;
flipVertical = 1;
filterOptions[0].click();
applyFilter();
}
const saveImage = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = previewImg.naturalWidth;
canvas.height = previewImg.naturalHeight;
ctx.filter = `brightness(${brightness}%) saturate(${saturation}%) invert(${inversion}%) grayscale(${grayscale}%)`;
ctx.translate(canvas.width / 2, canvas.height / 2);
if (rotate !== 0) {
ctx.rotate(rotate * Math.PI / 180);
}
ctx.scale(flipHorizontal, flipVertical);
ctx.drawImage(previewImg, -canvas.width / 2, -canvas.height / 2, canvas.width, canvas.height);
const link = document.createElement("a");
link.download = "image.jpg";
link.href = canvas.toDataURL();
link.click();
}
filterSlider.addEventListener("input", updateFilter);
resetFilterBtn.addEventListener("click", resetFilter);
saveImgBtn.addEventListener("click", saveImage);
fileInput.addEventListener("change", loadImage);
chooseImgBtn.addEventListener("click", () => fileInput.click());
</script>
</body>
</html>
知识点:
涉及到的知识点:
1、HTML DOM querySelector() 方法
querySelector() 方法返回文档中匹配指定 CSS 选择器的一个元素。
注意: querySelector() 方法仅仅返回匹配指定选择器的第一个元素。如果你需要返回所有的元素,请使用 querySelectorAll() 方法替代。
语法
document.querySelector(CSS selectors)
参数值
参数 | 类型 | 描述 |
---|---|---|
CSS 选择器 | String | 必须。指定一个或多个匹配元素的 CSS 选择器。 可以使用它们的 id, 类, 类型, 属性, 属性值等来选取元素。 对于多个选择器,使用逗号隔开,返回一个匹配的元素。 提示: 更多 CSS 选择器,请参阅我们的 CSS 选择器参考手册。 |
技术细节
DOM 版本: | Selectors Level 1 Document Object |
---|---|
返回值: | 匹配指定 CSS 选择器的第一个元素。 如果没有找到,返回 null。如果指定了非法选择器则 抛出 SYNTAX_ERR 异常。 |
推荐阅读:
2、HTML DOM addEventListener() 方法
定义和用法
addEventListener() 方法用于向指定元素添加事件句柄。
提示: 使用 removeEventListener() 方法来移除 addEventListener() 方法添加的事件句柄。
语法
element.addEventListener(event, function, useCapture)
参数值
参数 | 描述 |
---|---|
event | 必须。字符串,指定事件名。 注意: 不要使用 "on" 前缀。 例如,使用 "click" ,而不是使用 "onclick"。 提示: 所有 HTML DOM 事件,可以查看我们完整的 HTML DOM Event 对象参考手册。 |
function | 必须。指定要事件触发时执行的函数。 当事件对象会作为第一个参数传入函数。 事件对象的类型取决于特定的事件。例如, "click" 事件属于 MouseEvent(鼠标事件) 对象。 |
useCapture | 可选。布尔值,指定事件是否在捕获或冒泡阶段执行。 可能值:
|
推荐阅读:
3、HTML DOM classList 属性
定义和用法
classList 属性返回元素的类名,作为 DOMTokenList 对象。
该属性用于在元素中添加,移除及切换 CSS 类。
classList 属性是只读的,但你可以使用 add() 和 remove() 方法修改它。
语法
element.classList
Properties
属性 | Description |
---|---|
length | 返回类列表中类的数量 该属性是只读的 |
推荐阅读:
4、HTML DOM innerHTML 属性
定义和用法
innerHTML 属性设置或返回表格行的开始和结束标签之间的 HTML。
语法
HTMLElementObject.innerHTML=text
4、forEach() 方法
5、map() 方法
6、filter()方法
7、join()方法
8、toLowerCase()方法
9、es6模板字符串
待补充