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











