“Introducing a Vanilla JavaScript Image Editor!
Preview the exciting features of this web-based image editor, crafted with plain JavaScript. Users can:
Apply stunning filters: grayscale, inversion, saturation, and brightness adjustment
Rotate and flip images with ease
Save edited images
Get ready to experience a sleek and user-friendly image editing tool. Check out the demo to see it in action!”
Before diving into this image editor project, consider building some foundational JavaScript projects to solidify your skills and ensure a smooth learning experience. This will help you develop a strong understanding of JavaScript fundamentals, making it easier to tackle more complex projects like this one.
However, if you’re already comfortable with JavaScript and have a good grasp of its concepts, you’ll likely find the code and logic in this project straightforward. The project’s code is well-structured, and the logic is easy to follow.
One area that might require some extra attention is the Canvas API, which is used to save the edited image. But don’t worry, this is a relatively small part of the project, and with some patience and practice, you’ll get the hang of it.
To ensure you fully understand the code and can successfully complete the project, be sure to:
Watch the accompanying video multiple times to grasp the key concepts and code explanations.
Take advantage of the written comments throughout the code, which provide additional context and explanations.
Practice building small projects and experimenting with different code snippets to reinforce your learning.
By following these steps and being diligent in your learning, you’ll be well on your way to creating a fantastic image editor and expanding your JavaScript skills.
Index.html
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Image Editor in JavaScript </title>
<link rel="stylesheet" href="style.css">
<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"/>
</head>
<body>
<div class="container disable">
<h2>Easy Image Editor</h2>
<div class="wrapper">
<div class="editor-panel">
<div class="filter">
<label class="title">Filters</label>
<div class="options">
<button id="brightness" class="active">Brightness</button>
<button id="saturation">Saturation</button>
<button id="inversion">Inversion</button>
<button id="grayscale">Grayscale</button>
</div>
<div class="slider">
<div class="filter-info">
<p class="name">Brighteness</p>
<p class="value">100%</p>
</div>
<input type="range" value="100" min="0" max="200">
</div>
</div>
<div class="rotate">
<label class="title">Rotate & Flip</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="image-placeholder.svg" alt="preview-img">
</div>
</div>
<div class="controls">
<button class="reset-filter">Reset Filters</button>
<div class="row">
<input type="file" class="file-input" accept="image/*" hidden>
<button class="choose-img">Choose Image</button>
<button class="save-img">Save Image</button>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
Styles.css
/* 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;
}
.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;
}
}
Script.js
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");
// Initial filter values and transformations
let brightness = "100", saturation = "100", inversion = "0", grayscale = "0";
let rotate = 0, flipHorizontal = 1, flipVertical = 1;
// Load image from file input and display in preview
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");
});
}
// Apply current filter settings and transformations to preview image
const applyFilter = () => {
previewImg.style.transform = `rotate(${rotate}deg) scale(${flipHorizontal}, ${flipVertical})`;
previewImg.style.filter = `brightness(${brightness}%) saturate(${saturation}%) invert(${inversion}%) grayscale(${grayscale}%)`;
}
// Set up filter options event listeners
filterOptions.forEach(option => {
option.addEventListener("click", () => {
document.querySelector(".active").classList.remove("active");
option.classList.add("active");
filterName.innerText = option.innerText;
// Set slider max value and current value based on selected filter
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}%`;
}
});
});
// Update filter values and apply changes when slider input changes
const updateFilter = () => {
filterValue.innerText = `${filterSlider.value}%`;
const selectedFilter = document.querySelector(".filter .active");
// Update the relevant filter value based on the selected filter
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(); // Apply updated filter settings
}
// Set up rotation and flip options event listeners
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(); // Apply updated transformations
});
});
// Reset all filters and transformations to default values
const resetFilter = () => {
brightness = "100"; saturation = "100"; inversion = "0"; grayscale = "0";
rotate = 0; flipHorizontal = 1; flipVertical = 1;
filterOptions[0].click();
applyFilter(); // Apply default settings
}
// Save the current image with applied filters and transformations
const saveImage = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
// Set canvas dimensions to match the image
canvas.width = previewImg.naturalWidth;
canvas.height = previewImg.naturalHeight;
// Apply current filter settings
ctx.filter = `brightness(${brightness}%) saturate(${saturation}%) invert(${inversion}%) grayscale(${grayscale}%)`;
// Center the canvas context
ctx.translate(canvas.width / 2, canvas.height / 2);
// Apply rotation if needed
if (rotate !== 0) {
ctx.rotate(rotate * Math.PI / 180);
}
// Apply scaling for flips
ctx.scale(flipHorizontal, flipVertical);
// Draw the image centered on the canvas
ctx.drawImage(previewImg, -canvas.width / 2, -canvas.height / 2, canvas.width, canvas.height);
// Create a download link and trigger the download
const link = document.createElement("a");
link.download = "image.jpg";
link.href = canvas.toDataURL();
link.click();
}
// Set up event listeners for UI controls
filterSlider.addEventListener("input", updateFilter);
resetFilterBtn.addEventListener("click", resetFilter);
saveImgBtn.addEventListener("click", saveImage);
fileInput.addEventListener("change", loadImage);
chooseImgBtn.addEventListener("click", () => fileInput.click());