const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const video = document.createElement('video'); const toggleCameraButton = document.getElementById('toggle-camera'); const captureButton = document.getElementById('capture'); const editImageButton = document.getElementById('edit-image'); const imageInput = document.getElementById('image-input'); const focusControl = document.getElementById('focus-control'); const focusSlider = document.getElementById('focus-slider'); const focusValue = document.getElementById('focus-value'); const slideControls = document.querySelector('.slide-controls'); const SETTINGS_KEY = 'leibovitz-settings'; let cameraOn = false; let stream = null; let track = null; let isEditMode = false; let originalImage = null; // Store the original image // Initialize managers ColorManager.init(); DitherManager.init(); ContrastManager.init(); BlurManager.init(); BalanceManager.init(); // Function to update slider controls visibility function updateSliderControlsVisibility() { if (cameraOn || isEditMode) { slideControls.classList.add('visible'); } else { slideControls.classList.remove('visible'); } } // Set the canvas dimensions to match the window size function updateCanvasSize() { // Get the container dimensions const container = document.querySelector('.preview-container'); const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; // If video is playing, use its aspect ratio if (video.videoWidth && video.videoHeight) { const videoAspect = video.videoWidth / video.videoHeight; const containerAspect = containerWidth / containerHeight; // Determine dimensions that maintain aspect ratio while fitting in container if (containerAspect > videoAspect) { // Container is wider than video canvas.height = containerHeight; canvas.width = containerHeight * videoAspect; } else { // Container is taller than video canvas.width = containerWidth; canvas.height = containerWidth / videoAspect; } } else { // Default to container dimensions until video starts canvas.width = containerWidth; canvas.height = containerHeight; } } // Update canvas size when window is resized window.addEventListener('resize', () => { if (cameraOn || isEditMode) { updateCanvasSize(); if (isEditMode && originalImage) { applyEffects(); } } }); // Initialize canvas size updateCanvasSize(); // Function to completely clear the canvas function clearCanvas() { // Get container dimensions const container = document.querySelector('.preview-container'); const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; // Set canvas dimensions to match container canvas.width = containerWidth; canvas.height = containerHeight; // Clear the entire canvas context ctx.clearRect(0, 0, containerWidth, containerHeight); // Reset any transformations or other context properties ctx.setTransform(1, 0, 0, 1, 0, 0); // Hide the canvas canvas.style.display = 'none'; } function startCamera() { // If we're in edit mode, show confirmation dialog if (isEditMode) { if (!confirm('Switching to camera mode will discard your current image and edits. Continue?')) { return; // User cancelled } // Clear edit mode state isEditMode = false; originalImage = null; clearCanvas(); } navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: 'environment' } } }) .then(s => { stream = s; video.srcObject = stream; video.play(); canvas.style.display = 'block'; // Show the canvas captureButton.disabled = false; captureButton.active = true; editImageButton.classList.add('hidden'); isEditMode = false; originalImage = null; // Clear the original image updateSliderControlsVisibility(); // Show slider controls track = stream.getVideoTracks()[0]; const settings = track.getSettings(); // Check if focus control is supported with this browser and device combo if ('focusDistance' in settings) { focusControl.style.display = 'flex'; focusSlider.disabled = false; focusSlider.value = settings.focusDistance || focusSlider.min; focusValue.textContent = `${focusSlider.value}%`; focusSlider.addEventListener('input', () => { const value = focusSlider.value; focusValue.textContent = `${value}%`; track.applyConstraints({ advanced: [{ focusDistance: value }] }); }); } else { console.warn('Focus control is not supported on this device.'); focusControl.style.display = 'none'; } // Draw the video feed to the canvas video.addEventListener('play', function() { function step() { if (!cameraOn) return; drawVideoProportional(); applyContrast(); applyColorTint(); applyBlur(); applyBalance(); applyDither(); requestAnimationFrame(step); } requestAnimationFrame(step); }); }) .catch(err => { console.error('Error accessing camera: ', err); }); } function stopCamera() { if (stream) { stream.getTracks().forEach(track => track.stop()); video.pause(); clearCanvas(); captureButton.disabled = true; captureButton.active = false; focusSlider.disabled = true; focusControl.style.display = 'none'; stream = null; editImageButton.classList.remove('hidden'); updateSliderControlsVisibility(); // Hide slider controls if no image is loaded } } function loadImage(file) { const reader = new FileReader(); reader.onload = function(e) { const img = new Image(); img.onload = function() { // Clear any existing content first clearCanvas(); // Calculate dimensions to maintain aspect ratio const container = document.querySelector('.preview-container'); const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; const imgAspect = img.width / img.height; const containerAspect = containerWidth / containerHeight; let canvasWidth, canvasHeight; if (containerAspect > imgAspect) { canvasHeight = containerHeight; canvasWidth = containerHeight * imgAspect; } else { canvasWidth = containerWidth; canvasHeight = containerWidth / imgAspect; } // Set canvas dimensions canvas.width = canvasWidth; canvas.height = canvasHeight; // Store the original image originalImage = img; // Draw the new image ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight); canvas.style.display = 'block'; captureButton.disabled = false; captureButton.active = true; updateSliderControlsVisibility(); // Show slider controls // Start the effect loop function step() { if (!isEditMode) return; applyEffects(); requestAnimationFrame(step); } requestAnimationFrame(step); }; img.src = e.target.result; }; reader.readAsDataURL(file); } function applyEffects() { // Clear the canvas first ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw the original image ctx.drawImage(originalImage, 0, 0, canvas.width, canvas.height); // Apply effects in sequence applyContrast(); applyColorTint(); applyBlur(); applyBalance(); applyDither(); } function drawVideoProportional() { // Clear the canvas first ctx.clearRect(0, 0, canvas.width, canvas.height); const videoAspectRatio = video.videoWidth / video.videoHeight; const canvasAspectRatio = canvas.width / canvas.height; let drawWidth, drawHeight; if (canvasAspectRatio > videoAspectRatio) { drawHeight = canvas.height; drawWidth = videoAspectRatio * drawHeight; } else { drawWidth = canvas.width; drawHeight = drawWidth / videoAspectRatio; } const offsetX = (canvas.width - drawWidth) / 2; const offsetY = (canvas.height - drawHeight) / 2; // Draw the video content centered ctx.drawImage(video, offsetX, offsetY, drawWidth, drawHeight); } function applyColorTint() { const currentColor = ColorManager.getCurrentColor(); if (!currentColor) return; const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const tintedImageData = ColorManager.applyTint(imageData, currentColor); ctx.putImageData(tintedImageData, 0, 0); } function applyBalance() { const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const balancedImageData = BalanceManager.applyBalance(imageData); ctx.putImageData(balancedImageData, 0, 0); } function applyContrast() { const currentContrast = ContrastManager.getCurrentContrast(); if (!currentContrast || currentContrast === 1.0) return; const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const contrastedImageData = ContrastManager.applyContrast(imageData, currentContrast); ctx.putImageData(contrastedImageData, 0, 0); } function applyDither() { const currentMode = DitherManager.getCurrentMode(); if (!currentMode || currentMode === 'none') return; const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const ditheredImageData = DitherManager.applyDither(imageData, currentMode); ctx.putImageData(ditheredImageData, 0, 0); } function applyBlur() { const currentBlur = BlurManager.getCurrentBlur(); if (!currentBlur) return; const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const blurredImageData = BlurManager.applyBlur(imageData, currentBlur); ctx.putImageData(blurredImageData, 0, 0); } captureButton.addEventListener('click', () => { const currentColor = ColorManager.getCurrentColor(); const borderWidth = 4; // Create a canvas with extra space for the border const captureCanvas = document.createElement('canvas'); const captureCtx = captureCanvas.getContext('2d'); // Set dimensions including border captureCanvas.width = canvas.width + (borderWidth * 2); captureCanvas.height = canvas.height + (borderWidth * 2); // Fill with border color if a tint is selected if (currentColor) { captureCtx.fillStyle = currentColor; captureCtx.fillRect(0, 0, captureCanvas.width, captureCanvas.height); } // Draw the main canvas content captureCtx.drawImage(canvas, borderWidth, borderWidth); const link = document.createElement('a'); link.download = 'captured-image.png'; link.href = captureCanvas.toDataURL('image/png'); link.click(); }); toggleCameraButton.addEventListener('click', () => { cameraOn = !cameraOn; if (cameraOn) { startCamera(); toggleCameraButton.textContent = 'Camera Off'; } else { stopCamera(); toggleCameraButton.textContent = 'Camera On'; } }); editImageButton.addEventListener('click', () => { if (!cameraOn) { imageInput.click(); } }); imageInput.addEventListener('change', (e) => { if (e.target.files && e.target.files[0]) { isEditMode = true; loadImage(e.target.files[0]); } }); if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js') .then(registration => { console.log('ServiceWorker registration successful with scope: ', registration.scope); }, err => { console.log('ServiceWorker registration failed: ', err); }); }); } ColorManager._setupEventListeners(); // Update the reset handlers in each manager to trigger a redraw function resetEffects() { if (isEditMode && originalImage) { applyEffects(); } } // Add reset handlers to each manager BlurManager.reset = function() { this._currentBlur = 0; this._slider.value = 0; this._value.textContent = '0%'; this._notifyObservers(); resetEffects(); }; ContrastManager.reset = function() { this._currentContrast = 1.0; this._slider.value = 0; document.getElementById('contrast-value').textContent = '0'; this._notifyObservers(); resetEffects(); }; ColorManager.reset = function() { this._currentColor = null; this._colorInput.value = '#ffffff'; this._notifyObservers(); resetEffects(); }; BalanceManager.reset = function() { this.balanceSlider.value = 6500; this.balanceValue.textContent = '6500K'; resetEffects(); }; DitherManager.reset = function() { this._currentMode = 'none'; this._modeSelect.value = 'none'; this._notifyObservers(); resetEffects(); }; function saveSettings() { const settings = { blur: BlurManager._currentBlur, contrast: ContrastManager._currentContrast, color: ColorManager._currentColor, balance: BalanceManager._currentBalance, dither: DitherManager._currentMode, blockSize: DitherManager._currentBlockSize }; localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } function loadSettings() { const savedSettings = localStorage.getItem(SETTINGS_KEY); if (savedSettings) { const settings = JSON.parse(savedSettings); // Apply saved settings if (settings.blur !== undefined) { BlurManager._currentBlur = settings.blur; BlurManager._slider.value = settings.blur; BlurManager._value.textContent = `${settings.blur}%`; } if (settings.contrast !== undefined) { ContrastManager._currentContrast = settings.contrast; ContrastManager._slider.value = settings.contrast; document.getElementById('contrast-value').textContent = settings.contrast; } if (settings.color !== undefined) { ColorManager._currentColor = settings.color; ColorManager._colorInput.value = settings.color; } if (settings.balance !== undefined) { BalanceManager._currentBalance = settings.balance; BalanceManager.balanceSlider.value = settings.balance; BalanceManager.balanceValue.textContent = `${settings.balance}K`; } if (settings.dither !== undefined) { DitherManager._currentMode = settings.dither; DitherManager._modeSelect.value = settings.dither; } if (settings.blockSize !== undefined) { DitherManager._currentBlockSize = settings.blockSize; DitherManager._blockSizeSlider.value = settings.blockSize; document.getElementById('block-size-value').textContent = `${settings.blockSize}px`; } } } // Add event listeners for settings changes function setupSettingsListeners() { // Blur BlurManager._slider.addEventListener('change', saveSettings); // Contrast ContrastManager._slider.addEventListener('change', saveSettings); // Color ColorManager._colorInput.addEventListener('change', saveSettings); // Balance BalanceManager.balanceSlider.addEventListener('change', saveSettings); // Dither DitherManager._modeSelect.addEventListener('change', saveSettings); // Block Size if (DitherManager._blockSizeSlider) { DitherManager._blockSizeSlider.addEventListener('change', saveSettings); } } // Call this after all managers are initialized setupSettingsListeners(); loadSettings();