How To Create a Photo Editor Application in JavaScript, HTML and CSS
In this JavaScript Tutorial, we will see how to create a Photo editing application using JavaScript, HTML5, CSS3.
This photo editor is a single-page application that provides editing features like filters, brightness/contrast adjustment, cropping, and image manipulation.
What We Are Gonna Use In This Project:
- JavaScript Programming Language.- HTML and CSS.
- Font-awesome.
- Visual Studio Editor.
Project Source Code:
- Menu Toggle
Toggles the mobile sidebar menu open/closed and switches between hamburger and X icons.
// Step 4: Set up what happens when someone clicks the hamburger menu button
menuToggle.addEventListener('click', () => {
// Toggle the menu open or closed
toolbar.classList.toggle('open');
overlay.classList.toggle('active');
// When opening the menu, make sure we're scrolled to the top
if (toolbar.classList.contains('open')) {
toolbar.scrollTop = 0;
}
// Change the hamburger icon to an X when open, or back to hamburger when closed
if (toolbar.classList.contains('open')) {
menuToggle.innerHTML = '<i class="fas fa-times"></i>'; // X icon
} else {
menuToggle.innerHTML = '<i class="fas fa-bars"></i>'; // Hamburger icon
}
});
- Overlay Click
Closes the mobile menu when user clicks outside the sidebar area.
// Step 5: Allow users to close the menu by clicking anywhere outside it
overlay.addEventListener('click', () => {
toolbar.classList.remove('open'); // Close the menu
overlay.classList.remove('active'); // Hide the dark overlay
// Change icon back to hamburger
menuToggle.innerHTML = '<i class="fas fa-bars"></i>';
});
- Mobile Menu Auto-Close.
Automatically closes the mobile menu after tool selection on devices with screen width ≤ 768px.
// Step 6: Function to close the menu on mobile after selecting a tool
function closeMenuIfMobile() {
// Only close the menu if we're on a mobile device (screen width <= 768px)
if (window.innerWidth <= 768) {
toolbar.classList.remove('open');
overlay.classList.remove('active');
menuToggle.innerHTML = '<i class="fas fa-bars"></i>';
}
}
- Show and Hide Loading Spinner.
Shows a spinning loader icon during image processing operations.
Hides the loading spinner when image processing is complete.
// Step 7: Function to show the loading spinner when processing images
function showLoading() {
loading.style.display = 'block';
}
// Step 8: Function to hide the loading spinner when done processing
function hideLoading() {
loading.style.display = 'none';
}
- Slider Value Updater.
Updates the percentage display text next to brightness and contrast sliders in real-time.
// Step 9: Function to update the percentage text next to sliders
function updateSliderValue(sliderId, valueId) {
const slider = document.getElementById(sliderId);
const valueSpan = document.getElementById(valueId);
valueSpan.textContent = `${slider.value}%`; // Display current value with % sign
}
- Filter Application.
Applies all active filters (grayscale, invert, sepia, brightness, contrast) to the canvas image using CSS filter properties.
// Step 10: Apply brightness and contrast filters based on slider positions
function applyFilters() {
if (!img.src) return; // Don't do anything if no image is loaded
// Construct filter string using all active filters
let filterString = '';
if (activeFilters.grayscale > 0) filterString +=
`grayscale(${activeFilters.grayscale}%) `;
if (activeFilters.invert > 0) filterString +=
`invert(${activeFilters.invert}%) `;
if (activeFilters.sepia > 0) filterString +=
`sepia(${activeFilters.sepia}%) `;
filterString += `brightness(${activeFilters.brightness}%)
contrast(${activeFilters.contrast}%)`;
// Apply the combined filters
ctx.filter = filterString;
// Redraw the image with the new filters applied
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}
- Resize Canvas.
Dynamically resizes the canvas display to fit within the container while maintaining image aspect ratio.
// Step 11: Make sure the image fits nicely on screen without being too big or small
function resizeCanvasToFitScreen() {
if (!img.src) return; // Don't do anything if no image is loaded
// Find out how much space we have available
const containerWidth = document.getElementById('canvasContainer').clientWidth;
const containerHeight = document.getElementById('canvasContainer').clientHeight;
// Calculate the width-to-height ratio of the image
const aspectRatio = img.width / img.height;
// Start with the original image size
let newWidth = img.width;
let newHeight = img.height;
// If the image is wider than our container
// (with 10% margin), make it smaller
if (newWidth > containerWidth * 0.9) {
newWidth = containerWidth * 0.9;
// Keep the same width-to-height ratio
newHeight = newWidth / aspectRatio;
}
// If the image is taller than our container
// (with 10% margin), make it smaller
if (newHeight > containerHeight * 0.9) {
newHeight = containerHeight * 0.9;
// Keep the same width-to-height ratio
newWidth = newHeight * aspectRatio;
}
// Remember these dimensions for later comparison
currentCanvasWidth = newWidth;
currentCanvasHeight = newHeight;
// Update the display size of the canvas
// (visual only, not the actual pixel dimensions)
canvas.style.width = `${newWidth}px`;
canvas.style.height = `${newHeight}px`;
}
- Upload Image.
Creates a file input dialog, processes selected image files, and loads them onto the canvas while preserving original data.
// Step 12: Handle the Upload Image button
document.getElementById('uploadBtn').addEventListener('click', () => {
// Create an invisible file input element (the file picker dialog)
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*'; // Allow any image format
// When the user selects a file from their device
input.onchange = (e) => {
const file = e.target.files[0];
// If they canceled without selecting a file, do nothing
if (!file) return;
// This helps us read the selected file
const reader = new FileReader();
showLoading(); // Show the spinner while we load the image
// When the file finishes loading into memory
reader.onload = function(event) {
// Set our image source to the loaded file data
img.src = event.target.result;
// Once the image is fully processed and ready to use
img.onload = function() {
// Set the canvas size to match the actual image dimensions (not display size)
canvas.width = img.width;
canvas.height = img.height;
// Draw the image onto the canvas
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// Save a backup of the original image for the reset button
originalImage = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Reset sliders to default position (100%)
brightnessSlider.value = 100;
contrastSlider.value = 100;
// Update slider value displays
updateSliderValue('brightnessSlider', 'brightnessValue');
updateSliderValue('contrastSlider', 'contrastValue');
// Adjust the display size to fit nicely on screen
resizeCanvasToFitScreen();
hideLoading(); // Hide the spinner now that we're done
closeMenuIfMobile(); // Close the menu if on mobile
}
};
// Start reading the selected image file
reader.readAsDataURL(file);
};
// Open the file selection dialog
input.click();
});
- Grayscale Toggle.
Toggles black and white filter on/off for the current image.
// Step 13: Apply black & white filter when grayscale button is clicked
document.getElementById('grayscaleBtn').addEventListener('click', () => {
if (!img.src) return; // Don't do anything if no image is loaded
// Toggle grayscale filter
activeFilters.grayscale = activeFilters.grayscale === 100 ? 0 : 100;
applyFilters();
closeMenuIfMobile();
});
- Color Inversion Toggle.
Toggles color inversion filter (negative effect) on/off for the current image.
// Step 14: Invert all colors in the image (like a negative)
document.getElementById('invertBtn').addEventListener('click', () => {
if (!img.src) return; // Don't do anything if no image is loaded
// Toggle invert filter
activeFilters.invert = activeFilters.invert === 100 ? 0 : 100;
applyFilters();
closeMenuIfMobile();
});
- Sepia Tone Toggle
Toggles vintage sepia tone filter on/off for the current image.
// Step 15: Apply sepia filter
document.getElementById('sepiaBtn').addEventListener('click', () => {
if (!img.src) return; // Don't do anything if no image is loaded
// Toggle sepia filter
activeFilters.sepia = activeFilters.sepia === 100 ? 0 : 100;
applyFilters();
closeMenuIfMobile();
});
- Brightness Control
Adjusts image brightness in real-time as user moves the brightness slider (0-200%).
// Step 16: Set up the brightness slider
const brightnessSlider = document.getElementById('brightnessSlider');
brightnessSlider.addEventListener('input', () => {
// Update the percentage display
updateSliderValue('brightnessSlider', 'brightnessValue');
activeFilters.brightness = brightnessSlider.value;
applyFilters();
});
- Brightness Mobile
Closes mobile menu automatically after brightness adjustment is complete.
// Step 17: Close the menu after adjusting brightness on mobile devices
brightnessSlider.addEventListener('change', () => {
if (window.innerWidth <= 768) {
// Wait a short moment before closing (feels more natural)
setTimeout(closeMenuIfMobile, 300);
}
});
- Contrast Control
Adjusts image contrast in real-time as user moves the contrast slider (0-200%).
// Step 18: Set up the contrast slider
const contrastSlider = document.getElementById('contrastSlider');
contrastSlider.addEventListener('input', () => {
// Update the percentage display
updateSliderValue('contrastSlider', 'contrastValue');
activeFilters.contrast = contrastSlider.value;
applyFilters();
});
- Contrast Mobile
Closes mobile menu automatically after contrast adjustment is complete.
// Step 19: Close the menu after adjusting contrast on mobile devices
contrastSlider.addEventListener('change', () => {
if (window.innerWidth <= 768) {
// Wait a short moment before closing (feels more natural)
setTimeout(closeMenuIfMobile, 300);
}
});
- Crop Mode Activator
Enables crop selection mode and activates the Apply Crop button.
// Step 20: Enter crop mode when crop button is clicked
document.getElementById('cropBtn').addEventListener('click', () => {
if (!img.src) return; // Don't do anything if no image is loaded
cropActive = true; // Turn on crop mode
// Make the Apply Crop button clickable
document.getElementById('applyCropBtn').classList.remove('disabled');
closeMenuIfMobile();
});
- Coordinate Converter
Converts screen mouse/touch coordinates to actual canvas image coordinates for accurate crop selection.
// Step 21: Function to convert screen coordinates to actual image coordinates
function getScaledCoordinates(e) {
// Get the canvas position and size on screen
const rect = canvas.getBoundingClientRect();
// Calculate the scaling ratio between screen size and actual image size
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
// Convert the screen coordinates to actual image coordinates
return {
x: (e.clientX - rect.left) * scaleX,
y: (e.clientY - rect.top) * scaleY
};
}
- Crop Selection Start
Initiates crop rectangle selection when user clicks/touches the canvas in crop mode.
// Step 22: Handle start of crop selection (mouse down or touch start)
canvas.addEventListener('mousedown', handlePointerStart);
canvas.addEventListener('touchstart', (e) => {
if (!img.src) return; // Don't do anything if no image is loaded
e.preventDefault(); // Prevent default touch behavior
const touch = e.touches[0]; // Get the first touch point
// Pass the touch coordinates to our handler function
handlePointerStart({
clientX: touch.clientX,
clientY: touch.clientY
});
});
function handlePointerStart(e) {
if (!cropActive || !img.src) return; // Only proceed if in crop mode with an image
isDragging = true; // Start dragging mode
// Save the starting point for our crop rectangle
const coords = getScaledCoordinates(e);
cropRect.startX = coords.x;
cropRect.startY = coords.y;
}
- Crop Selection Update
Updates crop rectangle size and position as user drags mouse/finger across the canvas.
// Step 23: Update crop selection while dragging
canvas.addEventListener('mousemove', handlePointerMove);
canvas.addEventListener('touchmove', (e) => {
if (!cropActive || !isDragging || !img.src) return;
e.preventDefault(); // Prevent default touch behavior like scrolling
const touch = e.touches[0]; // Get the first touch point
// Pass the touch coordinates to our handler function
handlePointerMove({
clientX: touch.clientX,
clientY: touch.clientY
});
});
function handlePointerMove(e) {
if (!isDragging || !cropActive || !img.src) return;
// Get current pointer position
const coords = getScaledCoordinates(e);
// Calculate the width and height of our crop selection
cropRect.width = coords.x - cropRect.startX;
cropRect.height = coords.y - cropRect.startY;
// Redraw the image and draw our crop selection rectangle on top
ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear the canvas
ctx.drawImage(img, 0, 0, canvas.width, canvas.height); // Redraw the image
ctx.strokeStyle = '#03dac6'; // Teal color for the crop rectangle
ctx.lineWidth = 2; // 2 pixels thick line
// Draw the rectangle
ctx.strokeRect(cropRect.startX, cropRect.startY, cropRect.width, cropRect.height);
}
- Crop Selection End
Finalizes crop rectangle selection when user releases mouse button or lifts finger.
// Step 24: End crop selection when pointer is released or leaves canvas
canvas.addEventListener('mouseup', handlePointerEnd);
canvas.addEventListener('touchend', handlePointerEnd);
canvas.addEventListener('mouseleave', handlePointerEnd);
canvas.addEventListener('touchcancel', handlePointerEnd);
function handlePointerEnd() {
if (cropActive) isDragging = false; // Stop dragging mode
}
- Crop Processor
Extracts selected crop area, resizes canvas, and replaces current image with cropped version.
// Step 25: Process the crop operation when Apply Crop button is clicked
document.getElementById('applyCropBtn').addEventListener('click', () => {
if (!img.src) return; // Don't do anything if no image is loaded
// Only proceed if in crop mode and we have a valid selection area
if (cropActive && cropRect.width && cropRect.height) {
// Handle negative width/height if user dragged in reverse direction
const positiveWidth = Math.abs(cropRect.width);
const positiveHeight = Math.abs(cropRect.height);
// Find the correct starting position (top-left corner of selection)
const startX = cropRect.width > 0 ? cropRect.startX :
cropRect.startX + cropRect.width;
const startY = cropRect.height > 0 ? cropRect.startY :
cropRect.startY + cropRect.height;
// Extract just the selected portion of the image
const croppedImage = ctx.getImageData(startX, startY,
positiveWidth, positiveHeight);
// Resize the canvas to match the cropped size
canvas.width = positiveWidth;
canvas.height = positiveHeight;
// Place the cropped portion on the canvas
ctx.putImageData(croppedImage, 0, 0);
// Create a temporary canvas to help create a new image
const tempCanvas = document.createElement('canvas');
tempCanvas.width = positiveWidth;
tempCanvas.height = positiveHeight;
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
tempCtx.putImageData(croppedImage, 0, 0);
// Create a new image from the cropped canvas
const newImg = new Image();
newImg.onload = function() {
img = newImg; // Update our main image to be the cropped version
// Exit crop mode
cropActive = false;
document.getElementById('applyCropBtn').classList.add('disabled');
// Resize to fit screen
resizeCanvasToFitScreen();
closeMenuIfMobile();
};
newImg.src = tempCanvas.toDataURL(); // Convert canvas to image data
}
});
- Image Reset
Restores the original uploaded image and resets all filters and adjustments to default values.
// Step 26: Reset to original image when Reset button is clicked
document.getElementById('resetBtn').addEventListener('click', () => {
// Don't do anything if no image or backup
if (!img.src || !originalImage) return;
// Reset all filter values
activeFilters = {
grayscale: 0,
invert: 0,
sepia: 0,
brightness: 100,
contrast: 100
};
// Reset UI sliders
brightnessSlider.value = 100;
contrastSlider.value = 100;
// Update slider displays
updateSliderValue('brightnessSlider', 'brightnessValue');
updateSliderValue('contrastSlider', 'contrastValue');
// Restore canvas dimensions and original image
canvas.width = originalImage.width;
canvas.height = originalImage.height;
ctx.putImageData(originalImage, 0, 0);
// Reset filter
ctx.filter = 'none';
// Create a new image from the reset canvas
const newImg = new Image();
newImg.onload = function() {
img = newImg;
resizeCanvasToFitScreen();
};
newImg.src = canvas.toDataURL();
closeMenuIfMobile();
});
- Image Saver
Downloads the current edited image as a PNG file to user's device.
// Step 27: Save the edited image when Save button is clicked
document.getElementById('saveBtn').addEventListener('click', () => {
if (!img.src) return; // Don't do anything if no image is loaded
// Create a download link
const link = document.createElement('a');
link.href = canvas.toDataURL('image/png'); // Convert canvas to PNG format
link.download = 'edited-image.png'; // Default filename for download
link.click(); // Simulate a click to trigger the download
closeMenuIfMobile();
});
if you want the source code click on the download button below
disclaimer: you will get the source code with the database script and to make it work in your machine is your responsibility and to debug any error/exception is your responsibility this project is for the students who want to see an example and read the code not to get and run.
Download Projects Source Code












