about summary refs log tree commit diff stats
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/leibovitz/ChicagoFLF.ttfbin0 -> 31256 bytes
-rw-r--r--js/leibovitz/android-icon-144x144.pngbin0 -> 3068 bytes
-rw-r--r--js/leibovitz/android-icon-192x192.pngbin0 -> 3253 bytes
-rw-r--r--js/leibovitz/android-icon-36x36.pngbin0 -> 1791 bytes
-rw-r--r--js/leibovitz/android-icon-48x48.pngbin0 -> 1953 bytes
-rw-r--r--js/leibovitz/android-icon-72x72.pngbin0 -> 2429 bytes
-rw-r--r--js/leibovitz/android-icon-96x96.pngbin0 -> 1993 bytes
-rw-r--r--js/leibovitz/apple-icon-114x114.pngbin0 -> 3027 bytes
-rw-r--r--js/leibovitz/apple-icon-120x120.pngbin0 -> 2916 bytes
-rw-r--r--js/leibovitz/apple-icon-144x144.pngbin0 -> 3068 bytes
-rw-r--r--js/leibovitz/apple-icon-152x152.pngbin0 -> 3882 bytes
-rw-r--r--js/leibovitz/apple-icon-180x180.pngbin0 -> 4656 bytes
-rw-r--r--js/leibovitz/apple-icon-57x57.pngbin0 -> 2258 bytes
-rw-r--r--js/leibovitz/apple-icon-60x60.pngbin0 -> 2291 bytes
-rw-r--r--js/leibovitz/apple-icon-72x72.pngbin0 -> 2429 bytes
-rw-r--r--js/leibovitz/apple-icon-76x76.pngbin0 -> 2572 bytes
-rw-r--r--js/leibovitz/apple-icon-precomposed.pngbin0 -> 3689 bytes
-rw-r--r--js/leibovitz/apple-icon.pngbin0 -> 3689 bytes
-rw-r--r--js/leibovitz/balance.js103
-rw-r--r--js/leibovitz/blur.js167
-rw-r--r--js/leibovitz/browserconfig.xml2
-rw-r--r--js/leibovitz/color.js245
-rw-r--r--js/leibovitz/contrast.js100
-rw-r--r--js/leibovitz/dither.js613
-rw-r--r--js/leibovitz/favicon-16x16.pngbin0 -> 1228 bytes
-rw-r--r--js/leibovitz/favicon-32x32.pngbin0 -> 1813 bytes
-rw-r--r--js/leibovitz/favicon-96x96.pngbin0 -> 1993 bytes
-rw-r--r--js/leibovitz/favicon.icobin0 -> 1150 bytes
-rw-r--r--js/leibovitz/index.html548
-rw-r--r--js/leibovitz/leibovitz.js446
-rw-r--r--js/leibovitz/manifest.json71
-rw-r--r--js/leibovitz/ms-icon-144x144.pngbin0 -> 3068 bytes
-rw-r--r--js/leibovitz/ms-icon-150x150.pngbin0 -> 3884 bytes
-rw-r--r--js/leibovitz/ms-icon-310x310.pngbin0 -> 9141 bytes
-rw-r--r--js/leibovitz/ms-icon-70x70.pngbin0 -> 2518 bytes
-rw-r--r--js/leibovitz/service-worker.js89
-rw-r--r--js/pipe.js4
-rw-r--r--js/sentiment/sentiment/index.html40
38 files changed, 2395 insertions, 33 deletions
diff --git a/js/leibovitz/ChicagoFLF.ttf b/js/leibovitz/ChicagoFLF.ttf
new file mode 100644
index 0000000..60691e1
--- /dev/null
+++ b/js/leibovitz/ChicagoFLF.ttf
Binary files differdiff --git a/js/leibovitz/android-icon-144x144.png b/js/leibovitz/android-icon-144x144.png
new file mode 100644
index 0000000..ae37a7e
--- /dev/null
+++ b/js/leibovitz/android-icon-144x144.png
Binary files differdiff --git a/js/leibovitz/android-icon-192x192.png b/js/leibovitz/android-icon-192x192.png
new file mode 100644
index 0000000..4fd03e4
--- /dev/null
+++ b/js/leibovitz/android-icon-192x192.png
Binary files differdiff --git a/js/leibovitz/android-icon-36x36.png b/js/leibovitz/android-icon-36x36.png
new file mode 100644
index 0000000..d5fbc51
--- /dev/null
+++ b/js/leibovitz/android-icon-36x36.png
Binary files differdiff --git a/js/leibovitz/android-icon-48x48.png b/js/leibovitz/android-icon-48x48.png
new file mode 100644
index 0000000..ae93a97
--- /dev/null
+++ b/js/leibovitz/android-icon-48x48.png
Binary files differdiff --git a/js/leibovitz/android-icon-72x72.png b/js/leibovitz/android-icon-72x72.png
new file mode 100644
index 0000000..89d3e98
--- /dev/null
+++ b/js/leibovitz/android-icon-72x72.png
Binary files differdiff --git a/js/leibovitz/android-icon-96x96.png b/js/leibovitz/android-icon-96x96.png
new file mode 100644
index 0000000..a4fcd87
--- /dev/null
+++ b/js/leibovitz/android-icon-96x96.png
Binary files differdiff --git a/js/leibovitz/apple-icon-114x114.png b/js/leibovitz/apple-icon-114x114.png
new file mode 100644
index 0000000..2a2af04
--- /dev/null
+++ b/js/leibovitz/apple-icon-114x114.png
Binary files differdiff --git a/js/leibovitz/apple-icon-120x120.png b/js/leibovitz/apple-icon-120x120.png
new file mode 100644
index 0000000..dd9823f
--- /dev/null
+++ b/js/leibovitz/apple-icon-120x120.png
Binary files differdiff --git a/js/leibovitz/apple-icon-144x144.png b/js/leibovitz/apple-icon-144x144.png
new file mode 100644
index 0000000..ae37a7e
--- /dev/null
+++ b/js/leibovitz/apple-icon-144x144.png
Binary files differdiff --git a/js/leibovitz/apple-icon-152x152.png b/js/leibovitz/apple-icon-152x152.png
new file mode 100644
index 0000000..c43bf96
--- /dev/null
+++ b/js/leibovitz/apple-icon-152x152.png
Binary files differdiff --git a/js/leibovitz/apple-icon-180x180.png b/js/leibovitz/apple-icon-180x180.png
new file mode 100644
index 0000000..f7435e7
--- /dev/null
+++ b/js/leibovitz/apple-icon-180x180.png
Binary files differdiff --git a/js/leibovitz/apple-icon-57x57.png b/js/leibovitz/apple-icon-57x57.png
new file mode 100644
index 0000000..7f5dfa5
--- /dev/null
+++ b/js/leibovitz/apple-icon-57x57.png
Binary files differdiff --git a/js/leibovitz/apple-icon-60x60.png b/js/leibovitz/apple-icon-60x60.png
new file mode 100644
index 0000000..3a6a826
--- /dev/null
+++ b/js/leibovitz/apple-icon-60x60.png
Binary files differdiff --git a/js/leibovitz/apple-icon-72x72.png b/js/leibovitz/apple-icon-72x72.png
new file mode 100644
index 0000000..89d3e98
--- /dev/null
+++ b/js/leibovitz/apple-icon-72x72.png
Binary files differdiff --git a/js/leibovitz/apple-icon-76x76.png b/js/leibovitz/apple-icon-76x76.png
new file mode 100644
index 0000000..9dc77b1
--- /dev/null
+++ b/js/leibovitz/apple-icon-76x76.png
Binary files differdiff --git a/js/leibovitz/apple-icon-precomposed.png b/js/leibovitz/apple-icon-precomposed.png
new file mode 100644
index 0000000..8e17e9c
--- /dev/null
+++ b/js/leibovitz/apple-icon-precomposed.png
Binary files differdiff --git a/js/leibovitz/apple-icon.png b/js/leibovitz/apple-icon.png
new file mode 100644
index 0000000..8e17e9c
--- /dev/null
+++ b/js/leibovitz/apple-icon.png
Binary files differdiff --git a/js/leibovitz/balance.js b/js/leibovitz/balance.js
new file mode 100644
index 0000000..aeff62e
--- /dev/null
+++ b/js/leibovitz/balance.js
@@ -0,0 +1,103 @@
+/**
+ * White balance management module implementing temperature-based color adjustment.
+ * 
+ * Implements white balance adjustment using temperature-based RGB channel scaling.
+ * Provides non-linear temperature adjustment for natural color correction.
+ * 
+ * Implements the following design patterns:
+ * - Observer Pattern: state management and effect application
+ * - Factory Pattern: UI initialization
+ * - Strategy Pattern: temperature adjustment algorithm
+ * - Command Pattern: state reset operations
+ * 
+ * White balance adjustment process:
+ * 1. Convert temperature to ratio relative to neutral (6500K)
+ * 2. Apply non-linear scaling (0.2 factor) to red and blue channels
+ * 3. Warmer temps (<6500K) increase red, decrease blue
+ * 4. Cooler temps (>6500K) increase blue, decrease red
+ * 
+ * Features:
+ * - Temperature-based color adjustment
+ * - Non-linear response curve
+ * - Preserves green channel
+ * - Real-time updates
+ */
+
+const BalanceManager = {
+    // Private state
+    _observers: new Set(),
+    _slider: null,
+    _value: null,
+
+    /**
+     * Initializes the balance manager and sets up UI controls
+     */
+    init() {
+        this._slider = document.getElementById('balance-slider');
+        this._value = document.getElementById('balance-value');
+        this._setupEventListeners();
+    },
+
+    _setupEventListeners() {
+        this._slider.addEventListener('input', () => {
+            const value = this._slider.value;
+            this._value.textContent = `${value}K`;
+            this._notifyObservers();
+        });
+    },
+
+    _notifyObservers() {
+        this._observers.forEach(observer => observer(this.getCurrentBalance()));
+    },
+
+    /**
+     * Subscribes to balance state changes
+     * @param {Function} observer - Callback function for state changes
+     * @returns {Function} Unsubscribe function
+     */
+    subscribe(observer) {
+        this._observers.add(observer);
+        return () => this._observers.delete(observer);
+    },
+
+    /**
+     * Gets the current white balance temperature
+     * @returns {number} Current temperature in Kelvin (2000K-10000K)
+     */
+    getCurrentBalance() {
+        return parseInt(this._slider.value);
+    },
+
+    /**
+     * Applies white balance adjustment to an image
+     * And implements temperature-based RGB channel scaling with non-linear response
+     * @param {ImageData} imageData - Source image data
+     * @returns {ImageData} White balanced image data
+     */
+    applyBalance(imageData) {
+        const balance = this.getCurrentBalance();
+        if (!balance || balance === 6500) return imageData; // 6500K is neutral
+
+        const data = imageData.data;
+        const temperature = balance / 6500; // Convert to temperature ratio
+
+        for (let i = 0; i < data.length; i += 4) {
+            // Adjust red and blue channels based on temperature
+            // Warmer (lower K) increases red, decreases blue
+            // Cooler (higher K) increases blue, decreases red
+            data[i] = Math.min(255, data[i] * (1 + (temperature - 1) * 0.2));     // Red
+            data[i + 2] = Math.min(255, data[i + 2] * (1 + (1 - temperature) * 0.2)); // Blue
+        }
+
+        return imageData;
+    },
+
+    /**
+     * Resets balance effect to default state
+     */
+    reset() {
+        this._slider.value = 6500;
+        this._value.textContent = '6500K';
+        this._notifyObservers();
+    }
+}; 
\ No newline at end of file
diff --git a/js/leibovitz/blur.js b/js/leibovitz/blur.js
new file mode 100644
index 0000000..bc6cddf
--- /dev/null
+++ b/js/leibovitz/blur.js
@@ -0,0 +1,167 @@
+/**
+ * Blur management module implementing optimized box blur algorithm.
+ * 
+ * Implements a two-pass box blur algorithm with boundary optimization.
+ * Uses block-based processing for improved performance on large images.
+ * 
+ * Implements the following design patterns:
+ * - Observer Pattern: state management and effect application
+ * - Factory Pattern: UI initialization
+ * - Strategy Pattern: blur algorithm implementation
+ * - Command Pattern: state reset operations
+ * 
+ * The blur implementation uses a two-pass approach:
+ * 1. Horizontal pass: Applies blur along rows
+ * 2. Vertical pass: Applies blur along columns
+ * 
+ * Features:
+ * - Boundary optimization for performance
+ * - Block-based processing
+ * - Two-pass implementation for better performance
+ * - Edge clamping to prevent artifacts
+ */
+
+const BlurManager = {
+    // Private state
+    _currentBlur: 0, // Default blur (no blur)
+    _observers: new Set(),
+    _slider: null,
+    _value: null,
+
+    init() {
+        this._slider = document.getElementById('blur-slider');
+        this._value = document.getElementById('blur-value');
+        this._setupEventListeners();
+    },
+
+    _setupEventListeners() {
+        this._slider.addEventListener('input', () => {
+            const value = this._slider.value;
+            this._value.textContent = `${value}%`;
+            this._currentBlur = parseInt(value);
+            this._notifyObservers();
+        });
+    },
+
+    _notifyObservers() {
+        this._observers.forEach(observer => observer(this._currentBlur));
+    },
+
+    /**
+     * Subscribes to blur state changes
+     * @param {Function} observer - Callback function for state changes
+     * @returns {Function} Unsubscribe function
+     */
+    subscribe(observer) {
+        this._observers.add(observer);
+        return () => this._observers.delete(observer);
+    },
+
+    getCurrentBlur() {
+        return this._currentBlur;
+    },
+
+    /**
+     * Applies optimized box blur to an image
+     * And implements two-pass blur with content-aware boundary detection
+     * Uses separate horizontal and vertical passes, which is more performant
+     * @param {ImageData} imageData - Source image data
+     * @param {number} radius - Blur radius
+     * @returns {ImageData} Blurred image data
+     */
+    applyBlur(imageData, radius) {
+        if (!radius) return imageData;
+
+        const { data, width, height } = imageData;
+        const tempData = new Uint8ClampedArray(data);
+        
+        // Calculate the actual image boundaries
+        let minX = width, minY = height, maxX = 0, maxY = 0;
+        let hasContent = false;
+
+        // Find the actual image boundaries
+        for (let y = 0; y < height; y++) {
+            for (let x = 0; x < width; x++) {
+                const i = (y * width + x) * 4;
+                if (data[i + 3] > 0) { // Check alpha channel
+                    hasContent = true;
+                    minX = Math.min(minX, x);
+                    minY = Math.min(minY, y);
+                    maxX = Math.max(maxX, x);
+                    maxY = Math.max(maxY, y);
+                }
+            }
+        }
+
+        if (!hasContent) return imageData;
+
+        // Add padding to boundaries to prevent edge artifacts
+        minX = Math.max(0, minX - radius);
+        minY = Math.max(0, minY - radius);
+        maxX = Math.min(width - 1, maxX + radius);
+        maxY = Math.min(height - 1, maxY + radius);
+
+        // First pass: horizontal blur
+        for (let y = minY; y <= maxY; y++) {
+            for (let x = minX; x <= maxX; x++) {
+                let r = 0, g = 0, b = 0, a = 0;
+                let count = 0;
+
+                for (let dx = -radius; dx <= radius; dx++) {
+                    const nx = x + dx;
+                    if (nx >= 0 && nx < width) {
+                        const i = (y * width + nx) * 4;
+                        r += data[i];
+                        g += data[i + 1];
+                        b += data[i + 2];
+                        a += data[i + 3];
+                        count++;
+                    }
+                }
+
+                // Store horizontal blur result
+                const i = (y * width + x) * 4;
+                tempData[i] = r / count;
+                tempData[i + 1] = g / count;
+                tempData[i + 2] = b / count;
+                tempData[i + 3] = a / count;
+            }
+        }
+
+        // Second pass: vertical blur
+        for (let y = minY; y <= maxY; y++) {
+            for (let x = minX; x <= maxX; x++) {
+                let r = 0, g = 0, b = 0, a = 0;
+                let count = 0;
+
+                for (let dy = -radius; dy <= radius; dy++) {
+                    const ny = y + dy;
+                    if (ny >= 0 && ny < height) {
+                        const i = (ny * width + x) * 4;
+                        r += tempData[i];
+                        g += tempData[i + 1];
+                        b += tempData[i + 2];
+                        a += tempData[i + 3];
+                        count++;
+                    }
+                }
+
+                // Store final blur result
+                const i = (y * width + x) * 4;
+                data[i] = r / count;
+                data[i + 1] = g / count;
+                data[i + 2] = b / count;
+                data[i + 3] = a / count;
+            }
+        }
+
+        return imageData;
+    },
+
+    reset() {
+        this._currentBlur = 0;
+        this._slider.value = 0;
+        this._value.textContent = '0%';
+        this._notifyObservers();
+    }
+}; 
\ No newline at end of file
diff --git a/js/leibovitz/browserconfig.xml b/js/leibovitz/browserconfig.xml
new file mode 100644
index 0000000..c554148
--- /dev/null
+++ b/js/leibovitz/browserconfig.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
\ No newline at end of file
diff --git a/js/leibovitz/color.js b/js/leibovitz/color.js
new file mode 100644
index 0000000..78f4ebc
--- /dev/null
+++ b/js/leibovitz/color.js
@@ -0,0 +1,245 @@
+/**
+ * Color tint management module implementing HSL-based color manipulation.
+ * 
+ * Implements color tinting using HSL color space transformation with circular interpolation.
+ * Features noise reduction and smooth blending for high-quality results.
+ * 
+ * Implements the following design patterns:
+ * - Observer Pattern: state management and effect application
+ * - Factory Pattern: UI initialization
+ * - Strategy Pattern: color manipulation algorithms
+ * - Command Pattern: state reset operations
+ * 
+ * Color manipulation process:
+ * 1. Convert RGB to HSL color space
+ * 2. Apply circular interpolation for hue blending
+ * 3. Smooth blending for saturation and lightness
+ * 4. Noise reduction through value rounding
+ * 5. Convert back to RGB color space
+ * 
+ * Features:
+ * - Circular interpolation for natural hue transitions
+ * - Noise reduction through value rounding
+ * - Smooth blending with quadratic easing
+ * - HSL color space for better color manipulation
+ */
+
+const ColorManager = {
+    // Private state
+    _currentColor: null,
+    _observers: new Set(),
+    _colorInput: null,
+
+    init() {
+        this._setupEventListeners();
+    },
+
+    _setupEventListeners() {
+        this._colorInput = document.getElementById('color-tint');
+        this._colorInput.addEventListener('input', (e) => {
+            this._currentColor = e.target.value;
+            this._notifyObservers();
+        });
+
+        const resetButton = document.getElementById('reset-color');
+        resetButton.addEventListener('click', () => {
+            // Reset color tint
+            this._currentColor = null;
+            this._colorInput.value = '#ffffff';
+            this._notifyObservers();
+
+            // Reset contrast
+            ContrastManager.reset();
+
+            // Reset blur
+            BlurManager.reset();
+
+            // Reset white balance to default (6500K)
+            const balanceSlider = document.getElementById('balance-slider');
+            const balanceValue = document.getElementById('balance-value');
+            if (balanceSlider && balanceValue) {
+                balanceSlider.value = 6500;
+                balanceValue.textContent = '6500K';
+            }
+        });
+    },
+
+    _notifyObservers() {
+        this._observers.forEach(observer => observer(this._currentColor));
+    },
+
+    /**
+     * Subscribes to color state changes
+     * @param {Function} observer - Callback function for state changes
+     * @returns {Function} Unsubscribe function
+     */
+    subscribe(observer) {
+        this._observers.add(observer);
+        return () => this._observers.delete(observer);
+    },
+
+    getCurrentColor() {
+        return this._currentColor;
+    },
+
+    /**
+     * Applies color tint to an image using HSL color space
+     * Uses noise reduction and smooth blending for quality
+     * @param {ImageData} imageData - Source image data
+     * @param {string} color - Hex color value
+     * @returns {ImageData} Tinted image data
+     */
+    applyTint(imageData, color) {
+        if (!color) return imageData;
+
+        const { data } = imageData;
+        const [tintR, tintG, tintB] = this._hexToRgb(color);
+        
+        // Convert tint color to HSL for better color manipulation
+        const [tintH, tintS, tintL] = this._rgbToHsl(tintR, tintG, tintB);
+        
+        // Apply tint to each pixel with reduced noise
+        for (let i = 0; i < data.length; i += 4) {
+            const r = data[i];
+            const g = data[i + 1];
+            const b = data[i + 2];
+            
+            // Convert pixel to HSL
+            const [h, s, l] = this._rgbToHsl(r, g, b);
+            
+            // Blend the tint color with the original color
+            // This tries to create a more natural LUT effect
+            const blendFactor = 0.15; // Reduced from 0.3 to 0.15 for smoother effect
+            
+            // Smooth blending for hue (circular interpolation)
+            const newH = this._blendHue(h, tintH, blendFactor);
+            
+            // Smooth blending for saturation and lightness with noise reduction
+            const newS = this._smoothBlend(s, tintS, blendFactor);
+            const newL = this._smoothBlend(l, tintL, blendFactor);
+            
+            // Convert back to RGB with noise reduction
+            const [newR, newG, newB] = this._hslToRgb(newH, newS, newL);
+            
+            // Apply noise reduction by rounding to nearest multiple of 4
+            data[i] = Math.round(newR / 4) * 4;
+            data[i + 1] = Math.round(newG / 4) * 4;
+            data[i + 2] = Math.round(newB / 4) * 4;
+        }
+
+        return imageData;
+    },
+
+    /**
+     * Converts hex color to RGB values
+     * @param {string} hex - Hex color string
+     * @returns {Array} RGB values [r, g, b]
+     */
+    _hexToRgb(hex) {
+        const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+        return result ? [
+            parseInt(result[1], 16),
+            parseInt(result[2], 16),
+            parseInt(result[3], 16)
+        ] : null;
+    },
+
+    /**
+     * Converts RGB to HSL color space
+     * @param {number} r - Red component
+     * @param {number} g - Green component
+     * @param {number} b - Blue component
+     * @returns {Array} HSL values [h, s, l]
+     */
+    _rgbToHsl(r, g, b) {
+        r /= 255;
+        g /= 255;
+        b /= 255;
+        
+        const max = Math.max(r, g, b);
+        const min = Math.min(r, g, b);
+        let h, s, l = (max + min) / 2;
+        
+        if (max === min) {
+            h = s = 0;
+        } else {
+            const d = max - min;
+            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+            
+            switch (max) {
+                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+                case g: h = (b - r) / d + 2; break;
+                case b: h = (r - g) / d + 4; break;
+            }
+            h /= 6;
+        }
+        
+        return [h, s, l];
+    },
+
+    /**
+     * Converts HSL to RGB color space
+     * @param {number} h - Hue component
+     * @param {number} s - Saturation component
+     * @param {number} l - Lightness component
+     * @returns {Array} RGB values [r, g, b]
+     */
+    _hslToRgb(h, s, l) {
+        let r, g, b;
+        
+        if (s === 0) {
+            r = g = b = l;
+        } else {
+            const hue2rgb = (p, q, t) => {
+                if (t < 0) t += 1;
+                if (t > 1) t -= 1;
+                if (t < 1/6) return p + (q - p) * 6 * t;
+                if (t < 1/2) return q;
+                if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
+                return p;
+            };
+            
+            const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+            const p = 2 * l - q;
+            
+            r = hue2rgb(p, q, h + 1/3);
+            g = hue2rgb(p, q, h);
+            b = hue2rgb(p, q, h - 1/3);
+        }
+        
+        return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
+    },
+
+    /**
+     * Blends two hue values with circular interpolation
+     * @param {number} h1 - First hue value
+     * @param {number} h2 - Second hue value
+     * @param {number} factor - Blend factor
+     * @returns {number} Blended hue value
+     */
+    _blendHue(h1, h2, factor) {
+        const diff = h2 - h1;
+        if (Math.abs(diff) > 0.5) {
+            if (h1 > h2) {
+                return h1 + (h2 + 1 - h1) * factor;
+            } else {
+                return h1 + (h2 - (h1 + 1)) * factor;
+            }
+        }
+        return h1 + diff * factor;
+    },
+
+    /**
+     * Smooth blending with noise reduction
+     * Uses cubic easing function for smooth transitions
+     * @param {number} v1 - First value
+     * @param {number} v2 - Second value
+     * @param {number} factor - Blend factor
+     * @returns {number} Blended value
+     */
+    _smoothBlend(v1, v2, factor) {
+        // Apply a smooth easing function
+        const t = factor * factor * (3 - 2 * factor);
+        return v1 + (v2 - v1) * t;
+    }
+}; 
\ No newline at end of file
diff --git a/js/leibovitz/contrast.js b/js/leibovitz/contrast.js
new file mode 100644
index 0000000..c2b1a28
--- /dev/null
+++ b/js/leibovitz/contrast.js
@@ -0,0 +1,100 @@
+/**
+ * Contrast management module implementing linear contrast adjustment.
+ * 
+ * Implements contrast adjustment using a linear scaling algorithm.
+ * Provides real-time contrast control with immediate visual feedback.
+ * 
+ * Implements the following design patterns:
+ * - Observer Pattern: state management and effect application
+ * - Factory Pattern: UI initialization
+ * - Strategy Pattern: contrast adjustment algorithm
+ * - Command Pattern: state reset operations
+ * 
+ * Contrast adjustment process:
+ * 1. Calculate contrast factor using formula: (259 * (contrast + 255)) / (255 * (259 - contrast))
+ * 2. Apply linear scaling to each color channel
+ * 3. Maintain color balance while adjusting contrast
+ * 
+ * Features:
+ * - Linear contrast adjustment
+ * - Per-channel processing
+ * - Real-time updates
+ * - Preserves color relationships
+ */
+
+const ContrastManager = {
+    // Private state
+    _currentContrast: 1.0, // Default contrast (no change)
+    _observers: new Set(),
+    _slider: null,
+
+    /**
+     * Initializes the contrast manager and sets up UI controls
+     */
+    init() {
+        this._setupEventListeners();
+    },
+
+    /**
+     * Sets up event listeners for UI controls
+     */
+    _setupEventListeners() {
+        this._slider = document.getElementById('contrast-slider');
+        this._slider.addEventListener('input', (e) => {
+            this._currentContrast = parseFloat(e.target.value);
+            document.getElementById('contrast-value').textContent = this._currentContrast;
+            this._notifyObservers();
+        });
+    },
+
+    _notifyObservers() {
+        this._observers.forEach(observer => observer(this._currentContrast));
+    },
+
+    /**
+     * Subscribes to contrast state changes
+     * @param {Function} observer - Callback function for state changes
+     * @returns {Function} Unsubscribe function
+     */
+    subscribe(observer) {
+        this._observers.add(observer);
+        return () => this._observers.delete(observer);
+    },
+
+    getCurrentContrast() {
+        return this._currentContrast;
+    },
+
+    /**
+     * Applies contrast adjustment to an image
+     * Implements linear contrast adjustment algorithm
+     * @param {ImageData} imageData - Source image data
+     * @param {number} contrast - Contrast value
+     * @returns {ImageData} Contrasted image data
+     */
+    applyContrast(imageData, contrast) {
+        if (!contrast || contrast === 1.0) return imageData;
+
+        const { data } = imageData;
+        const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
+
+        for (let i = 0; i < data.length; i += 4) {
+            // Apply contrast to each color channel
+            for (let c = 0; c < 3; c++) {
+                const pixel = data[i + c];
+                data[i + c] = factor * (pixel - 128) + 128;
+            }
+        }
+
+        return imageData;
+    },
+
+    /**
+     * Resets contrast effect to default state
+     */
+    reset() {
+        this._currentContrast = 1.0;
+        this._slider.value = 0; // Reset slider to middle position
+        this._notifyObservers();
+    }
+}; 
\ No newline at end of file
diff --git a/js/leibovitz/dither.js b/js/leibovitz/dither.js
new file mode 100644
index 0000000..e74f1be
--- /dev/null
+++ b/js/leibovitz/dither.js
@@ -0,0 +1,613 @@
+/**
+ * Dithering management module implementing multiple dithering algorithms.
+ * 
+ * Implements a couple dithering algorithms with block-based processing.
+ * Block-based processing is faster, and has better performance.
+ * Supports multiple dithering patterns with configurable block sizes.
+ * 
+ * Implements the following design patterns:
+ * - Observer Pattern: state management and effect application
+ * - Factory Pattern: UI initialization
+ * - Strategy Pattern: dithering algorithm selection
+ * - Command Pattern: state reset operations
+ * 
+ * Supported dithering algorithms:
+ * - Floyd-Steinberg: Error diffusion with standard distribution pattern
+ * - Ordered: Matrix-based threshold dithering
+ * - Atkinson: Error diffusion with 1/8 error distribution
+ * - Bayer: Pattern-based threshold dithering
+ * 
+ * Each color channel (Red, Green, Blue) has 4 possible values:
+ * - 0   -> Black/None
+ * - 85  -> Low
+ * - 170 -> Medium
+ * - 255 -> Full
+ * 
+ * Features:
+ * - Block-based processing for performance
+ * - Multiple dithering algorithms
+ * - Configurable block sizes
+ * - Error diffusion patterns
+ */
+
+const DitherManager = {
+    // Private state
+    _currentMode: 'none',
+    _observers: new Set(),
+    _modeSelect: null,
+    _pixelSizeControl: null,
+    currentBlockSize: 4,
+
+    /**
+     * Initializes the dither manager and sets up UI controls
+     */
+    init() {
+        this._setupEventListeners();
+        this._pixelSizeControl = document.getElementById('pixel-size-control');
+    },
+
+    _setupEventListeners() {
+        this._modeSelect = document.getElementById('dither-select');
+        this._modeSelect.addEventListener('change', (e) => {
+            this._currentMode = e.target.value;
+            // Show/hide pixel size control based on dithering mode
+            this._pixelSizeControl.style.display = 
+                this._currentMode === 'none' ? 'none' : 'flex';
+            this._notifyObservers();
+        });
+
+        // Only add the event listener if the element actually exists
+        const blockSizeSlider = document.getElementById('block-size-slider');
+        if (blockSizeSlider) {
+            blockSizeSlider.addEventListener('input', (e) => {
+                this.currentBlockSize = parseInt(e.target.value);
+                document.getElementById('block-size-value').textContent = 
+                    `${this.currentBlockSize}px`;
+                // Notify observers instead of directly calling processFrame
+                this._notifyObservers();
+            });
+        }
+    },
+
+    _notifyObservers() {
+        this._observers.forEach(observer => observer(this._currentMode));
+    },
+
+    /**
+     * Subscribes to dithering state changes
+     * @param {Function} observer - Callback function for state changes
+     * @returns {Function} Unsubscribe function
+     */
+    subscribe(observer) {
+        this._observers.add(observer);
+        return () => this._observers.delete(observer);
+    },
+
+    getCurrentMode() {
+        return this._currentMode;
+    },
+
+    /**
+     * Applies selected dithering algorithm to image data
+     * @param {ImageData} imageData - Source image data
+     * @param {string} mode - Selected dithering algorithm
+     * @returns {ImageData} Processed image data
+     */
+    applyDither(imageData, mode) {
+        if (!mode || mode === 'none') return imageData;
+
+        const { data } = imageData;
+        const width = imageData.width;
+        const height = imageData.height;
+
+        switch (mode) {
+            case 'floyd-steinberg':
+                return this._floydSteinbergDither(data, width, height);
+            case 'ordered':
+                return this._orderedDither(data, width, height);
+            case 'atkinson':
+                return this._atkinsonDither(data, width, height);
+            case 'bayer':
+                return this._bayerDither(data, width, height);
+            default:
+                return imageData;
+        }
+    },
+
+    /**
+     * Quantizes a value to create chunkier output
+     * @param {number} value - Input value
+     * @param {number} levels - Number of quantization levels
+     * @returns {number} Quantized value
+     */
+    _quantize(value, levels = 4) {
+        const step = 255 / (levels - 1);
+        return Math.round(value / step) * step;
+    },
+
+    /**
+     * Applies Floyd-Steinberg dithering algorithm
+     * Uses a 4x4 error distribution pattern for smoother results
+     * @param {Uint8ClampedArray} data - Image data
+     * @param {number} width - Image width
+     * @param {number} height - Image height
+     * @returns {ImageData} Dithered image data
+     */
+    _floydSteinbergDither(data, width, height) {
+        const newData = new Uint8ClampedArray(data);
+        const threshold = 128;
+        const levels = 4;
+        const blockSize = this.currentBlockSize;
+
+        // Process in blocks, block by block
+        for (let y = 0; y < height; y += blockSize) {
+            for (let x = 0; x < width; x += blockSize) {
+                // Calculate block average
+                let blockSum = [0, 0, 0];
+                let pixelCount = 0;
+                
+                for (let by = 0; by < blockSize && y + by < height; by++) {
+                    for (let bx = 0; bx < blockSize && x + bx < width; bx++) {
+                        const idx = ((y + by) * width + (x + bx)) * 4;
+                        for (let c = 0; c < 3; c++) {
+                            blockSum[c] += newData[idx + c];
+                        }
+                        pixelCount++;
+                    }
+                }
+
+                // Calculate block average
+                const blockAvg = blockSum.map(sum => sum / pixelCount);
+                
+                // Apply dithering to the block average
+                for (let c = 0; c < 3; c++) {
+                    const oldPixel = blockAvg[c];
+                    const quantizedPixel = this._quantize(oldPixel, levels);
+                    const newPixel = quantizedPixel > threshold ? 255 : 0;
+                    const error = oldPixel - newPixel;
+
+                    // Fill the entire block with the new color
+                    for (let by = 0; by < blockSize && y + by < height; by++) {
+                        for (let bx = 0; bx < blockSize && x + bx < width; bx++) {
+                            const idx = ((y + by) * width + (x + bx)) * 4;
+                            newData[idx + c] = newPixel;
+                        }
+                    }
+
+                    // Distribute error to neighboring blocks
+                    if (x + blockSize < width) {
+                        this._distributeBlockError(newData, x + blockSize, y, c, error * 7/16, width, blockSize, height);
+                    }
+                    if (y + blockSize < height) {
+                        if (x - blockSize >= 0) {
+                            this._distributeBlockError(newData, x - blockSize, y + blockSize, c, error * 3/16, width, blockSize, height);
+                        }
+                        this._distributeBlockError(newData, x, y + blockSize, c, error * 5/16, width, blockSize, height);
+                        if (x + blockSize < width) {
+                            this._distributeBlockError(newData, x + blockSize, y + blockSize, c, error * 1/16, width, blockSize, height);
+                        }
+                    }
+                }
+            }
+        }
+
+        return new ImageData(newData, width, height);
+    },
+
+    /**
+     * Distributes error to neighboring blocks
+     * @param {Uint8ClampedArray} data - Image data
+     * @param {number} x - X coordinate
+     * @param {number} y - Y coordinate
+     * @param {number} channel - Color channel
+     * @param {number} error - Error value to distribute
+     * @param {number} width - Image width
+     * @param {number} blockSize - Size of processing blocks
+     * @param {number} height - Image height
+     */
+    _distributeBlockError(data, x, y, channel, error, width, blockSize, height) {
+        for (let by = 0; by < blockSize && y + by < height; by++) {
+            for (let bx = 0; bx < blockSize && x + bx < width; bx++) {
+                const idx = ((y + by) * width + (x + bx)) * 4;
+                data[idx + channel] = Math.max(0, Math.min(255, data[idx + channel] + error));
+            }
+        }
+    },
+
+    /**
+     * Applies ordered dithering using a Bayer matrix
+     * And implements threshold-based dithering with block processing
+     * Also uses a 4x4 Bayer matrix pattern for structured dithering
+     * @param {Uint8ClampedArray} data - Image data
+     * @param {number} width - Image width
+     * @param {number} height - Image height
+     * @returns {ImageData} Dithered image data
+     */
+    _orderedDither(data, width, height) {
+        const newData = new Uint8ClampedArray(data);
+        const matrix = [
+            [0, 8, 2, 10],
+            [12, 4, 14, 6],
+            [3, 11, 1, 9],
+            [15, 7, 13, 5]
+        ];
+        const matrixSize = 4;
+        const threshold = 128;
+        const levels = 4;
+        const blockSize = this.currentBlockSize;
+
+        // Process in blocks
+        for (let y = 0; y < height; y += blockSize) {
+            for (let x = 0; x < width; x += blockSize) {
+                // Calculate block average
+                let blockSum = [0, 0, 0];
+                let pixelCount = 0;
+                
+                for (let by = 0; by < blockSize && y + by < height; by++) {
+                    for (let bx = 0; bx < blockSize && x + bx < width; bx++) {
+                        const idx = ((y + by) * width + (x + bx)) * 4;
+                        for (let c = 0; c < 3; c++) {
+                            blockSum[c] += newData[idx + c];
+                        }
+                        pixelCount++;
+                    }
+                }
+
+                // Calculate block average
+                const blockAvg = blockSum.map(sum => sum / pixelCount);
+                
+                // Get matrix value for this block
+                const matrixX = Math.floor(x / blockSize) % matrixSize;
+                const matrixY = Math.floor(y / blockSize) % matrixSize;
+                const matrixValue = matrix[matrixY][matrixX] * 16;
+
+                // Apply dithering to the block
+                for (let c = 0; c < 3; c++) {
+                    const quantizedPixel = this._quantize(blockAvg[c], levels);
+                    const newPixel = quantizedPixel + matrixValue > threshold ? 255 : 0;
+
+                    // Fill the entire block with the new color
+                    for (let by = 0; by < blockSize && y + by < height; by++) {
+                        for (let bx = 0; bx < blockSize && x + bx < width; bx++) {
+                            const idx = ((y + by) * width + (x + bx)) * 4;
+                            newData[idx + c] = newPixel;
+                        }
+                    }
+                }
+            }
+        }
+
+        return new ImageData(newData, width, height);
+    },
+
+    /**
+     * Applies Atkinson dithering algorithm
+     * @param {Uint8ClampedArray} data - Image data
+     * @param {number} width - Image width
+     * @param {number} height - Image height
+     * @returns {ImageData} Dithered image data
+     */
+    _atkinsonDither(data, width, height) {
+        const newData = new Uint8ClampedArray(data);
+        const threshold = 128;
+        const levels = 4;
+        const blockSize = this.currentBlockSize;
+
+        // Process in blocks
+        for (let y = 0; y < height; y += blockSize) {
+            for (let x = 0; x < width; x += blockSize) {
+                // Calculate block average
+                let blockSum = [0, 0, 0];
+                let pixelCount = 0;
+                
+                for (let by = 0; by < blockSize && y + by < height; by++) {
+                    for (let bx = 0; bx < blockSize && x + bx < width; bx++) {
+                        const idx = ((y + by) * width + (x + bx)) * 4;
+                        for (let c = 0; c < 3; c++) {
+                            blockSum[c] += newData[idx + c];
+                        }
+                        pixelCount++;
+                    }
+                }
+
+                // Calculate block average
+                const blockAvg = blockSum.map(sum => sum / pixelCount);
+                
+                // Apply dithering to the block average
+                for (let c = 0; c < 3; c++) {
+                    const oldPixel = blockAvg[c];
+                    const quantizedPixel = this._quantize(oldPixel, levels);
+                    const newPixel = quantizedPixel > threshold ? 255 : 0;
+                    const error = oldPixel - newPixel;
+
+                    // Fill the entire block with the new color
+                    for (let by = 0; by < blockSize && y + by < height; by++) {
+                        for (let bx = 0; bx < blockSize && x + bx < width; bx++) {
+                            const idx = ((y + by) * width + (x + bx)) * 4;
+                            newData[idx + c] = newPixel;
+                        }
+                    }
+
+                    // Distribute error to neighboring blocks (Atkinson pattern)
+                    if (x + blockSize < width) {
+                        this._distributeBlockError(newData, x + blockSize, y, c, error / 8, width, blockSize, height);
+                        if (x + blockSize * 2 < width) {
+                            this._distributeBlockError(newData, x + blockSize * 2, y, c, error / 8, width, blockSize, height);
+                        }
+                    }
+                    if (y + blockSize < height) {
+                        if (x - blockSize >= 0) {
+                            this._distributeBlockError(newData, x - blockSize, y + blockSize, c, error / 8, width, blockSize, height);
+                        }
+                        this._distributeBlockError(newData, x, y + blockSize, c, error / 8, width, blockSize, height);
+                        if (x + blockSize < width) {
+                            this._distributeBlockError(newData, x + blockSize, y + blockSize, c, error / 8, width, blockSize, height);
+                        }
+                    }
+                    if (y + blockSize * 2 < height && x + blockSize < width) {
+                        this._distributeBlockError(newData, x + blockSize, y + blockSize * 2, c, error / 8, width, blockSize, height);
+                    }
+                }
+            }
+        }
+
+        return new ImageData(newData, width, height);
+    },
+
+    /**
+     * Applies Bayer dithering algorithm
+     * @param {Uint8ClampedArray} data - Image data
+     * @param {number} width - Image width
+     * @param {number} height - Image height
+     * @returns {ImageData} Dithered image data
+     */
+    _bayerDither(data, width, height) {
+        const newData = new Uint8ClampedArray(data);
+        const blockSize = this.currentBlockSize;
+        
+        const bayerMatrix = [
+            [ 0, 8, 2, 10],
+            [12, 4, 14, 6 ],
+            [ 3, 11, 1, 9 ],
+            [15, 7, 13, 5 ]
+        ];
+        
+        const scaleFactor = 16;
+        
+        // Process in blocks
+        for (let y = 0; y < height; y += blockSize) {
+            for (let x = 0; x < width; x += blockSize) {
+                // Calculate block average
+                let blockSum = [0, 0, 0];
+                let pixelCount = 0;
+                
+                for (let by = 0; by < blockSize && y + by < height; by++) {
+                    for (let bx = 0; bx < blockSize && x + bx < width; bx++) {
+                        const idx = ((y + by) * width + (x + bx)) * 4;
+                        for (let c = 0; c < 3; c++) {
+                            blockSum[c] += newData[idx + c];
+                        }
+                        pixelCount++;
+                    }
+                }
+
+                // Calculate block average
+                const blockAvg = blockSum.map(sum => sum / pixelCount);
+                
+                // Get matrix value for this block position
+                const matrixX = Math.floor(x / blockSize) % 4;
+                const matrixY = Math.floor(y / blockSize) % 4;
+                const threshold = (bayerMatrix[matrixY][matrixX] * scaleFactor);
+                
+                // Apply dithering to the block
+                for (let c = 0; c < 3; c++) {
+                    const normalizedPixel = blockAvg[c];
+                    const newPixel = normalizedPixel > threshold ? 255 : 0;
+                    
+                    // Fill the entire block with the new color
+                    for (let by = 0; by < blockSize && y + by < height; by++) {
+                        for (let bx = 0; bx < blockSize && x + bx < width; bx++) {
+                            const idx = ((y + by) * width + (x + bx)) * 4;
+                            newData[idx + c] = newPixel;
+                        }
+                    }
+                }
+            }
+        }
+
+        // Preserve alpha channel
+        for (let i = 3; i < newData.length; i += 4) {
+            newData[i] = data[i];
+        }
+        
+        return new ImageData(newData, width, height);
+    }
+};
+
+// Legacy functions for backward compatibility
+function bayerDither(imageData, width) {
+    const data = new Uint8ClampedArray(imageData.data);
+    const height = imageData.data.length / 4 / width;
+    const blockSize = DitherManager.currentBlockSize;
+    
+    // 8x8 Bayer matrix normalized to 0-1 range
+    const bayerMatrix = [
+        [ 0, 48, 12, 60,  3, 51, 15, 63],
+        [32, 16, 44, 28, 35, 19, 47, 31],
+        [ 8, 56,  4, 52, 11, 59,  7, 55],
+        [40, 24, 36, 20, 43, 27, 39, 23],
+        [ 2, 50, 14, 62,  1, 49, 13, 61],
+        [34, 18, 46, 30, 33, 17, 45, 29],
+        [10, 58,  6, 54,  9, 57,  5, 53],
+        [42, 26, 38, 22, 41, 25, 37, 21]
+    ].map(row => row.map(x => x / 64));
+
+    // Process in blocks
+    for (let y = 0; y < height; y += blockSize) {
+        for (let x = 0; x < width; x += blockSize) {
+            // Calculate block average
+            let blockSum = [0, 0, 0];
+            let pixelCount = 0;
+            
+            for (let by = 0; by < blockSize && y + by < height; by++) {
+                for (let bx = 0; bx < blockSize && x + bx < width; bx++) {
+                    const idx = ((y + by) * width + (x + bx)) * 4;
+                    for (let c = 0; c < 3; c++) {
+                        blockSum[c] += data[idx + c];
+                    }
+                    pixelCount++;
+                }
+            }
+
+            // Calculate block average
+            const blockAvg = blockSum.map(sum => sum / pixelCount);
+            
+            // Get threshold from Bayer matrix for this block
+            const matrixX = Math.floor(x / blockSize) % 8;
+            const matrixY = Math.floor(y / blockSize) % 8;
+            const threshold = bayerMatrix[matrixY][matrixX];
+            
+            // Apply threshold to the block
+            for (let c = 0; c < 3; c++) {
+                const normalizedPixel = blockAvg[c] / 255;
+                const newPixel = normalizedPixel > threshold ? 255 : 0;
+                
+                // Fill the entire block with the new color
+                for (let by = 0; by < blockSize && y + by < height; by++) {
+                    for (let bx = 0; bx < blockSize && x + bx < width; bx++) {
+                        const idx = ((y + by) * width + (x + bx)) * 4;
+                        data[idx + c] = newPixel;
+                    }
+                }
+            }
+        }
+    }
+    
+    // Preserve alpha channel
+    for (let i = 3; i < data.length; i += 4) {
+        data[i] = imageData.data[i];
+    }
+    
+    return data;
+}
+
+function applyDithering(imageData, width) {
+    const method = document.getElementById('dither-select').value;
+    return DitherManager.applyDither(imageData, method);
+}
+
+function floydSteinbergDither(imageData, width, blockSize) {
+    const { data } = imageData;
+    const height = imageData.data.length / 4 / width;
+
+    const newData = new Uint8ClampedArray(data);
+    const threshold = 128;
+    const levels = 4;
+
+    for (let y = 0; y < height; y++) {
+        for (let x = 0; x < width; x++) {
+            const idx = (y * width + x) * 4;
+            
+            for (let c = 0; c < 3; c++) {
+                const oldPixel = data[idx + c];
+                const quantizedPixel = DitherManager._quantize(oldPixel, levels);
+                const newPixel = quantizedPixel > threshold ? 255 : 0;
+                const error = oldPixel - newPixel;
+                
+                newData[idx + c] = newPixel;
+                
+                if (x + 1 < width) {
+                    newData[idx + 4 + c] += error * 7 / 16;
+                }
+                if (y + 1 === height) continue;
+                if (x > 0) {
+                    newData[idx + width * 4 - 4 + c] += error * 3 / 16;
+                }
+                newData[idx + width * 4 + c] += error * 5 / 16;
+                if (x + 1 < width) {
+                    newData[idx + width * 4 + 4 + c] += error * 1 / 16;
+                }
+            }
+        }
+    }
+
+    return new ImageData(newData, width, height);
+}
+
+function atkinsonDither(imageData, width, blockSize) {
+    const { data } = imageData;
+    const height = imageData.data.length / 4 / width;
+
+    const newData = new Uint8ClampedArray(data);
+    const threshold = 128;
+    const levels = 4;
+
+    for (let y = 0; y < height; y++) {
+        for (let x = 0; x < width; x++) {
+            const idx = (y * width + x) * 4;
+            
+            for (let c = 0; c < 3; c++) {
+                const oldPixel = data[idx + c];
+                const quantizedPixel = DitherManager._quantize(oldPixel, levels);
+                const newPixel = quantizedPixel > threshold ? 255 : 0;
+                const error = oldPixel - newPixel;
+                
+                newData[idx + c] = newPixel;
+                
+                if (x + 1 < width) {
+                    newData[idx + 4 + c] += error / 8;
+                }
+                if (x + 2 < width) {
+                    newData[idx + 8 + c] += error / 8;
+                }
+                if (y + 1 === height) continue;
+                if (x > 0) {
+                    newData[idx + width * 4 - 4 + c] += error / 8;
+                }
+                newData[idx + width * 4 + c] += error / 8;
+                if (x + 1 < width) {
+                    newData[idx + width * 4 + 4 + c] += error / 8;
+                }
+                if (y + 2 === height) continue;
+                if (x + 1 < width) {
+                    newData[idx + width * 8 + 4 + c] += error / 8;
+                }
+            }
+        }
+    }
+
+    return new ImageData(newData, width, height);
+}
+
+function orderedDither(imageData, width, blockSize) {
+    const { data } = imageData;
+    const height = imageData.data.length / 4 / width;
+    
+    const newData = new Uint8ClampedArray(data);
+    const matrix = [
+        [0, 8, 2, 10],
+        [12, 4, 14, 6],
+        [3, 11, 1, 9],
+        [15, 7, 13, 5]
+    ];
+    const matrixSize = 4;
+    const threshold = 128;
+    const levels = 4;
+
+    for (let y = 0; y < height; y++) {
+        for (let x = 0; x < width; x++) {
+            const idx = (y * width + x) * 4;
+            const matrixX = x % matrixSize;
+            const matrixY = y % matrixSize;
+            const matrixValue = matrix[matrixY][matrixX] * 16;
+
+            for (let c = 0; c < 3; c++) {
+                const pixel = data[idx + c];
+                const quantizedPixel = DitherManager._quantize(pixel, levels);
+                newData[idx + c] = quantizedPixel + matrixValue > threshold ? 255 : 0;
+            }
+        }
+    }
+
+    return new ImageData(newData, width, height);
+} 
\ No newline at end of file
diff --git a/js/leibovitz/favicon-16x16.png b/js/leibovitz/favicon-16x16.png
new file mode 100644
index 0000000..9293108
--- /dev/null
+++ b/js/leibovitz/favicon-16x16.png
Binary files differdiff --git a/js/leibovitz/favicon-32x32.png b/js/leibovitz/favicon-32x32.png
new file mode 100644
index 0000000..b6b0694
--- /dev/null
+++ b/js/leibovitz/favicon-32x32.png
Binary files differdiff --git a/js/leibovitz/favicon-96x96.png b/js/leibovitz/favicon-96x96.png
new file mode 100644
index 0000000..a4fcd87
--- /dev/null
+++ b/js/leibovitz/favicon-96x96.png
Binary files differdiff --git a/js/leibovitz/favicon.ico b/js/leibovitz/favicon.ico
new file mode 100644
index 0000000..8ce4e6e
--- /dev/null
+++ b/js/leibovitz/favicon.ico
Binary files differdiff --git a/js/leibovitz/index.html b/js/leibovitz/index.html
new file mode 100644
index 0000000..6207f07
--- /dev/null
+++ b/js/leibovitz/index.html
@@ -0,0 +1,548 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Leibovitz</title>
+    <meta name="description" content="Leibovitz is a web-based camera that lets you make fun photos.">
+    <link rel="apple-touch-icon" sizes="57x57" href="apple-icon-57x57.png">
+    <link rel="apple-touch-icon" sizes="60x60" href="apple-icon-60x60.png">
+    <link rel="apple-touch-icon" sizes="72x72" href="apple-icon-72x72.png">
+    <link rel="apple-touch-icon" sizes="76x76" href="apple-icon-76x76.png">
+    <link rel="apple-touch-icon" sizes="114x114" href="apple-icon-114x114.png">
+    <link rel="apple-touch-icon" sizes="120x120" href="apple-icon-120x120.png">
+    <link rel="apple-touch-icon" sizes="144x144" href="apple-icon-144x144.png">
+    <link rel="apple-touch-icon" sizes="152x152" href="apple-icon-152x152.png">
+    <link rel="apple-touch-icon" sizes="180x180" href="apple-icon-180x180.png">
+    <link rel="icon" type="image/png" sizes="192x192"  href="android-icon-192x192.png">
+    <link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
+    <link rel="icon" type="image/png" sizes="96x96" href="favicon-96x96.png">
+    <link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
+    <link rel="manifest" href="manifest.json">
+    <meta name="msapplication-TileColor" content="#ffffff">
+    <meta name="msapplication-TileImage" content="ms-icon-144x144.png">
+    <meta name="theme-color" content="#ffffff">
+    <style>
+        @font-face {
+            font-family: 'ChicagoFLF';
+            src: url('./ChicagoFLF.ttf') format('truetype');
+            font-weight: normal;
+            font-style: normal;
+        }
+        body, html {
+            margin: 0;
+            padding: 0;
+            width: 100%;
+            height: 100%;
+            display: flex;
+            flex-direction: column;
+            background-color: beige;
+            overflow: hidden;
+            font-family: 'ChicagoFLF', sans-serif;
+            font-size: 16px;
+        }
+        .preview-container {
+            flex: 1;
+            position: relative;
+            margin: 0;
+            min-height: 0;
+            padding-top: 28px;
+        }
+        #canvas {
+            width: 100%;
+            height: 100%;
+            object-fit: contain;
+            display: none;
+            background-color: transparent;
+            border-radius: 8px;
+            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+        }
+        .slide-controls {
+            position: absolute;
+            bottom: 20px;
+            left: 0;
+            right: 0;
+            display: none;
+            justify-content: space-around;
+            align-items: center;
+            background-color: rgba(255, 255, 255, 0.8);
+            padding: 10px;
+            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+            z-index: 10;
+            margin: 0 20px;
+        }
+        .slide-controls.visible {
+            display: flex;
+        }
+        .slider-group {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            gap: 4px;
+            flex: 1;
+            max-width: 200px;
+        }
+        .slider-group label {
+            font-size: 12px;
+            color: #666;
+            font-family: 'ChicagoFLF', sans-serif;
+        }
+        .slider-group input[type="range"] {
+            width: 100%;
+            height: 4px;
+            -webkit-appearance: none;
+            background: rgba(0, 0, 0, 0.2);
+            border-radius: 2px;
+            outline: none;
+        }
+        .slider-group input[type="range"]::-webkit-slider-thumb {
+            -webkit-appearance: none;
+            width: 16px;
+            height: 16px;
+            background: teal;
+            border-radius: 0;
+            cursor: pointer;
+        }
+        .slider-group input[type="range"]::-moz-range-thumb {
+            width: 16px;
+            height: 16px;
+            background: teal;
+            border-radius: 0;
+            cursor: pointer;
+            border: none;
+        }
+        .slider-group .value {
+            font-size: 12px;
+            color: #666;
+            font-family: 'ChicagoFLF', sans-serif;
+        }
+        .side-control {
+            position: absolute;
+            top: 50%;
+            height: 100%;
+            width: auto;
+            transform: translateY(-50%);
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            gap: 1em;
+            padding: 1em 0;
+            background-color: rgba(255, 255, 255, 0.8);
+            z-index: 10;
+        }
+        .side-control.left {
+            left: 0;
+        }
+        .side-control.right {
+            right: 0;
+        }
+
+        .vertical-slider {
+            transform: rotate(-90deg);
+            width: 200px;
+            margin: 90px -80px;
+            cursor: pointer;
+        }
+        input[type="range"]::-webkit-slider-thumb, .vertical-slider::-webkit-slider-thumb {
+            -webkit-appearance: none;
+            appearance: none;
+            width: 20px;
+            height: 20px;
+            background: teal;
+            border-radius: 0;
+            cursor: pointer;
+        }
+        input[type="range"]::-webkit-slider-runnable-track, .vertical-slider::-webkit-slider-runnable-track {
+            width: 100%;
+            height: 4px;
+            background: rgba(255, 255, 255, 0.8);
+            border-radius: 2px;
+        }
+        input[type="range"]::-moz-range-thumb, .vertical-slider::-moz-range-thumb {
+            width: 20px;
+            height: 20px;
+            background: teal;
+            border-radius: 0;
+            cursor: pointer;
+            border: none;
+        }
+        input[type="range"]::-moz-range-track, .vertical-slider::-moz-range-track {
+            width: 100%;
+            height: 4px;
+            background: rgba(255, 255, 255, 0.8);
+            border-radius: 2px;
+        }
+        .vertical-label {
+            transform: rotate(90deg);
+            font-size: 12px;
+            color: #666;
+            user-select: none;
+            white-space: nowrap;
+            margin: 20px 0;
+            font-family: 'ChicagoFLF', sans-serif;
+            padding: 0 5px;
+        }
+        #blur-value {
+            font-size: 12px;
+            color: #666;
+            user-select: none;
+            white-space: nowrap;
+            font-family: 'ChicagoFLF', sans-serif;
+            padding: 0 5px;
+        }
+        #contrast-value {
+            font-size: 12px;
+            color: #666;
+            user-select: none;
+            white-space: nowrap;
+            font-family: 'ChicagoFLF', sans-serif;
+            padding: 0 5px;
+        }
+        #controls {
+            width: 100%;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            gap: 0;
+            padding: 0;
+            background-color: rgba(255, 255, 255, 0.8);
+            box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
+            flex-shrink: 0;
+        }
+        #settings-container {
+            width: 100%;
+            display: none;
+            flex-direction: column;
+            gap: 10px;
+            align-items: center;
+            background-color: rgba(255, 255, 255, 0.8);
+            padding: 10px;
+            box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
+            flex-shrink: 0;
+            position: relative;
+        }
+        #settings-container.visible {
+            display: flex;
+        }
+        #offline-status {
+            position: fixed;
+            top: 0;
+            left: 0;
+            right: 0;
+            width: 100%;
+            font-size: 12px;
+            color: #666;
+            display: none;
+            font-family: 'ChicagoFLF', sans-serif;
+            background-color: rgba(255, 255, 255, 0.8);
+            text-align: center;
+            padding: 4px;
+            z-index: 1000;
+            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+        }
+        #offline-status.visible {
+            display: block;
+        }
+        .top-controls {
+            display: flex;
+            gap: 10px;
+            align-items: center;
+            width: 100%;
+            justify-content: center;
+            flex-wrap: nowrap;
+        }
+        .color-controls {
+            display: flex;
+            gap: 5px;
+            align-items: center;
+            flex-shrink: 0;
+        }
+        #reset-color {
+            padding: 8px;
+            font-size: 18px;
+            background: none;
+            border: 1px solid #ccc;
+            border-radius: 4px;
+            cursor: pointer;
+            transition: all 0.2s ease;
+            font-family: 'ChicagoFLF', sans-serif;
+            width: 40px;
+            height: 40px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+        }
+        input[type="color"] {
+            width: 40px;
+            height: 40px;
+            padding: 0;
+            border: 1px solid #ccc;
+            border-radius: 4px;
+            cursor: pointer;
+        }
+        select {
+            padding: 10px 15px;
+            font-family: 'ChicagoFLF', sans-serif;
+            -webkit-appearance: none;
+            -moz-appearance: none;
+            appearance: none;
+            background-color: #f0f0f0;
+            border: 1px solid #ccc;
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 16px;
+            line-height: 1.2;
+            color: #333;
+            background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%23333'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
+            background-repeat: no-repeat;
+            background-position: right 10px center;
+            background-size: 16px;
+            padding-right: 35px;
+        }
+        select:focus {
+            outline: none;
+            border-color: teal;
+            box-shadow: 0 0 0 2px rgba(0, 128, 128, 0.2);
+        }
+        #dither-select {
+            min-width: 200px;
+            max-width: none;
+            flex-shrink: 0;
+        }
+        button.capture {
+            background-color: teal;
+            color: #FFFFFF;
+            padding: 10px 20px;
+            font-family: 'ChicagoFLF', sans-serif;
+        }
+        #toggle-camera {
+            padding: 10px 20px;
+            font-family: 'ChicagoFLF', sans-serif;
+            border-right: 1px solid rgba(0, 0, 0, 0.1);
+        }
+        #toggle-camera.hidden {
+            display: none;
+        }
+        button, select, input[type="color"] {
+            padding: 10px;
+            font-size: 18px;
+            cursor: pointer;
+            font-family: 'ChicagoFLF', sans-serif;
+        }
+
+        button:hover, button:focus {
+            outline: none; 
+        }
+
+        button.capture:disabled {
+            background-color: #ccc;
+            color: #5c5c5c;
+        }
+
+        .contrast-control {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            gap: 4px;
+            width: 100%;
+            max-width: 300px;
+            margin: 0 auto;
+            padding: 0 10px;
+        }
+        .contrast-control label {
+            font-size: 12px;
+            color: #666;
+            width: 100%;
+            text-align: center;
+            font-family: 'ChicagoFLF', sans-serif;
+        }
+        .contrast-control input[type="range"] {
+            width: 100%;
+        }
+        .contrast-control .slider-container {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            width: 100%;
+        }
+        .contrast-control .slider-container input[type="range"] {
+            flex: 1;
+        }
+        #block-size-value {
+            font-size: 12px;
+            color: #666;
+            min-width: 40px;
+            text-align: right;
+            font-family: 'ChicagoFLF', sans-serif;
+        }
+        #focus-container {
+            display: none;
+        }
+        #toggle-camera, button.capture, #edit-image {
+            font-size: 18px;
+            padding: 20px;
+            font-family: 'ChicagoFLF', sans-serif;
+            border-radius: 0;
+            text-align: center;
+            flex: 1;
+        }
+        button.capture:disabled {
+            background-color: #ccc;
+            color: #5c5c5c;
+        }
+
+        @media (max-width: 600px) {
+            #controls {
+                flex-direction: column;
+            }
+
+            #toggle-camera, button.capture, #edit-image {
+                width: 100%;
+                border-right: none;
+                border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+            }
+
+            #toggle-camera:last-child, button.capture:last-child, #edit-image:last-child {
+                border-bottom: none;
+            }
+
+            .slide-controls {
+                flex-direction: column;
+                gap: 12px;
+                padding: 15px;
+            }
+            .slider-group {
+                width: 100%;
+                max-width: none;
+                flex-direction: row;
+                align-items: center;
+                gap: 12px;
+            }
+            .slider-group label {
+                min-width: 80px;
+                text-align: left;
+                font-size: 12px;
+                color: #666;
+            }
+            .slider-group input[type="range"] {
+                flex: 1;
+            }
+            .slider-group .value {
+                min-width: 40px;
+                text-align: right;
+                font-size: 12px;
+                color: #666;
+            }
+            select {
+                width: 100%;
+                max-width: 100%;
+            }
+            .top-controls {
+                flex-wrap: nowrap;
+                width: auto;
+                gap: 5px;
+            }
+            
+            #dither-select {
+                min-width: 150px;
+                max-width: none;
+            }
+
+            .color-controls {
+                flex-shrink: 0;
+            }
+        }
+        #edit-image {
+            display: block;
+            padding: 20px;
+            font-family: 'ChicagoFLF', sans-serif;
+            border-radius: 0;
+            text-align: center;
+            background-color: teal;
+            color: #FFFFFF;
+        }
+        #edit-image.hidden {
+            display: none;
+        }
+    </style>
+</head>
+<body>
+
+<div class="preview-container">
+    <canvas id="canvas"></canvas>
+    <div class="slide-controls">
+        <div class="slider-group">
+            <label for="blur-slider">Blur</label>
+            <input type="range" id="blur-slider" min="0" max="20" value="0" step="1">
+            <span class="value" id="blur-value">0%</span>
+        </div>
+        <div class="slider-group">
+            <label for="contrast-slider">Contrast</label>
+            <input type="range" id="contrast-slider" min="-255" max="255" value="0" step="1">
+            <span class="value" id="contrast-value">0</span>
+        </div>
+        <div class="slider-group">
+            <label for="balance-slider">Balance</label>
+            <input type="range" id="balance-slider" min="2000" max="10000" value="6500" step="100">
+            <span class="value" id="balance-value">6500K</span>
+        </div>
+        <div class="slider-group" id="focus-control" style="display: none;">
+            <label for="focus-slider">Focus</label>
+            <input type="range" id="focus-slider" min="0" max="100" step="1" value="50" disabled>
+            <span class="value" id="focus-value">50%</span>
+        </div>
+        <div class="slider-group" id="pixel-size-control" style="display: none;">
+            <label for="block-size-slider">Pixel Size</label>
+            <input type="range" id="block-size-slider" min="1" max="12" value="4" step="1">
+            <span class="value" id="block-size-value">4px</span>
+        </div>
+    </div>
+</div>
+
+<div id="settings-container">
+    <div id="offline-status">Offline Mode</div>
+    <div class="top-controls">
+        <select id="dither-select">
+            <option value="none">No Dithering</option>
+            <option value="floyd-steinberg">Floyd-Steinberg</option>
+            <option value="ordered">Ordered</option>
+            <option value="atkinson">Atkinson</option>
+            <option value="bayer">Bayer</option>
+        </select>
+        <div class="color-controls">
+            <input type="color" id="color-tint" title="Color Tint">
+            <button id="reset-color" title="Reset Color Tint">↺</button>
+        </div>
+    </div>
+</div>
+
+<div id="controls">
+    <button id="toggle-camera">Camera On</button>
+    <button id="edit-image" class="edit-image">Upload Image</button>
+    <button id="capture" disabled class="capture">Capture Image</button>
+    <input type="file" id="image-input" accept="image/*" style="display: none;">
+</div>
+
+<script src="dither.js"></script>
+<script src="contrast.js"></script>
+<script src="color.js"></script>
+<script src="blur.js"></script>
+<script src="balance.js"></script>
+<script src="leibovitz.js"></script>
+<script>
+// Add offline status handling
+window.addEventListener('online', () => {
+    document.getElementById('offline-status').classList.remove('visible');
+});
+
+window.addEventListener('offline', () => {
+    document.getElementById('offline-status').classList.add('visible');
+});
+
+// Check initial online status
+if (!navigator.onLine) {
+    document.getElementById('offline-status').classList.add('visible');
+}
+</script>
+</body>
+</html>
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
diff --git a/js/leibovitz/manifest.json b/js/leibovitz/manifest.json
new file mode 100644
index 0000000..1ddc0b2
--- /dev/null
+++ b/js/leibovitz/manifest.json
@@ -0,0 +1,71 @@
+{
+ "name": "Leibovitz",
+ "short_name": "Leibovitz",
+ "description": "A web-based camera that lets you make fun photos",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#f5f5dc",
+ "theme_color": "#f5f5dc",
+ "orientation": "portrait",
+ "icons": [
+  {
+   "src": "android-icon-36x36.png",
+   "sizes": "36x36",
+   "type": "image/png",
+   "density": "0.75"
+  },
+  {
+   "src": "android-icon-48x48.png",
+   "sizes": "48x48",
+   "type": "image/png",
+   "density": "1.0"
+  },
+  {
+   "src": "android-icon-72x72.png",
+   "sizes": "72x72",
+   "type": "image/png",
+   "density": "1.5"
+  },
+  {
+   "src": "android-icon-96x96.png",
+   "sizes": "96x96",
+   "type": "image/png",
+   "density": "2.0"
+  },
+  {
+   "src": "android-icon-144x144.png",
+   "sizes": "144x144",
+   "type": "image/png",
+   "density": "3.0"
+  },
+  {
+   "src": "android-icon-192x192.png",
+   "sizes": "192x192",
+   "type": "image/png",
+   "density": "4.0"
+  },
+  {
+   "src": "apple-icon-180x180.png",
+   "sizes": "180x180",
+   "type": "image/png"
+  }
+ ],
+ "categories": ["photo", "camera", "art"],
+ "prefer_related_applications": false,
+ "shortcuts": [
+  {
+   "name": "Take Photo",
+   "short_name": "Camera",
+   "description": "Open camera to take a photo",
+   "url": "?action=camera",
+   "icons": [{ "src": "android-icon-96x96.png", "sizes": "96x96" }]
+  },
+  {
+   "name": "Edit Photo",
+   "short_name": "Edit",
+   "description": "Open photo editor",
+   "url": "?action=edit",
+   "icons": [{ "src": "android-icon-96x96.png", "sizes": "96x96" }]
+  }
+ ]
+}
\ No newline at end of file
diff --git a/js/leibovitz/ms-icon-144x144.png b/js/leibovitz/ms-icon-144x144.png
new file mode 100644
index 0000000..ae37a7e
--- /dev/null
+++ b/js/leibovitz/ms-icon-144x144.png
Binary files differdiff --git a/js/leibovitz/ms-icon-150x150.png b/js/leibovitz/ms-icon-150x150.png
new file mode 100644
index 0000000..d9edbdb
--- /dev/null
+++ b/js/leibovitz/ms-icon-150x150.png
Binary files differdiff --git a/js/leibovitz/ms-icon-310x310.png b/js/leibovitz/ms-icon-310x310.png
new file mode 100644
index 0000000..9512221
--- /dev/null
+++ b/js/leibovitz/ms-icon-310x310.png
Binary files differdiff --git a/js/leibovitz/ms-icon-70x70.png b/js/leibovitz/ms-icon-70x70.png
new file mode 100644
index 0000000..45b1734
--- /dev/null
+++ b/js/leibovitz/ms-icon-70x70.png
Binary files differdiff --git a/js/leibovitz/service-worker.js b/js/leibovitz/service-worker.js
new file mode 100644
index 0000000..9ac287e
--- /dev/null
+++ b/js/leibovitz/service-worker.js
@@ -0,0 +1,89 @@
+const CACHE_NAME = 'leibovitz-cache-v1';
+const urlsToCache = [
+    '.',
+    'index.html',
+    'leibovitz.js',
+    'blur.js',
+    'contrast.js',
+    'color.js',
+    'balance.js',
+    'dither.js',
+    'service-worker.js',
+    'ChicagoFLF.ttf',
+    'manifest.json',
+    // Icons
+    'android-icon-192x192.png',
+    'android-icon-512x512.png',
+    'favicon.ico',
+    'favicon-16x16.png',
+    'favicon-32x32.png',
+    'favicon-96x96.png',
+    'apple-icon-57x57.png',
+    'apple-icon-60x60.png',
+    'apple-icon-72x72.png',
+    'apple-icon-76x76.png',
+    'apple-icon-114x114.png',
+    'apple-icon-120x120.png',
+    'apple-icon-144x144.png',
+    'apple-icon-152x152.png'
+];
+
+// Install event - cache all necessary files
+self.addEventListener('install', event => {
+    event.waitUntil(
+        caches.open(CACHE_NAME)
+            .then(cache => {
+                return cache.addAll(urlsToCache);
+            })
+    );
+});
+
+// Activate event - clean up old caches
+self.addEventListener('activate', event => {
+    event.waitUntil(
+        caches.keys().then(cacheNames => {
+            return Promise.all(
+                cacheNames.map(cacheName => {
+                    if (cacheName !== CACHE_NAME) {
+                        return caches.delete(cacheName);
+                    }
+                })
+            );
+        })
+    );
+});
+
+// Fetch event - serve from cache, fallback to network
+self.addEventListener('fetch', event => {
+    event.respondWith(
+        caches.match(event.request)
+            .then(response => {
+                // Return cached response if found
+                if (response) {
+                    return response;
+                }
+                
+                // Clone the request because it can only be used once
+                const fetchRequest = event.request.clone();
+                
+                // Try to fetch from network
+                return fetch(fetchRequest).then(response => {
+                    // Check if we received a valid response
+                    if (!response || response.status !== 200 || response.type !== 'basic') {
+                        return response;
+                    }
+                    
+                    // Clone the response because it can only be used once
+                    const responseToCache = response.clone();
+                    
+                    // Cache the fetched response
+                    caches.open(CACHE_NAME)
+                        .then(cache => {
+                            cache.put(event.request, responseToCache);
+                        });
+                    
+                    return response;
+                });
+            })
+    );
+});
\ No newline at end of file
diff --git a/js/pipe.js b/js/pipe.js
index 69ccae3..ace5fb9 100644
--- a/js/pipe.js
+++ b/js/pipe.js
@@ -1,2 +1,6 @@
 const pipe = (...args) => args.reduce((acc, el) => el(acc));
 
+/* alt implementation
+const pipe = (...fns) => (initialValue) =>
+  fns.reduce((acc, fn) => fn(acc), initialValue);
+*/
\ No newline at end of file
diff --git a/js/sentiment/sentiment/index.html b/js/sentiment/sentiment/index.html
index 90698e4..f84d42c 100644
--- a/js/sentiment/sentiment/index.html
+++ b/js/sentiment/sentiment/index.html
@@ -3,7 +3,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Sentiment Analyzer</title>
+    <title>Naive Sentiment Analyzer</title>
     <link href="https://smallandnearlysilent.com/sentiment/PressStart2P-Regular.ttf" rel="stylesheet">
     <style>
         @font-face {
@@ -172,7 +172,6 @@
     </style>
 </head>
 <body>
-    <!-- Pixel art corners -->
     <div class="pixel-corner top-left"></div>
     <div class="pixel-corner top-right"></div>
     <div class="pixel-corner bottom-left"></div>
@@ -182,58 +181,33 @@
     
     <div class="window">
         <p>
-            This bookmarklet analyzes the emotional tone of any webpage.
+            This bookmarklet analyzes the emotional tone of any webpage...badly.
         </p>
     </div>
 
     <div class="installation">
         <h2>Installation guide</h2>
-        <p><strong>Drag this power-up to your inventory:</strong></p>
+        <p><strong>Drag this bookmarklet to your bookmarks bar:</strong></p>
         <a class="bookmarklet" href="javascript:void function(){let e=document.createElement('script');e.src='https://smallandnearlysilent.com/sentiment/sentiment.browser.js',e.onload=function(){analyzePage()},document.head.appendChild(e)}();">
             Sentiment
         </a>
-        
-        <p><strong>Installation steps:</strong></p>
-        <ol>
-            <li>Open your bookmarks (<code>ctrl/cmd + shift + b</code>)</li>
-            <li>Drag the bookmarklet to your bookmarks bar</li>
-            <li>Cook</li>
-        </ol>
     </div>
 
     <h2>How to use it</h2>
     <ol>
-        <li>Navigate to target webpage</li>
+        <li>Navigate to a web page</li>
         <li>Click the bookmarklet in your bookmarks bar</li>
         <li>It'll analyze the page and display the results in a popup</li>
     </ol>
 
-    <div class="window">
-        <h3>Results include:</h3>
-        <ul>
-            <li>Emotional alignment</li>
-            <li>Power level</li>
-            <li>Sentiment score</li>
-            <li>Word statistics</li>
-            <li>Positive/negative ratio</li>
-        </ul>
-    </div>
-
     <div class="warning">
-        <h3>Important notice</h3>
+        <h3>Considerations</h3>
         <ul>
-            <li>Optimal targets: articles & blog posts</li>
+            <li>Primarily works with articles and blog posts</li>
             <li>English text only</li>
-            <li>Some sites may have shields up</li>
-            <li>No data collection - stealth mode</li>
         </ul>
     </div>
 
-    <h2>Security Considerations</h2>
-    <p>
-        Everything runs locally. No data is transmitted anywhere.
-    </p>
-
     <footer>
         <p>
             <a href="sentiment.browser.js" target="_blank">VIEW THE SCRIPT</a>
@@ -243,7 +217,7 @@
     <script>
         document.querySelector('.bookmarklet').addEventListener('click', function(e) {
             e.preventDefault();
-            alert('COMMAND INVALID!\nDRAG TO INVENTORY (BOOKMARKS) INSTEAD!');
+            alert('COMMAND INVALID!\nDRAG TO BOOKMARKS INSTEAD OF CLICKING!');
         });
     </script>
 </body>