JavaScript Photo Editor Project Source Code

How To Create a Photo Editor Application in JavaScript, HTML and CSS

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.




if you want the source code click on the download button below




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();

});