diff options
author | elioat <elioat@tilde.institute> | 2025-03-30 14:17:25 -0400 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2025-03-30 14:17:25 -0400 |
commit | dc35422ab591a9240485da2e69d10020eb1de5ff (patch) | |
tree | 1d315ce4b369040b1fd7c71b70ebe2bec999061c | |
parent | 79e9336d5334c229092ff228e1146a6fdf33793c (diff) | |
download | tour-dc35422ab591a9240485da2e69d10020eb1de5ff.tar.gz |
*
-rw-r--r-- | js/leibovitz/balance.js | 32 | ||||
-rw-r--r-- | js/leibovitz/blur.js | 59 | ||||
-rw-r--r-- | js/leibovitz/color.js | 72 | ||||
-rw-r--r-- | js/leibovitz/contrast.js | 36 | ||||
-rw-r--r-- | js/leibovitz/dither.js | 143 | ||||
-rw-r--r-- | js/leibovitz/leibovitz.js | 172 |
6 files changed, 357 insertions, 157 deletions
diff --git a/js/leibovitz/balance.js b/js/leibovitz/balance.js index 6566176..73f60e8 100644 --- a/js/leibovitz/balance.js +++ b/js/leibovitz/balance.js @@ -1,11 +1,25 @@ -// BalanceManager handles white balance adjustments +/** + * White balance management module implementing temperature-based color adjustment + * Uses the Observer pattern for state management and effect application + * Implements a non-linear temperature adjustment algorithm with RGB channel scaling + * Uses a static class pattern for state management + */ + class BalanceManager { + /** + * Initializes the balance manager and sets up UI controls + * Implements the Factory pattern for UI initialization + */ static init() { this.balanceSlider = document.getElementById('balance-slider'); this.balanceValue = document.getElementById('balance-value'); this._setupEventListeners(); } + /** + * Sets up event listeners for UI controls + * Implements the Observer pattern for state changes + */ static _setupEventListeners() { this.balanceSlider.addEventListener('input', () => { const value = this.balanceSlider.value; @@ -13,10 +27,26 @@ class BalanceManager { }); } + /** + * Gets the current white balance temperature + * @returns {number} Current temperature in Kelvin (2000K-10000K) + */ static getCurrentBalance() { return parseInt(this.balanceSlider.value); } + /** + * Applies white balance adjustment to an image + * Implements temperature-based RGB channel scaling with non-linear response + * @param {ImageData} imageData - Source image data + * @returns {ImageData} White balanced image data + * + * Algorithm: + * 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 + */ static applyBalance(imageData) { const balance = this.getCurrentBalance(); if (!balance || balance === 6500) return imageData; // 6500K is neutral diff --git a/js/leibovitz/blur.js b/js/leibovitz/blur.js index 08d1ec5..27fa480 100644 --- a/js/leibovitz/blur.js +++ b/js/leibovitz/blur.js @@ -1,5 +1,9 @@ -// Blur management module -// Uses the Observer pattern to notify the main camera module of blur changes +/** + * Blur management module implementing optimized box blur + * Uses the Observer pattern for state management and effect application + * Implements two-pass box blur algorithm with boundary detection + * Uses content-aware optimization for performance + */ const BlurManager = { // Private state @@ -8,14 +12,20 @@ const BlurManager = { _slider: null, _value: null, - // Initialize the blur manager + /** + * Initializes the blur manager and sets up UI controls + * Implements the Factory pattern for UI initialization + */ init() { this._slider = document.getElementById('blur-slider'); this._value = document.getElementById('blur-value'); this._setupEventListeners(); }, - // Private methods + /** + * Sets up event listeners for UI controls + * Implements the Observer pattern for state changes + */ _setupEventListeners() { this._slider.addEventListener('input', () => { const value = this._slider.value; @@ -29,7 +39,11 @@ const BlurManager = { this._observers.forEach(observer => observer(this._currentBlur)); }, - // Public methods + /** + * 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); @@ -39,7 +53,14 @@ const BlurManager = { return this._currentBlur; }, - // Apply Gaussian blur to an image + /** + * Applies optimized box blur to an image + * Implements two-pass blur with content-aware boundary detection + * Uses separate horizontal and vertical passes for performance + * @param {ImageData} imageData - Source image data + * @param {number} radius - Blur radius + * @returns {ImageData} Blurred image data + */ applyBlur(imageData, radius) { if (!radius) return imageData; @@ -132,28 +153,10 @@ const BlurManager = { return imageData; }, - // Create a 1D Gaussian kernel - _createGaussianKernel(radius) { - const sigma = radius / 3; - const kernelSize = Math.ceil(radius * 2 + 1); - const kernel = new Array(kernelSize); - let sum = 0; - - for (let i = 0; i < kernelSize; i++) { - const x = i - radius; - kernel[i] = Math.exp(-(x * x) / (2 * sigma * sigma)); - sum += kernel[i]; - } - - // Normalize the kernel - for (let i = 0; i < kernelSize; i++) { - kernel[i] /= sum; - } - - return kernel; - }, - - // Add reset method + /** + * Resets blur effect to default state + * Implements the Command pattern for state reset + */ reset() { this._currentBlur = 0; this._slider.value = 0; diff --git a/js/leibovitz/color.js b/js/leibovitz/color.js index 4319a21..1438403 100644 --- a/js/leibovitz/color.js +++ b/js/leibovitz/color.js @@ -1,5 +1,9 @@ -// Color tint management module -// Uses the Observer pattern to notify the main camera module of color tint changes +/** + * Color tint management module implementing HSL-based color manipulation + * Uses the Observer pattern for state management and effect application + * Implements HSL color space transformation with circular interpolation + * Uses noise reduction and smooth blending for quality + */ const ColorManager = { // Private state @@ -7,12 +11,18 @@ const ColorManager = { _observers: new Set(), _colorInput: null, - // Initialize the color manager + /** + * Initializes the color manager and sets up UI controls + * Implements the Factory pattern for UI initialization + */ init() { this._setupEventListeners(); }, - // Private methods + /** + * Sets up event listeners for UI controls + * Implements the Observer pattern for state changes + */ _setupEventListeners() { this._colorInput = document.getElementById('color-tint'); this._colorInput.addEventListener('input', (e) => { @@ -47,7 +57,11 @@ const ColorManager = { this._observers.forEach(observer => observer(this._currentColor)); }, - // Public methods + /** + * 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); @@ -57,7 +71,14 @@ const ColorManager = { return this._currentColor; }, - // Apply color tint to an image using a more sophisticated LUT approach + /** + * Applies color tint to an image using HSL color space + * Implements circular interpolation for hue blending + * 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; @@ -99,7 +120,11 @@ const ColorManager = { return imageData; }, - // Helper method to convert hex color to RGB + /** + * 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 ? [ @@ -109,7 +134,13 @@ const ColorManager = { ] : null; }, - // Convert RGB to HSL + /** + * 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; @@ -136,7 +167,13 @@ const ColorManager = { return [h, s, l]; }, - // Convert HSL to RGB + /** + * 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; @@ -163,7 +200,13 @@ const ColorManager = { return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; }, - // Blend two hue values (handles circular nature of hue) + /** + * 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) { @@ -176,7 +219,14 @@ const ColorManager = { return h1 + diff * factor; }, - // Smooth blending with noise reduction + /** + * 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); diff --git a/js/leibovitz/contrast.js b/js/leibovitz/contrast.js index 5c4bc1c..01312ad 100644 --- a/js/leibovitz/contrast.js +++ b/js/leibovitz/contrast.js @@ -1,5 +1,8 @@ -// Contrast management module -// Uses the Observer pattern to notify the main camera module of contrast changes +/** + * Contrast management module implementing contrast adjustment + * Uses the Observer pattern for state management and effect application + * Implements linear contrast adjustment algorithm + */ const ContrastManager = { // Private state @@ -7,12 +10,18 @@ const ContrastManager = { _observers: new Set(), _slider: null, - // Initialize the contrast manager + /** + * Initializes the contrast manager and sets up UI controls + * Implements the Factory pattern for UI initialization + */ init() { this._setupEventListeners(); }, - // Private methods + /** + * Sets up event listeners for UI controls + * Implements the Observer pattern for state changes + */ _setupEventListeners() { this._slider = document.getElementById('contrast-slider'); this._slider.addEventListener('input', (e) => { @@ -26,7 +35,11 @@ const ContrastManager = { this._observers.forEach(observer => observer(this._currentContrast)); }, - // Public methods + /** + * 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); @@ -36,7 +49,13 @@ const ContrastManager = { return this._currentContrast; }, - // Apply contrast to an image + /** + * 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; @@ -54,7 +73,10 @@ const ContrastManager = { return imageData; }, - // Add reset method + /** + * Resets contrast effect to default state + * Implements the Command pattern for state reset + */ reset() { this._currentContrast = 1.0; this._slider.value = 0; // Reset slider to middle position diff --git a/js/leibovitz/dither.js b/js/leibovitz/dither.js index 90d6325..3689354 100644 --- a/js/leibovitz/dither.js +++ b/js/leibovitz/dither.js @@ -1,5 +1,9 @@ -// Dithering management module -// Uses the Observer pattern to notify the main camera module of dithering changes +/** + * Dithering management module implementing various dithering algorithms + * Uses the Observer pattern for state management and effect application + * Implements block-based processing for performance optimization + * Uses the Strategy pattern for algorithm selection + */ const DitherManager = { // Private state @@ -9,13 +13,19 @@ const DitherManager = { _pixelSizeControl: null, currentBlockSize: 4, - // Initialize the dither manager + /** + * Initializes the dither manager and sets up UI controls + * Implements the Factory pattern for UI initialization + */ init() { this._setupEventListeners(); this._pixelSizeControl = document.getElementById('pixel-size-control'); }, - // Private methods + /** + * Sets up event listeners for UI controls + * Implements the Observer pattern for state changes + */ _setupEventListeners() { this._modeSelect = document.getElementById('dither-select'); this._modeSelect.addEventListener('change', (e) => { @@ -43,7 +53,11 @@ const DitherManager = { this._observers.forEach(observer => observer(this._currentMode)); }, - // Public methods + /** + * 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); @@ -53,7 +67,13 @@ const DitherManager = { return this._currentMode; }, - // Apply dithering to an image + /** + * Applies selected dithering algorithm to image data + * Implements the Strategy pattern for algorithm selection + * @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; @@ -75,20 +95,34 @@ const DitherManager = { } }, - // Quantize a value to create chunkier output + /** + * Quantizes a value to create chunkier output + * Implements the Strategy pattern for quantization + * @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; }, - // Floyd-Steinberg dithering + /** + * Applies Floyd-Steinberg dithering algorithm + * Implements error diffusion with block-based processing + * 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 + // Process in blocks for performance for (let y = 0; y < height; y += blockSize) { for (let x = 0; x < width; x += blockSize) { // Calculate block average @@ -143,7 +177,17 @@ const DitherManager = { return new ImageData(newData, width, height); }, - // Helper method to distribute error to blocks + /** + * 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++) { @@ -153,7 +197,15 @@ const DitherManager = { } }, - // Ordered dithering (Bayer matrix) + /** + * Applies ordered dithering using a Bayer matrix + * Implements threshold-based dithering with block processing + * 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 = [ @@ -211,7 +263,14 @@ const DitherManager = { return new ImageData(newData, width, height); }, - // Atkinson dithering with block-based processing + /** + * Applies Atkinson dithering algorithm + * Implements error diffusion with block-based processing + * @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; @@ -255,22 +314,22 @@ const DitherManager = { // Distribute error to neighboring blocks (Atkinson pattern) if (x + blockSize < width) { - this._distributeBlockError(newData, x + blockSize, y, c, error / 8, width, blockSize, height); // right + 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); // right + 1 + 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); // bottom left + 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); // bottom + 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); // bottom right + 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); // bottom + 1 + this._distributeBlockError(newData, x + blockSize, y + blockSize * 2, c, error / 8, width, blockSize, height); } } } @@ -279,12 +338,19 @@ const DitherManager = { return new ImageData(newData, width, height); }, - // Add this as a method in DitherManager + /** + * Applies Bayer dithering algorithm + * Implements threshold-based dithering with block processing + * @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; - // 4x4 Bayer matrix (simpler and more visible pattern than 8x8) + // 4x4 Bayer matrix for pattern generation const bayerMatrix = [ [ 0, 8, 2, 10], [12, 4, 14, 6 ], @@ -292,8 +358,7 @@ const DitherManager = { [15, 7, 13, 5 ] ]; - // Scale factor to make the pattern more pronounced - const scaleFactor = 16; // Increase this value to make pattern more visible + const scaleFactor = 16; // Process in blocks for (let y = 0; y < height; y += blockSize) { @@ -322,7 +387,6 @@ const DitherManager = { // Apply dithering to the block for (let c = 0; c < 3; c++) { - // Normalize pixel value and apply threshold const normalizedPixel = blockAvg[c]; const newPixel = normalizedPixel > threshold ? 255 : 0; @@ -346,7 +410,7 @@ const DitherManager = { } }; -// Update the Bayer dithering to use blocks +// Legacy functions for backward compatibility function bayerDither(imageData, width) { const data = new Uint8ClampedArray(imageData.data); const height = imageData.data.length / 4 / width; @@ -424,13 +488,12 @@ function floydSteinbergDither(imageData, width, blockSize) { const newData = new Uint8ClampedArray(data); const threshold = 128; - const levels = 4; // Number of quantization levels + const levels = 4; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const idx = (y * width + x) * 4; - // Process each color channel for (let c = 0; c < 3; c++) { const oldPixel = data[idx + c]; const quantizedPixel = DitherManager._quantize(oldPixel, levels); @@ -439,17 +502,16 @@ function floydSteinbergDither(imageData, width, blockSize) { newData[idx + c] = newPixel; - // Distribute error to neighboring pixels if (x + 1 < width) { - newData[idx + 4 + c] += error * 7 / 16; // right + newData[idx + 4 + c] += error * 7 / 16; } if (y + 1 === height) continue; if (x > 0) { - newData[idx + width * 4 - 4 + c] += error * 3 / 16; // bottom left + newData[idx + width * 4 - 4 + c] += error * 3 / 16; } - newData[idx + width * 4 + c] += error * 5 / 16; // bottom + newData[idx + width * 4 + c] += error * 5 / 16; if (x + 1 < width) { - newData[idx + width * 4 + 4 + c] += error * 1 / 16; // bottom right + newData[idx + width * 4 + 4 + c] += error * 1 / 16; } } } @@ -464,13 +526,12 @@ function atkinsonDither(imageData, width, blockSize) { const newData = new Uint8ClampedArray(data); const threshold = 128; - const levels = 4; // Number of quantization levels + const levels = 4; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const idx = (y * width + x) * 4; - // Process each color channel for (let c = 0; c < 3; c++) { const oldPixel = data[idx + c]; const quantizedPixel = DitherManager._quantize(oldPixel, levels); @@ -479,24 +540,23 @@ function atkinsonDither(imageData, width, blockSize) { newData[idx + c] = newPixel; - // Distribute error to neighboring pixels (Atkinson's algorithm) if (x + 1 < width) { - newData[idx + 4 + c] += error / 8; // right + newData[idx + 4 + c] += error / 8; } if (x + 2 < width) { - newData[idx + 8 + c] += error / 8; // right + 1 + newData[idx + 8 + c] += error / 8; } if (y + 1 === height) continue; if (x > 0) { - newData[idx + width * 4 - 4 + c] += error / 8; // bottom left + newData[idx + width * 4 - 4 + c] += error / 8; } - newData[idx + width * 4 + c] += error / 8; // bottom + newData[idx + width * 4 + c] += error / 8; if (x + 1 < width) { - newData[idx + width * 4 + 4 + c] += error / 8; // bottom right + 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; // bottom + 1 right + newData[idx + width * 8 + 4 + c] += error / 8; } } } @@ -518,7 +578,7 @@ function orderedDither(imageData, width, blockSize) { ]; const matrixSize = 4; const threshold = 128; - const levels = 4; // Number of quantization levels + const levels = 4; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { @@ -527,7 +587,6 @@ function orderedDither(imageData, width, blockSize) { const matrixY = y % matrixSize; const matrixValue = matrix[matrixY][matrixX] * 16; - // Process each color channel for (let c = 0; c < 3; c++) { const pixel = data[idx + c]; const quantizedPixel = DitherManager._quantize(pixel, levels); diff --git a/js/leibovitz/leibovitz.js b/js/leibovitz/leibovitz.js index 0b5bfdc..769216c 100644 --- a/js/leibovitz/leibovitz.js +++ b/js/leibovitz/leibovitz.js @@ -1,3 +1,10 @@ +/** + * Main application entry point for the Leibovitz camera app. + * Implements a functional architecture with separate managers for each effect. + * Uses the Observer pattern for state management and effect application. + * Implements the State pattern for mode management (camera/edit). + */ + const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const video = document.createElement('video'); @@ -15,16 +22,19 @@ let cameraOn = false; let stream = null; let track = null; let isEditMode = false; -let originalImage = null; // Store the original image +let originalImage = null; // Store the original image for edit mode -// Initialize managers +// Initialize managers - each implements the Observer pattern for state changes ColorManager.init(); DitherManager.init(); ContrastManager.init(); BlurManager.init(); BalanceManager.init(); -// Function to update slider controls visibility +/** + * Updates visibility of slider controls based on camera/edit mode state + * Uses the State pattern to manage UI visibility + */ function updateSliderControlsVisibility() { if (cameraOn || isEditMode) { slideControls.classList.add('visible'); @@ -33,36 +43,33 @@ function updateSliderControlsVisibility() { } } -// Set the canvas dimensions to match the window size +/** + * Updates canvas dimensions while maintaining aspect ratio + * Implements the Strategy pattern for different aspect ratio calculations + */ function updateCanvasSize() { - // Get the container dimensions const container = document.querySelector('.preview-container'); const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; - // If video is playing, use its aspect ratio if (video.videoWidth && video.videoHeight) { const videoAspect = video.videoWidth / video.videoHeight; const containerAspect = containerWidth / containerHeight; - // Determine dimensions that maintain aspect ratio while fitting in container if (containerAspect > videoAspect) { - // Container is wider than video canvas.height = containerHeight; canvas.width = containerHeight * videoAspect; } else { - // Container is taller than video canvas.width = containerWidth; canvas.height = containerWidth / videoAspect; } } else { - // Default to container dimensions until video starts canvas.width = containerWidth; canvas.height = containerHeight; } } -// Update canvas size when window is resized +// Observer pattern: Listen for window resize events window.addEventListener('resize', () => { if (cameraOn || isEditMode) { updateCanvasSize(); @@ -72,38 +79,34 @@ window.addEventListener('resize', () => { } }); -// Initialize canvas size updateCanvasSize(); -// Function to completely clear the canvas +/** + * Clears the canvas and resets its state + * Implements the Command pattern for canvas operations + */ function clearCanvas() { - // Get container dimensions const container = document.querySelector('.preview-container'); const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; - // Set canvas dimensions to match container canvas.width = containerWidth; canvas.height = containerHeight; - - // Clear the entire canvas context ctx.clearRect(0, 0, containerWidth, containerHeight); - - // Reset any transformations or other context properties ctx.setTransform(1, 0, 0, 1, 0, 0); - - // Hide the canvas canvas.style.display = 'none'; } +/** + * 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 we're in edit mode, show confirmation dialog if (isEditMode) { if (!confirm('Switching to camera mode will discard your current image and edits. Continue?')) { - return; // User cancelled + return; } - - // Clear edit mode state isEditMode = false; originalImage = null; clearCanvas(); @@ -114,19 +117,19 @@ function startCamera() { stream = s; video.srcObject = stream; video.play(); - canvas.style.display = 'block'; // Show the canvas + canvas.style.display = 'block'; captureButton.disabled = false; captureButton.active = true; editImageButton.classList.add('hidden'); - toggleCameraButton.classList.add('hidden'); // Hide the toggle button + toggleCameraButton.classList.add('hidden'); isEditMode = false; - originalImage = null; // Clear the original image - updateSliderControlsVisibility(); // Show slider controls + originalImage = null; + updateSliderControlsVisibility(); track = stream.getVideoTracks()[0]; const settings = track.getSettings(); - // Check if focus control is supported with this browser and device combo + // Feature detection for focus control if ('focusDistance' in settings) { focusControl.style.display = 'flex'; focusSlider.disabled = false; @@ -145,7 +148,7 @@ function startCamera() { focusControl.style.display = 'none'; } - // Draw the video feed to the canvas + // Animation loop using requestAnimationFrame video.addEventListener('play', function() { function step() { if (!cameraOn) return; @@ -162,10 +165,14 @@ function startCamera() { }) .catch(err => { console.error('Error accessing camera: ', err); - toggleCameraButton.classList.remove('hidden'); // Show the button again if there's an error + toggleCameraButton.classList.remove('hidden'); }); } +/** + * Stops camera stream and resets UI state + * Implements the Command pattern for cleanup operations + */ function stopCamera() { if (stream) { stream.getTracks().forEach(track => track.stop()); @@ -177,20 +184,23 @@ function stopCamera() { focusControl.style.display = 'none'; stream = null; editImageButton.classList.remove('hidden'); - toggleCameraButton.classList.remove('hidden'); // Show the toggle button again - updateSliderControlsVisibility(); // Hide slider controls if no image is loaded + toggleCameraButton.classList.remove('hidden'); + updateSliderControlsVisibility(); } } +/** + * Loads and displays an image file + * Implements the Factory pattern for image creation + * 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() { - // Clear any existing content first clearCanvas(); - // Calculate dimensions to maintain aspect ratio const container = document.querySelector('.preview-container'); const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; @@ -208,21 +218,16 @@ function loadImage(file) { canvasHeight = containerWidth / imgAspect; } - // Set canvas dimensions canvas.width = canvasWidth; canvas.height = canvasHeight; - - // Store the original image originalImage = img; - // Draw the new image ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight); canvas.style.display = 'block'; captureButton.disabled = false; captureButton.active = true; - updateSliderControlsVisibility(); // Show slider controls + updateSliderControlsVisibility(); - // Start the effect loop function step() { if (!isEditMode) return; applyEffects(); @@ -235,14 +240,13 @@ function loadImage(file) { reader.readAsDataURL(file); } +/** + * Applies all effects in sequence using the Chain of Responsibility pattern + * Each effect is applied using the Strategy pattern for algorithm selection + */ function applyEffects() { - // Clear the canvas first ctx.clearRect(0, 0, canvas.width, canvas.height); - - // Draw the original image ctx.drawImage(originalImage, 0, 0, canvas.width, canvas.height); - - // Apply effects in sequence applyContrast(); applyColorTint(); applyBlur(); @@ -250,8 +254,11 @@ function applyEffects() { applyDither(); } +/** + * Draws video feed maintaining aspect ratio + * Implements the Strategy pattern for aspect ratio handling + */ function drawVideoProportional() { - // Clear the canvas first ctx.clearRect(0, 0, canvas.width, canvas.height); const videoAspectRatio = video.videoWidth / video.videoHeight; @@ -270,10 +277,12 @@ function drawVideoProportional() { const offsetX = (canvas.width - drawWidth) / 2; const offsetY = (canvas.height - drawHeight) / 2; - // Draw the video content centered ctx.drawImage(video, offsetX, offsetY, drawWidth, drawHeight); } +/** + * Applies color tint effect using the Strategy pattern + */ function applyColorTint() { const currentColor = ColorManager.getCurrentColor(); if (!currentColor) return; @@ -283,12 +292,18 @@ function applyColorTint() { ctx.putImageData(tintedImageData, 0, 0); } +/** + * Applies white balance effect using the Strategy pattern + */ function applyBalance() { const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const balancedImageData = BalanceManager.applyBalance(imageData); ctx.putImageData(balancedImageData, 0, 0); } +/** + * Applies contrast effect using the Strategy pattern + */ function applyContrast() { const currentContrast = ContrastManager.getCurrentContrast(); if (!currentContrast || currentContrast === 1.0) return; @@ -298,6 +313,9 @@ function applyContrast() { ctx.putImageData(contrastedImageData, 0, 0); } +/** + * Applies dithering effect using the Strategy pattern + */ function applyDither() { const currentMode = DitherManager.getCurrentMode(); if (!currentMode || currentMode === 'none') return; @@ -307,6 +325,9 @@ function applyDither() { ctx.putImageData(ditheredImageData, 0, 0); } +/** + * Applies blur effect using the Strategy pattern + */ function applyBlur() { const currentBlur = BlurManager.getCurrentBlur(); if (!currentBlur) return; @@ -316,25 +337,25 @@ function applyBlur() { ctx.putImageData(blurredImageData, 0, 0); } +/** + * Captures the current canvas state with effects + * Implements the Command pattern for image capture + */ captureButton.addEventListener('click', () => { const currentColor = ColorManager.getCurrentColor(); const borderWidth = 4; - // Create a canvas with extra space for the border const captureCanvas = document.createElement('canvas'); const captureCtx = captureCanvas.getContext('2d'); - // Set dimensions including border captureCanvas.width = canvas.width + (borderWidth * 2); captureCanvas.height = canvas.height + (borderWidth * 2); - // Fill with border color if a tint is selected if (currentColor) { captureCtx.fillStyle = currentColor; captureCtx.fillRect(0, 0, captureCanvas.width, captureCanvas.height); } - // Draw the main canvas content captureCtx.drawImage(canvas, borderWidth, borderWidth); const link = document.createElement('a'); @@ -343,6 +364,9 @@ captureButton.addEventListener('click', () => { link.click(); }); +/** + * Toggles camera state using the State pattern + */ toggleCameraButton.addEventListener('click', () => { cameraOn = !cameraOn; if (cameraOn) { @@ -354,6 +378,9 @@ toggleCameraButton.addEventListener('click', () => { } }); +/** + * Handles image upload using the Factory pattern + */ editImageButton.addEventListener('click', () => { if (!cameraOn) { imageInput.click(); @@ -367,6 +394,10 @@ imageInput.addEventListener('change', (e) => { } }); +/** + * Service Worker registration for offline functionality + * Implements the Service Worker pattern for PWA support + */ if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js') @@ -380,14 +411,19 @@ if ('serviceWorker' in navigator) { ColorManager._setupEventListeners(); -// Update the reset handlers in each manager to trigger a redraw +/** + * Resets all effects using the Command pattern + */ function resetEffects() { if (isEditMode && originalImage) { applyEffects(); } } -// Add reset handlers to each manager +/** + * Reset handlers for each effect manager + * Implements the Command pattern for state reset + */ BlurManager.reset = function() { this._currentBlur = 0; this._slider.value = 0; @@ -424,6 +460,10 @@ DitherManager.reset = function() { resetEffects(); }; +/** + * Saves current settings to localStorage + * Implements the Memento pattern for state persistence + */ function saveSettings() { const settings = { blur: BlurManager._currentBlur, @@ -436,12 +476,15 @@ function saveSettings() { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } +/** + * Loads saved settings from localStorage + * Implements the Memento pattern for state restoration + */ function loadSettings() { const savedSettings = localStorage.getItem(SETTINGS_KEY); if (savedSettings) { const settings = JSON.parse(savedSettings); - // Apply saved settings if (settings.blur !== undefined) { BlurManager._currentBlur = settings.blur; BlurManager._slider.value = settings.blur; @@ -478,29 +521,22 @@ function loadSettings() { } } -// Add event listeners for settings changes +/** + * Sets up event listeners for settings changes + * Implements the Observer pattern for state change detection + */ function setupSettingsListeners() { - // Blur BlurManager._slider.addEventListener('change', saveSettings); - - // Contrast ContrastManager._slider.addEventListener('change', saveSettings); - - // Color ColorManager._colorInput.addEventListener('change', saveSettings); - - // Balance BalanceManager.balanceSlider.addEventListener('change', saveSettings); - - // Dither DitherManager._modeSelect.addEventListener('change', saveSettings); - // Block Size if (DitherManager._blockSizeSlider) { DitherManager._blockSizeSlider.addEventListener('change', saveSettings); } } -// Call this after all managers are initialized +// Initialize settings setupSettingsListeners(); loadSettings(); \ No newline at end of file |