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

});





















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.






Share this

Related Posts

Latest
Previous
Next Post »