UNPKG

@raven-js/cortex

Version:

Zero-dependency machine learning, AI, and data processing library for modern JavaScript

1,044 lines (950 loc) 33.6 kB
/** * @author Anonyfox <max@anonyfox.com> * @license MIT * @see {@link https://github.com/Anonyfox/ravenjs} * @see {@link https://ravenjs.dev} * @see {@link https://anonyfox.com} */ /** * @file Main color adjustment functionality for RGBA pixel data. * * This module provides efficient brightness and contrast adjustment operations * for RGBA pixel data. Operations preserve the alpha channel and use optimized * lookup tables for maximum performance on large images. * * @example * // Adjust brightness * const result = adjustBrightness(pixels, 800, 600, 1.2); * * // Adjust contrast * const result = adjustContrast(pixels, 800, 600, 1.5); * * // Combine both adjustments * const result = adjustBrightnessContrast(pixels, 800, 600, 1.2, 1.5); */ import { applyBrightness, applyBrightnessContrast, applyColorInversionToPixel, applyContrast, applyGrayscaleToPixel, applyHslAdjustmentToPixel, applyLookupTableToRGB, applySepiaToPixel, createColorLookupTable, getGrayscaleConverter, isIdentityFactor, rgbToHsl, validateColorInversionParameters, validateColorParameters, validateFactorBounds, validateGrayscaleParameters, validateHslAdjustmentParameters, validateSepiaParameters, } from "./utils.js"; /** * Adjusts the brightness of RGBA pixel data. * * Brightness adjustment multiplies each RGB channel by the given factor. * Values are clamped to [0, 255] range. Alpha channel is preserved. * * @param {Uint8Array} pixels - Source RGBA pixel data (4 bytes per pixel) * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @param {number} factor - Brightness factor (1.0 = no change, >1.0 = brighter, <1.0 = darker) * @param {boolean} [inPlace=true] - Whether to modify the original array * @returns {{pixels: Uint8Array, width: number, height: number}} Adjusted image data * @throws {Error} If parameters are invalid * * @example * // Make image 20% brighter * const result = adjustBrightness(pixels, 800, 600, 1.2); * * // Make image 50% darker * const result = adjustBrightness(pixels, 800, 600, 0.5); * * // Create new array instead of modifying original * const result = adjustBrightness(pixels, 800, 600, 1.2, false); */ export function adjustBrightness(pixels, width, height, factor, inPlace = true) { // Validate all parameters validateColorParameters(pixels, width, height, factor); validateFactorBounds(factor, 0, 10); // Early return for identity operations if (isIdentityFactor(factor)) { return { pixels: inPlace ? pixels : new Uint8Array(pixels), width, height, }; } const output = inPlace ? pixels : new Uint8Array(pixels); // Use lookup table for maximum performance const lut = createColorLookupTable((value) => applyBrightness(value, factor)); // Process all pixels for (let i = 0; i < output.length; i += 4) { applyLookupTableToRGB(output, i, lut); } return { pixels: output, width, height, }; } /** * Adjusts the contrast of RGBA pixel data. * * Contrast adjustment centers around middle gray (128) and multiplies * the difference by the given factor. Alpha channel is preserved. * * @param {Uint8Array} pixels - Source RGBA pixel data (4 bytes per pixel) * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @param {number} factor - Contrast factor (1.0 = no change, >1.0 = more contrast, <1.0 = less contrast) * @param {boolean} [inPlace=true] - Whether to modify the original array * @returns {{pixels: Uint8Array, width: number, height: number}} Adjusted image data * @throws {Error} If parameters are invalid * * @example * // Increase contrast by 50% * const result = adjustContrast(pixels, 800, 600, 1.5); * * // Decrease contrast by 30% * const result = adjustContrast(pixels, 800, 600, 0.7); * * // Create new array instead of modifying original * const result = adjustContrast(pixels, 800, 600, 1.5, false); */ export function adjustContrast(pixels, width, height, factor, inPlace = true) { // Validate all parameters validateColorParameters(pixels, width, height, factor); validateFactorBounds(factor, 0, 10); // Early return for identity operations if (isIdentityFactor(factor)) { return { pixels: inPlace ? pixels : new Uint8Array(pixels), width, height, }; } const output = inPlace ? pixels : new Uint8Array(pixels); // Use lookup table for maximum performance const lut = createColorLookupTable((value) => applyContrast(value, factor)); // Process all pixels for (let i = 0; i < output.length; i += 4) { applyLookupTableToRGB(output, i, lut); } return { pixels: output, width, height, }; } /** * Adjusts both brightness and contrast of RGBA pixel data in a single pass. * More efficient than applying adjustments separately. * * @param {Uint8Array} pixels - Source RGBA pixel data (4 bytes per pixel) * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @param {number} brightnessFactor - Brightness factor (1.0 = no change) * @param {number} contrastFactor - Contrast factor (1.0 = no change) * @param {boolean} [inPlace=true] - Whether to modify the original array * @returns {{pixels: Uint8Array, width: number, height: number}} Adjusted image data * @throws {Error} If parameters are invalid * * @example * // Increase brightness and contrast * const result = adjustBrightnessContrast(pixels, 800, 600, 1.2, 1.3); * * // Decrease both * const result = adjustBrightnessContrast(pixels, 800, 600, 0.8, 0.9); */ export function adjustBrightnessContrast(pixels, width, height, brightnessFactor, contrastFactor, inPlace = true) { // Validate all parameters validateColorParameters(pixels, width, height, brightnessFactor); validateColorParameters(pixels, width, height, contrastFactor); validateFactorBounds(brightnessFactor, 0, 10); validateFactorBounds(contrastFactor, 0, 10); // Early return for identity operations if (isIdentityFactor(brightnessFactor) && isIdentityFactor(contrastFactor)) { return { pixels: inPlace ? pixels : new Uint8Array(pixels), width, height, }; } const output = inPlace ? pixels : new Uint8Array(pixels); // Use lookup table for maximum performance const lut = createColorLookupTable((value) => applyBrightnessContrast(value, brightnessFactor, contrastFactor)); // Process all pixels for (let i = 0; i < output.length; i += 4) { applyLookupTableToRGB(output, i, lut); } return { pixels: output, width, height, }; } /** * Gets information about a color adjustment operation without performing it. * Useful for validation and UI feedback. * * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @param {number} factor - Adjustment factor * @param {string} operation - Operation type ("brightness" or "contrast") * @returns {{ * operation: string, * factor: number, * isIdentity: boolean, * outputDimensions: {width: number, height: number}, * outputSize: number, * isValid: boolean * }} Color adjustment operation information */ export function getColorAdjustmentInfo(width, height, factor, operation) { try { // Basic validation if (!Number.isInteger(width) || width <= 0) { throw new Error("Invalid width"); } if (!Number.isInteger(height) || height <= 0) { throw new Error("Invalid height"); } if (typeof factor !== "number" || !Number.isFinite(factor)) { throw new Error("Invalid factor"); } if (operation !== "brightness" && operation !== "contrast") { throw new Error("Invalid operation"); } validateFactorBounds(factor, 0, 10); // Color adjustments never change dimensions const outputDimensions = { width, height }; const outputSize = width * height * 4; const isIdentity = isIdentityFactor(factor); return { operation, factor, isIdentity, outputDimensions, outputSize, isValid: true, }; } catch (_error) { return { operation: "brightness", factor: 1.0, isIdentity: true, outputDimensions: { width: 0, height: 0 }, outputSize: 0, isValid: false, }; } } /** * Creates a preview of color adjustment effects on a small sample. * Useful for real-time UI feedback without processing the entire image. * * @param {Uint8Array} samplePixels - Small sample of RGBA pixel data * @param {number} sampleWidth - Sample width in pixels * @param {number} sampleHeight - Sample height in pixels * @param {number} brightnessFactor - Brightness factor * @param {number} contrastFactor - Contrast factor * @returns {{pixels: Uint8Array, width: number, height: number}} Preview result */ export function createColorAdjustmentPreview( samplePixels, sampleWidth, sampleHeight, brightnessFactor, contrastFactor ) { // Validate sample size (should be small for performance) if (samplePixels.length > 64 * 64 * 4) { throw new Error("Sample too large for preview (max 64x64 pixels)"); } return adjustBrightnessContrast( samplePixels, sampleWidth, sampleHeight, brightnessFactor, contrastFactor, false // Always create new array for preview ); } /** * Analyzes the brightness distribution of an image. * Returns statistics useful for automatic adjustment suggestions. * * @param {Uint8Array} pixels - RGBA pixel data * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @returns {{ * averageBrightness: number, * minBrightness: number, * maxBrightness: number, * histogram: Uint32Array * }} Brightness analysis results */ export function analyzeBrightness(pixels, width, height) { validateColorParameters(pixels, width, height, 1.0); const histogram = new Uint32Array(256); let totalBrightness = 0; let minBrightness = 255; let maxBrightness = 0; let pixelCount = 0; // Process each pixel for (let i = 0; i < pixels.length; i += 4) { // Calculate luminance using standard weights const r = pixels[i]; const g = pixels[i + 1]; const b = pixels[i + 2]; const brightness = Math.round(0.299 * r + 0.587 * g + 0.114 * b); histogram[brightness]++; totalBrightness += brightness; minBrightness = Math.min(minBrightness, brightness); maxBrightness = Math.max(maxBrightness, brightness); pixelCount++; } return { averageBrightness: totalBrightness / pixelCount, minBrightness, maxBrightness, histogram, }; } /** * Converts RGBA pixel data to grayscale. * * Supports multiple conversion methods for different visual effects. * The luminance method provides the most perceptually accurate results. * Alpha channel is preserved in all methods. * * @param {Uint8Array} pixels - Source RGBA pixel data (4 bytes per pixel) * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @param {string} [method="luminance"] - Conversion method ("luminance", "average", "desaturate", "max", "min") * @param {boolean} [inPlace=true] - Whether to modify the original array * @returns {{pixels: Uint8Array, width: number, height: number}} Grayscale image data * @throws {Error} If parameters are invalid * * @example * // Convert to grayscale using luminance (most accurate) * const result = convertToGrayscale(pixels, 800, 600, "luminance"); * * // Convert using simple average (faster) * const result = convertToGrayscale(pixels, 800, 600, "average"); * * // Convert using desaturation * const result = convertToGrayscale(pixels, 800, 600, "desaturate"); * * // Create new array instead of modifying original * const result = convertToGrayscale(pixels, 800, 600, "luminance", false); */ export function convertToGrayscale(pixels, width, height, method = "luminance", inPlace = true) { // Validate all parameters validateGrayscaleParameters(pixels, width, height, method); const output = inPlace ? pixels : new Uint8Array(pixels); const converter = getGrayscaleConverter(method); // Process all pixels for (let i = 0; i < output.length; i += 4) { applyGrayscaleToPixel(output, i, converter); } return { pixels: output, width, height, }; } /** * Gets information about a grayscale conversion operation without performing it. * Useful for validation and UI feedback. * * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @param {string} method - Conversion method * @returns {{ * method: string, * isLossless: boolean, * outputDimensions: {width: number, height: number}, * outputSize: number, * description: string, * isValid: boolean * }} Grayscale conversion operation information */ export function getGrayscaleInfo(width, height, method = "luminance") { try { // Basic validation if (!Number.isInteger(width) || width <= 0) { throw new Error("Invalid width"); } if (!Number.isInteger(height) || height <= 0) { throw new Error("Invalid height"); } const validMethods = ["luminance", "average", "desaturate", "max", "min"]; if (!validMethods.includes(method)) { throw new Error("Invalid method"); } // Grayscale conversion never changes dimensions const outputDimensions = { width, height }; const outputSize = width * height * 4; // Method descriptions /** @type {Record<string, string>} */ const descriptions = { luminance: "ITU-R BT.709 standard luminance weights (most perceptually accurate)", average: "Simple RGB average (fastest, less accurate)", desaturate: "Average of min and max RGB values (preserves contrast)", max: "Maximum RGB value (preserves highlights)", min: "Minimum RGB value (preserves shadows)", }; return { method, isLossless: false, // Color information is lost outputDimensions, outputSize, description: descriptions[method] || "Unknown method", isValid: true, }; } catch (_error) { return { method: "luminance", isLossless: false, outputDimensions: { width: 0, height: 0 }, outputSize: 0, description: "", isValid: false, }; } } /** * Creates a preview of grayscale conversion effects on a small sample. * Useful for real-time UI feedback without processing the entire image. * * @param {Uint8Array} samplePixels - Small sample of RGBA pixel data * @param {number} sampleWidth - Sample width in pixels * @param {number} sampleHeight - Sample height in pixels * @param {string} method - Conversion method * @returns {{pixels: Uint8Array, width: number, height: number}} Preview result */ export function createGrayscalePreview(samplePixels, sampleWidth, sampleHeight, method = "luminance") { // Validate sample size (should be small for performance) if (samplePixels.length > 64 * 64 * 4) { throw new Error("Sample too large for preview (max 64x64 pixels)"); } return convertToGrayscale( samplePixels, sampleWidth, sampleHeight, method, false // Always create new array for preview ); } /** * Compares different grayscale conversion methods on the same image data. * Returns results from all methods for visual comparison. * * @param {Uint8Array} pixels - Source RGBA pixel data * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @returns {{ * luminance: Uint8Array, * average: Uint8Array, * desaturate: Uint8Array, * max: Uint8Array, * min: Uint8Array * }} Results from all conversion methods */ export function compareGrayscaleMethods(pixels, width, height) { validateGrayscaleParameters(pixels, width, height, "luminance"); const methods = ["luminance", "average", "desaturate", "max", "min"]; /** @type {Record<string, Uint8Array>} */ const results = {}; for (const method of methods) { const result = convertToGrayscale(pixels, width, height, method, false); results[method] = result.pixels; } return /** @type {{luminance: Uint8Array, average: Uint8Array, desaturate: Uint8Array, max: Uint8Array, min: Uint8Array}} */ ( results ); } /** * Inverts the colors of RGBA pixel data. * * Color inversion creates a photographic negative effect by subtracting each * RGB channel value from 255. Alpha channel is preserved unchanged. * * @param {Uint8Array} pixels - Source RGBA pixel data (4 bytes per pixel) * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @param {boolean} [inPlace=true] - Whether to modify the original array * @returns {{pixels: Uint8Array, width: number, height: number}} Inverted image data * @throws {Error} If parameters are invalid * * @example * // Invert colors to create negative effect * const result = applyColorInversion(pixels, 800, 600); * * // Create new array instead of modifying original * const result = applyColorInversion(pixels, 800, 600, false); * * // Double inversion should restore original * const inverted = applyColorInversion(pixels, 800, 600, false); * const restored = applyColorInversion(inverted.pixels, 800, 600, false); */ export function applyColorInversion(pixels, width, height, inPlace = true) { // Validate all parameters validateColorInversionParameters(pixels, width, height); const output = inPlace ? pixels : new Uint8Array(pixels); // Process all pixels - simple and fast operation for (let i = 0; i < output.length; i += 4) { applyColorInversionToPixel(output, i); } return { pixels: output, width, height, }; } /** * Gets information about a color inversion operation without performing it. * Useful for validation and UI feedback. * * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @returns {{ * operation: string, * isLossless: boolean, * isReversible: boolean, * outputDimensions: {width: number, height: number}, * outputSize: number, * description: string, * isValid: boolean * }} Color inversion operation information */ export function getColorInversionInfo(width, height) { try { // Basic validation if (!Number.isInteger(width) || width <= 0) { throw new Error("Invalid width"); } if (!Number.isInteger(height) || height <= 0) { throw new Error("Invalid height"); } // Color inversion never changes dimensions const outputDimensions = { width, height }; const outputSize = width * height * 4; return { operation: "inversion", isLossless: true, // No information is lost isReversible: true, // invert(invert(x)) = x outputDimensions, outputSize, description: "Photographic negative effect - subtracts each RGB value from 255", isValid: true, }; } catch (_error) { return { operation: "inversion", isLossless: false, isReversible: false, outputDimensions: { width: 0, height: 0 }, outputSize: 0, description: "", isValid: false, }; } } /** * Creates a preview of color inversion effects on a small sample. * Useful for real-time UI feedback without processing the entire image. * * @param {Uint8Array} samplePixels - Small sample of RGBA pixel data * @param {number} sampleWidth - Sample width in pixels * @param {number} sampleHeight - Sample height in pixels * @returns {{pixels: Uint8Array, width: number, height: number}} Preview result */ export function createColorInversionPreview(samplePixels, sampleWidth, sampleHeight) { // Validate sample size (should be small for performance) if (samplePixels.length > 64 * 64 * 4) { throw new Error("Sample too large for preview (max 64x64 pixels)"); } return applyColorInversion( samplePixels, sampleWidth, sampleHeight, false // Always create new array for preview ); } /** * Applies sepia tone effect to RGBA pixel data. * * Sepia tone creates a vintage brown-tinted photographic effect using a * standard 3x3 transformation matrix. Alpha channel is preserved unchanged. * * @param {Uint8Array} pixels - Source RGBA pixel data (4 bytes per pixel) * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @param {boolean} [inPlace=true] - Whether to modify the original array * @returns {{pixels: Uint8Array, width: number, height: number}} Sepia-toned image data * @throws {Error} If parameters are invalid * * @example * // Apply sepia tone for vintage effect * const result = applySepiaEffect(pixels, 800, 600); * * // Create new array instead of modifying original * const result = applySepiaEffect(pixels, 800, 600, false); * * // Sepia preserves luminance while adding brown tint * const sepia = applySepiaEffect(colorPixels, 400, 300); */ export function applySepiaEffect(pixels, width, height, inPlace = true) { // Validate all parameters validateSepiaParameters(pixels, width, height); const output = inPlace ? pixels : new Uint8Array(pixels); // Process all pixels using sepia transformation matrix for (let i = 0; i < output.length; i += 4) { applySepiaToPixel(output, i); } return { pixels: output, width, height, }; } /** * Gets information about a sepia tone operation without performing it. * Useful for validation and UI feedback. * * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @returns {{ * operation: string, * isLossless: boolean, * isReversible: boolean, * outputDimensions: {width: number, height: number}, * outputSize: number, * description: string, * isValid: boolean * }} Sepia tone operation information */ export function getSepiaInfo(width, height) { try { // Basic validation if (!Number.isInteger(width) || width <= 0) { throw new Error("Invalid width"); } if (!Number.isInteger(height) || height <= 0) { throw new Error("Invalid height"); } // Sepia tone never changes dimensions const outputDimensions = { width, height }; const outputSize = width * height * 4; return { operation: "sepia", isLossless: false, // Color information is transformed isReversible: false, // Matrix transformation is not reversible outputDimensions, outputSize, description: "Vintage brown-tinted photographic effect using standard sepia matrix", isValid: true, }; } catch (_error) { return { operation: "sepia", isLossless: false, isReversible: false, outputDimensions: { width: 0, height: 0 }, outputSize: 0, description: "", isValid: false, }; } } /** * Creates a preview of sepia tone effects on a small sample. * Useful for real-time UI feedback without processing the entire image. * * @param {Uint8Array} samplePixels - Small sample of RGBA pixel data * @param {number} sampleWidth - Sample width in pixels * @param {number} sampleHeight - Sample height in pixels * @returns {{pixels: Uint8Array, width: number, height: number}} Preview result */ export function createSepiaPreview(samplePixels, sampleWidth, sampleHeight) { // Validate sample size (should be small for performance) if (samplePixels.length > 64 * 64 * 4) { throw new Error("Sample too large for preview (max 64x64 pixels)"); } return applySepiaEffect( samplePixels, sampleWidth, sampleHeight, false // Always create new array for preview ); } /** * Adjusts the saturation of RGBA pixel data using HSL color space. * * Saturation adjustment multiplies the saturation component in HSL space * while preserving hue and lightness. Alpha channel is preserved unchanged. * * @param {Uint8Array} pixels - Source RGBA pixel data (4 bytes per pixel) * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @param {number} saturationFactor - Saturation multiplier [0.0 to 2.0] * @param {boolean} [inPlace=true] - Whether to modify the original array * @returns {{pixels: Uint8Array, width: number, height: number}} Adjusted image data * @throws {Error} If parameters are invalid * * @example * // Increase saturation by 50% * const result = adjustSaturation(pixels, 800, 600, 1.5); * * // Desaturate image (50% less saturated) * const result = adjustSaturation(pixels, 800, 600, 0.5); * * // Complete desaturation (grayscale) * const result = adjustSaturation(pixels, 800, 600, 0.0); */ export function adjustSaturation(pixels, width, height, saturationFactor, inPlace = true) { // Validate all parameters validateHslAdjustmentParameters(pixels, width, height, 0, saturationFactor); const output = inPlace ? pixels : new Uint8Array(pixels); // Process all pixels using HSL conversion for (let i = 0; i < output.length; i += 4) { applyHslAdjustmentToPixel(output, i, 0, saturationFactor); } return { pixels: output, width, height, }; } /** * Adjusts the hue of RGBA pixel data using HSL color space. * * Hue adjustment shifts the hue component in HSL space while preserving * saturation and lightness. Alpha channel is preserved unchanged. * * @param {Uint8Array} pixels - Source RGBA pixel data (4 bytes per pixel) * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @param {number} hueShift - Hue shift in degrees [-360 to 360] * @param {boolean} [inPlace=true] - Whether to modify the original array * @returns {{pixels: Uint8Array, width: number, height: number}} Adjusted image data * @throws {Error} If parameters are invalid * * @example * // Shift hue by 90 degrees (red -> green, green -> blue, etc.) * const result = adjustHue(pixels, 800, 600, 90); * * // Shift hue by -120 degrees (reverse color wheel) * const result = adjustHue(pixels, 800, 600, -120); * * // Full color inversion via hue (180 degrees) * const result = adjustHue(pixels, 800, 600, 180); */ export function adjustHue(pixels, width, height, hueShift, inPlace = true) { // Validate all parameters validateHslAdjustmentParameters(pixels, width, height, hueShift, 1.0); const output = inPlace ? pixels : new Uint8Array(pixels); // Process all pixels using HSL conversion for (let i = 0; i < output.length; i += 4) { applyHslAdjustmentToPixel(output, i, hueShift, 1.0); } return { pixels: output, width, height, }; } /** * Adjusts both hue and saturation of RGBA pixel data using HSL color space. * * Combined HSL adjustment for efficient processing when both hue and * saturation need to be modified. Alpha channel is preserved unchanged. * * @param {Uint8Array} pixels - Source RGBA pixel data (4 bytes per pixel) * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @param {number} hueShift - Hue shift in degrees [-360 to 360] * @param {number} saturationFactor - Saturation multiplier [0.0 to 2.0] * @param {boolean} [inPlace=true] - Whether to modify the original array * @returns {{pixels: Uint8Array, width: number, height: number}} Adjusted image data * @throws {Error} If parameters are invalid * * @example * // Shift hue and increase saturation * const result = adjustHueSaturation(pixels, 800, 600, 45, 1.3); * * // Create vintage effect with hue shift and desaturation * const result = adjustHueSaturation(pixels, 800, 600, 15, 0.7); */ export function adjustHueSaturation(pixels, width, height, hueShift, saturationFactor, inPlace = true) { // Validate all parameters validateHslAdjustmentParameters(pixels, width, height, hueShift, saturationFactor); const output = inPlace ? pixels : new Uint8Array(pixels); // Process all pixels using HSL conversion for (let i = 0; i < output.length; i += 4) { applyHslAdjustmentToPixel(output, i, hueShift, saturationFactor); } return { pixels: output, width, height, }; } /** * Gets information about HSL adjustment operations without performing them. * Useful for validation and UI feedback. * * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @param {number} hueShift - Hue shift in degrees [-360 to 360] * @param {number} saturationFactor - Saturation multiplier [0.0 to 2.0] * @returns {{ * operation: string, * isLossless: boolean, * isReversible: boolean, * outputDimensions: {width: number, height: number}, * outputSize: number, * description: string, * isValid: boolean * }} HSL adjustment operation information */ export function getHslAdjustmentInfo(width, height, hueShift, saturationFactor) { try { // Basic validation if (!Number.isInteger(width) || width <= 0) { throw new Error("Invalid width"); } if (!Number.isInteger(height) || height <= 0) { throw new Error("Invalid height"); } // Validate HSL parameters validateHslAdjustmentParameters(new Uint8Array(4), 1, 1, hueShift, saturationFactor); // HSL adjustments never change dimensions const outputDimensions = { width, height }; const outputSize = width * height * 4; // Determine operation type and reversibility const isHueAdjustment = hueShift !== 0; const isSaturationAdjustment = saturationFactor !== 1.0; let operation = "hsl"; let description = ""; let isReversible = true; if (isHueAdjustment && isSaturationAdjustment) { operation = "hue-saturation"; description = `Hue shift by ${hueShift}° and saturation adjustment by ${saturationFactor}x`; isReversible = saturationFactor !== 0; // Can't reverse complete desaturation } else if (isHueAdjustment) { operation = "hue"; description = `Hue shift by ${hueShift}° on color wheel`; isReversible = true; // Hue shifts are always reversible } else if (isSaturationAdjustment) { operation = "saturation"; description = `Saturation adjustment by ${saturationFactor}x`; isReversible = saturationFactor !== 0; // Can't reverse complete desaturation } else { operation = "identity"; description = "No HSL adjustments (identity operation)"; isReversible = true; } return { operation, isLossless: false, // HSL conversion involves rounding isReversible, outputDimensions, outputSize, description, isValid: true, }; } catch (_error) { return { operation: "hsl", isLossless: false, isReversible: false, outputDimensions: { width: 0, height: 0 }, outputSize: 0, description: "", isValid: false, }; } } /** * Creates a preview of HSL adjustment effects on a small sample. * Useful for real-time UI feedback without processing the entire image. * * @param {Uint8Array} samplePixels - Small sample of RGBA pixel data * @param {number} sampleWidth - Sample width in pixels * @param {number} sampleHeight - Sample height in pixels * @param {number} hueShift - Hue shift in degrees [-360 to 360] * @param {number} saturationFactor - Saturation multiplier [0.0 to 2.0] * @returns {{pixels: Uint8Array, width: number, height: number}} Preview result */ export function createHslAdjustmentPreview(samplePixels, sampleWidth, sampleHeight, hueShift, saturationFactor) { // Validate sample size (should be small for performance) if (samplePixels.length > 64 * 64 * 4) { throw new Error("Sample too large for preview (max 64x64 pixels)"); } return adjustHueSaturation( samplePixels, sampleWidth, sampleHeight, hueShift, saturationFactor, false // Always create new array for preview ); } /** * Converts RGB pixel data to HSL representation for analysis. * Useful for color analysis and histogram generation. * * @param {Uint8Array} pixels - Source RGBA pixel data (4 bytes per pixel) * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @returns {{ * hues: Float32Array, * saturations: Float32Array, * lightnesses: Float32Array, * averageHue: number, * averageSaturation: number, * averageLightness: number * }} HSL analysis data */ export function analyzeHslDistribution(pixels, width, height) { validateColorParameters(pixels, width, height, 1.0); const pixelCount = width * height; const hues = new Float32Array(pixelCount); const saturations = new Float32Array(pixelCount); const lightnesses = new Float32Array(pixelCount); let totalHue = 0; let totalSaturation = 0; let totalLightness = 0; let validHueCount = 0; // For averaging (exclude achromatic pixels) for (let i = 0, pixelIndex = 0; i < pixels.length; i += 4, pixelIndex++) { const r = pixels[i]; const g = pixels[i + 1]; const b = pixels[i + 2]; const [h, s, l] = rgbToHsl(r, g, b); hues[pixelIndex] = h; saturations[pixelIndex] = s; lightnesses[pixelIndex] = l; if (s > 0) { // Only count chromatic pixels for hue average totalHue += h; validHueCount++; } totalSaturation += s; totalLightness += l; } return { hues, saturations, lightnesses, averageHue: validHueCount > 0 ? totalHue / validHueCount : 0, averageSaturation: totalSaturation / pixelCount, averageLightness: totalLightness / pixelCount, }; }