about summary refs log tree commit diff stats
path: root/js/leibovitz/leibovitz.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/leibovitz/leibovitz.js')
-rw-r--r--js/leibovitz/leibovitz.js446
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