diff options
Diffstat (limited to 'js/leibovitz/leibovitz.js')
-rw-r--r-- | js/leibovitz/leibovitz.js | 446 |
1 files changed, 446 insertions, 0 deletions
diff --git a/js/leibovitz/leibovitz.js b/js/leibovitz/leibovitz.js new file mode 100644 index 0000000..5cd6f2d --- /dev/null +++ b/js/leibovitz/leibovitz.js @@ -0,0 +1,446 @@ +/** + * Start here. + * + * Susan Sontag: + * > The camera makes everyone a tourist in other people's reality, + * > and eventually in one's own. + * + * Uses multiple design patterns for state management and applying effects: + * - Observer Pattern: state management and effect application across modules + * - State Pattern: mode management (camera/edit) + * - Factory Pattern: UI initialization and media device creation + * - Strategy Pattern: algorithm selection when applying an effect + * - Command Pattern: canvas operations and state reset + * - Chain of Responsibility: sequential effect application + * + * + * Separate manager modules for each effect: + * - ColorManager: HSL-based color manipulation + * - DitherManager: multiple dithering algorithms + * - ContrastManager: linear contrast adjustment + * - BlurManager: optimized box blur + * - BalanceManager: temperature-based color adjustment + * + * + */ + +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'); + +let cameraOn = false; +let stream = null; +let track = null; +let isEditMode = false; +let originalImage = null; // Store the original image for edit mode + +// Initialize managers +ColorManager.init(); +DitherManager.init(); +ContrastManager.init(); +BlurManager.init(); +BalanceManager.init(); + +/** + * Updates visibility of controls based on camera/edit mode state + */ +function updateSliderControlsVisibility() { + const settingsContainer = document.getElementById('settings-container'); + if (cameraOn || isEditMode) { + slideControls.classList.add('visible'); + settingsContainer.classList.add('visible'); + } else { + slideControls.classList.remove('visible'); + settingsContainer.classList.remove('visible'); + } +} + +/** + * Updates canvas dimensions while maintaining aspect ratio + */ +function updateCanvasSize() { + const container = document.querySelector('.preview-container'); + const containerWidth = container.clientWidth; + const containerHeight = container.clientHeight; + + if (video.videoWidth && video.videoHeight) { + const videoAspect = video.videoWidth / video.videoHeight; + const containerAspect = containerWidth / containerHeight; + + if (containerAspect > videoAspect) { + canvas.height = containerHeight; + canvas.width = containerHeight * videoAspect; + } else { + canvas.width = containerWidth; + canvas.height = containerWidth / videoAspect; + } + } else { + canvas.width = containerWidth; + canvas.height = containerHeight; + } +} + +window.addEventListener('resize', () => { + if (cameraOn || isEditMode) { + updateCanvasSize(); + if (isEditMode && originalImage) { + applyEffects(); + } + } +}); + +updateCanvasSize(); + +function clearCanvas() { + const container = document.querySelector('.preview-container'); + const containerWidth = container.clientWidth; + const containerHeight = container.clientHeight; + + canvas.width = containerWidth; + canvas.height = containerHeight; + ctx.clearRect(0, 0, containerWidth, containerHeight); + ctx.setTransform(1, 0, 0, 1, 0, 0); + canvas.style.display = 'none'; +} + +/** + * Initializes camera access and sets up video stream + * Implements the Factory pattern for media device creation + * Uses the State pattern for mode management and UI state + */ +function startCamera() { + if (isEditMode) { + if (!confirm('Switching to camera mode will discard your current image and edits. Continue?')) { + return; + } + 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'; + captureButton.disabled = false; + captureButton.active = true; + editImageButton.classList.add('hidden'); + toggleCameraButton.classList.add('hidden'); + isEditMode = false; + originalImage = null; + updateSliderControlsVisibility(); + + track = stream.getVideoTracks()[0]; + const settings = track.getSettings(); + + // Feature detection for focus control + // Relatively untested because I don't have a device with focus control + 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'; + } + + // Animation loop using requestAnimationFrame + 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); + toggleCameraButton.classList.remove('hidden'); + }); +} + +/** + * Stops camera stream and resets UI state + */ +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'); + toggleCameraButton.classList.remove('hidden'); + updateSliderControlsVisibility(); + } +} + +/** + * Loads and displays an image file + * Uses aspect ratio preservation strategy for responsive display + */ +function loadImage(file) { + const reader = new FileReader(); + reader.onload = function(e) { + const img = new Image(); + img.onload = function() { + clearCanvas(); + + 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; + } + + canvas.width = canvasWidth; + canvas.height = canvasHeight; + originalImage = img; + + ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight); + canvas.style.display = 'block'; + captureButton.disabled = false; + captureButton.active = true; + updateSliderControlsVisibility(); + + function step() { + if (!isEditMode) return; + applyEffects(); + requestAnimationFrame(step); + } + requestAnimationFrame(step); + }; + img.src = e.target.result; + }; + reader.readAsDataURL(file); +} + +/** + * Sequentially applies all effects to the original image + */ +function applyEffects() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(originalImage, 0, 0, canvas.width, canvas.height); + applyContrast(); + applyColorTint(); + applyBlur(); + applyBalance(); + applyDither(); +} + +/** + * Draws video feed maintaining aspect ratio + */ +function drawVideoProportional() { + 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; + + 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); +} + +/** + * Captures the current canvas state with effects + */ +captureButton.addEventListener('click', () => { + const currentColor = ColorManager.getCurrentColor(); + const borderWidth = 4; + + const captureCanvas = document.createElement('canvas'); + const captureCtx = captureCanvas.getContext('2d'); + + captureCanvas.width = canvas.width + (borderWidth * 2); + captureCanvas.height = canvas.height + (borderWidth * 2); + + if (currentColor) { + captureCtx.fillStyle = currentColor; + captureCtx.fillRect(0, 0, captureCanvas.width, captureCanvas.height); + } + + 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]); + } +}); + +/** + * Service Worker registration for offline functionality + */ +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(); + +function resetEffects() { + if (isEditMode && originalImage) { + applyEffects(); + } +} + +/** + * Reset handlers for each effect 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(); +}; \ No newline at end of file |