Please bookmark this page to avoid losing your image tool!

Image HCL Color Showcase

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
function processImage(originalImg, numSamplesXStr = "5", numSamplesYStr = "5", swatchSizeStr = "40", fontSizeStr = "10") {
    // Parameter parsing and validation
    // Ensure parameters are numbers with valid defaults and constraints
    // Guideline 2: Parameters (except originalImg) can be string or number.
    // We'll convert them to numbers for internal use.
    const numSamplesX = Math.max(1, parseInt(String(numSamplesXStr), 10) || 5);
    const numSamplesY = Math.max(1, parseInt(String(numSamplesYStr), 10) || 5);
    const swatchSize = Math.max(10, parseInt(String(swatchSizeStr), 10) || 40);
    const fontSize = Math.max(8, parseInt(String(fontSizeStr), 10) || 10);

    // Color conversion helper functions: RGB -> XYZ -> Lab -> LCH (HCL)

    // sRGB (0-1 range) to Linear RGB (0-1 range)
    function srgbToLinear(c) {
        // Clamp input to 0-1 range to handle potential floating point inaccuracies
        c = Math.max(0, Math.min(1, c)); 
        if (c <= 0.04045) {
            return c / 12.92;
        } else {
            return Math.pow((c + 0.055) / 1.055, 2.4);
        }
    }

    // RGB (0-255 range) to XYZ (D65 illuminant)
    function rgbToXyz(r, g, b) {
        const r_norm = r / 255;
        const g_norm = g / 255;
        const b_norm = b / 255;

        const r_lin = srgbToLinear(r_norm);
        const g_lin = srgbToLinear(g_norm);
        const b_lin = srgbToLinear(b_norm);

        // sRGB to XYZ transformation matrix (D65 illuminant)
        // Coefficients from http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html
        const x = r_lin * 0.4124564 + g_lin * 0.3575761 + b_lin * 0.1804375;
        const y = r_lin * 0.2126729 + g_lin * 0.7151522 + b_lin * 0.0721750;
        const z = r_lin * 0.0193339 + g_lin * 0.1191920 + b_lin * 0.9503041;
        
        return [x, y, z];
    }

    // Constants for XYZ to Lab conversion (D65 reference white)
    const D65_XN = 0.95047;
    const D65_YN = 1.00000;
    const D65_ZN = 1.08883;
    
    // Epsilon and Kappa for CIELAB formula
    const XYZ_LAB_EPSILON = 216 / 24389;  // (6/29)^3
    const XYZ_LAB_KAPPA = 24389 / 27;    // (29/3)^3, used in the f(t) function variant (kappa*t+16)/116

    // The f(t) function for CIELAB conversion
    function xyz_to_lab_term_func(t) {
        if (t > XYZ_LAB_EPSILON) {
            return Math.cbrt(t); // t^(1/3)
        } else {
            return (XYZ_LAB_KAPPA * t + 16) / 116;
        }
    }

    // XYZ to CIELAB
    function xyzToLab(x, y, z) {
        const xr = x / D65_XN;
        const yr = y / D65_YN;
        const zr = z / D65_ZN;

        const fx = xyz_to_lab_term_func(xr);
        const fy = xyz_to_lab_term_func(yr);
        const fz = xyz_to_lab_term_func(zr);

        const l_star = (116 * fy) - 16;
        const a_star = 500 * (fx - fy);
        const b_star = 200 * (fy - fz);

        return [l_star, a_star, b_star];
    }

    // CIELAB to LCH (HCL: L=Lightness, C=Chroma, H=Hue)
    function labToLch(l_star, a_star, b_star) {
        const c_star = Math.sqrt(a_star * a_star + b_star * b_star); // Chroma
        let h_star_deg = 0; // Hue in degrees

        // Hue is undefined if Chroma is 0 (achromatic colors like white, gray, black)
        // We conventionally set it to 0 in such cases.
        if (c_star !== 0) { 
             const h_star_rad = Math.atan2(b_star, a_star); // Hue in radians
             h_star_deg = h_star_rad * (180 / Math.PI);
             if (h_star_deg < 0) { // Ensure hue is in [0, 360) range
                 h_star_deg += 360;
             }
        }
        // L_hcl (Lightness) is l_star from CIELAB
        return [l_star, c_star, h_star_deg]; 
    }

    // Create a source canvas to get image pixel data
    const srcCanvas = document.createElement('canvas');
    // Use willReadFrequently for potential performance optimization if supported
    const srcCtx = srcCanvas.getContext('2d', { willReadFrequently: true }); 
    
    const imgWidth = originalImg.naturalWidth || originalImg.width;
    const imgHeight = originalImg.naturalHeight || originalImg.height;

    // Handle cases where image might not have loaded or has invalid dimensions
    if (!imgWidth || !imgHeight || typeof imgWidth !== 'number' || typeof imgHeight !== 'number' || imgWidth <= 0 || imgHeight <= 0) {
        console.error("Image HCL Color Showcase: Image has invalid dimensions.", originalImg);
        const errCanvas = document.createElement('canvas');
        errCanvas.width = 350; errCanvas.height = 60;
        const errCtx = errCanvas.getContext('2d');
        errCtx.fillStyle = "rgba(255,230,230,1)"; 
        errCtx.fillRect(0,0,errCanvas.width, errCanvas.height);
        errCtx.strokeStyle = "red";
        errCtx.strokeRect(0.5,0.5,errCanvas.width-1, errCanvas.height-1);
        errCtx.fillStyle = "#A00000"; 
        errCtx.font = `12px Arial`;
        errCtx.fillText("Error: Image has invalid or zero dimensions.", 10, 25);
        errCtx.fillText("Please provide a valid loaded image.", 10, 45);
        return errCanvas;
    }
    srcCanvas.width = imgWidth;
    srcCanvas.height = imgHeight;
    
    try {
        srcCtx.drawImage(originalImg, 0, 0);
    } catch (e) {
        console.error("Image HCL Color Showcase: Error drawing image to source canvas.", e);
         const errCanvas = document.createElement('canvas');
        errCanvas.width = 300; errCanvas.height = 50;
        const errCtx = errCanvas.getContext('2d');
        errCtx.fillStyle = "rgba(255,230,230,1)"; 
        errCtx.fillRect(0,0,errCanvas.width, errCanvas.height);
        errCtx.strokeStyle = "red";
        errCtx.strokeRect(0.5,0.5,errCanvas.width-1, errCanvas.height-1);
        errCtx.fillStyle = "#A00000"; 
        errCtx.font = `12px Arial`;
        errCtx.fillText("Error drawing image. It might be invalid.", 10, 25);
        return errCanvas;
    }
    

    // Prepare output canvas
    const outCanvas = document.createElement('canvas');
    const outCtx = outCanvas.getContext('2d');

    const textLineHeight = fontSize + 4; // Height for one line of text including spacing
    const totalTextBlockHeight = 3 * textLineHeight; // For L, C, H values
    const interElementPadding = 5; // Padding around elements within a cell (e.g., swatch to text)

    // Calculate dimensions for each cell (swatch + text)
    const cellContentWidth = swatchSize;
    const cellContentHeight = swatchSize + interElementPadding + totalTextBlockHeight;

    const cellTotalWidth = cellContentWidth + 2 * interElementPadding; // Add padding for left/right of cell
    const cellTotalHeight = cellContentHeight + 2 * interElementPadding; // Add padding for top/bottom of cell

    outCanvas.width = numSamplesX * cellTotalWidth;
    outCanvas.height = numSamplesY * cellTotalHeight;

    // Fill background of the output canvas
    outCtx.fillStyle = '#FFFFFF'; // White background
    outCtx.fillRect(0, 0, outCanvas.width, outCanvas.height);

    outCtx.font = `${fontSize}px Arial`;
    outCtx.textBaseline = 'top'; // Makes y-coordinate calculations for text simpler

    for (let j = 0; j < numSamplesY; j++) { // Iterate over rows of samples
        for (let i = 0; i < numSamplesX; i++) { // Iterate over columns of samples
            // Calculate sample coordinates in the original image (center of grid cells)
            const imgSampleX = (i + 0.5) * imgWidth / numSamplesX;
            const imgSampleY = (j + 0.5) * imgHeight / numSamplesY;
            
            // Ensure pixel coordinates are within image bounds
            const imgPixelX = Math.max(0, Math.min(imgWidth - 1, Math.floor(imgSampleX)));
            const imgPixelY = Math.max(0, Math.min(imgHeight - 1, Math.floor(imgSampleY)));
            
            let R = 0, G = 0, B = 0; // Default to black if pixel data read fails
            try {
                 // Get pixel data [R, G, B, A] for the sampled point
                 const pixelData = srcCtx.getImageData(imgPixelX, imgPixelY, 1, 1).data;
                 R = pixelData[0]; G = pixelData[1]; B = pixelData[2];
            } catch (e) {
                // This error can occur if the canvas is tainted (e.g., cross-origin image without CORS)
                // The problem statement implies originalImg is a usable JS Image object, so this is defensive.
                console.error("Image HCL Color Showcase: Error reading pixel data (getImageData). Canvas might be tainted.", e);
                // Allow loop to continue, R,G,B will be 0,0,0 (black) for this sample
            }

            // Convert sampled RGB color to HCL
            const [x_xyz, y_xyz, z_xyz] = rgbToXyz(R, G, B);
            const [l_lab, a_lab, b_lab] = xyzToLab(x_xyz, y_xyz, z_xyz);
            const [L_hcl, C_hcl, H_hcl] = labToLch(l_lab, a_lab, b_lab);

            // Calculate current cell's top-left (base) X and Y on the output canvas
            const currentCellBaseX = i * cellTotalWidth;
            const currentCellBaseY = j * cellTotalHeight;

            // Draw the color swatch
            const swatchX = currentCellBaseX + interElementPadding;
            const swatchY = currentCellBaseY + interElementPadding;
            outCtx.fillStyle = `rgb(${R}, ${G}, ${B})`;
            outCtx.fillRect(swatchX, swatchY, swatchSize, swatchSize);
            
            // Draw a border around the swatch for better visibility
            outCtx.strokeStyle = '#888888'; // Medium gray border
            outCtx.lineWidth = 1;
            // Stroke slightly inside the swatch rect to avoid issues if cells are packed adjacently
            outCtx.strokeRect(swatchX + 0.5, swatchY + 0.5, swatchSize -1, swatchSize-1);

            // Draw HCL text values below the swatch
            outCtx.fillStyle = '#000000'; // Black text
            outCtx.textAlign = 'center'; // Center text horizontally under the swatch
            
            const textCenterX = swatchX + swatchSize / 2; // Horizontal center for text
            let textCurrentY = swatchY + swatchSize + interElementPadding; // Initial Y for the first line of text

            outCtx.fillText(`L: ${L_hcl.toFixed(1)}`, textCenterX, textCurrentY);
            textCurrentY += textLineHeight; // Move to next line
            outCtx.fillText(`C: ${C_hcl.toFixed(1)}`, textCenterX, textCurrentY);
            textCurrentY += textLineHeight; // Move to next line
            outCtx.fillText(`H: ${H_hcl.toFixed(1)}°`, textCenterX, textCurrentY);
        }
    }
    return outCanvas;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image HCL Color Showcase tool allows users to extract and visualize color data from an image by generating a grid of color swatches. Each swatch displays the corresponding lightness, chroma, and hue values in the HCL color space. This tool is useful for graphic designers, artists, and developers who want to analyze color palettes, create color studies, or gain insights into the color composition of images. Users can specify the number of swatches, their size, and the font size for text labels, making it flexible for various applications.

Leave a Reply

Your email address will not be published. Required fields are marked *