You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg,
padding = 10,
backgroundColor = '#f0f0f0',
labelColor = 'black',
labelFont = '16px Arial',
hueLabelText = 'Hue',
saturationLabelText = 'Saturation',
valueLabelText = 'Value'
) {
const w = originalImg.width;
const h = originalImg.height;
// Guard against unloaded or zero-size images
if (w === 0 || h === 0) {
const emptyCanvas = document.createElement('canvas');
// Return a small canvas, optionally with a message
emptyCanvas.width = Math.max(1, w * 3 + padding * 2); // Try to match expected width if only one dim is 0
emptyCanvas.height = Math.max(1, h + (labelFont.match(/(\d+)px/) ? parseInt(labelFont.match(/(\d+)px/)[1], 10) : 16) + 14);
if (emptyCanvas.width < 100 && (hueLabelText || saturationLabelText || valueLabelText )) emptyCanvas.width = 100; // Min width for a message
if (emptyCanvas.height < 20 && (hueLabelText || saturationLabelText || valueLabelText )) emptyCanvas.height = 20; // Min height for a message
const emptyCtx = emptyCanvas.getContext('2d');
emptyCtx.fillStyle = backgroundColor;
emptyCtx.fillRect(0, 0, emptyCanvas.width, emptyCanvas.height);
if (hueLabelText || saturationLabelText || valueLabelText) { // Only show message if labels were expected
emptyCtx.fillStyle = labelColor;
emptyCtx.font = labelFont.replace(/(\d+)px/, `${Math.min(12, parseInt(labelFont.match(/(\d+)px/)?.[1] || 12))}px`); // Smaller font for message
emptyCtx.textAlign = "center";
emptyCtx.textBaseline = "middle";
emptyCtx.fillText("Empty or invalid image", emptyCanvas.width / 2, emptyCanvas.height / 2);
}
return emptyCanvas;
}
// Helper function: RGB to HSV
// r_in, g_in, b_in are 0-255.
// Returns h (0-360 degrees), s (0-1), v (0-1).
function rgbToHsv(r_in, g_in, b_in) {
const r_norm = r_in / 255;
const g_norm = g_in / 255;
const b_norm = b_in / 255;
const max = Math.max(r_norm, g_norm, b_norm);
const min = Math.min(r_norm, g_norm, b_norm);
let h_val;
const v_val = max;
const d = max - min;
const s_val = max === 0 ? 0 : d / max;
if (d === 0) {
h_val = 0; // achromatic, hue is undefined but often set to 0
} else {
switch (max) {
case r_norm: h_val = (g_norm - b_norm) / d + (g_norm < b_norm ? 6 : 0); break;
case g_norm: h_val = (b_norm - r_norm) / d + 2; break;
case b_norm: h_val = (r_norm - g_norm) / d + 4; break;
}
h_val /= 6; // h_val is now in [0, 1]
}
return { h: h_val * 360, s: s_val, v: v_val };
}
// Helper function: HSV to RGB
// h_in (0-360 degrees), s_in (0-1), v_in (0-1).
// Returns r, g, b (0-255).
function hsvToRgb(h_in, s_in, v_in) {
let r_out, g_out, b_out;
// Normalize h_in to be in [0, 360)
let h_normalized = h_in % 360;
if (h_normalized < 0) h_normalized += 360;
const h_sector = h_normalized / 60; // h_sector is in [0, 6)
const i = Math.floor(h_sector);
const f = h_sector - i; // fractional part
const p = v_in * (1 - s_in);
const q = v_in * (1 - f * s_in);
const t = v_in * (1 - (1 - f) * s_in);
switch (i) {
case 0: r_out = v_in; g_out = t; b_out = p; break;
case 1: r_out = q; g_out = v_in; b_out = p; break;
case 2: r_out = p; g_out = v_in; b_out = t; break;
case 3: r_out = p; g_out = q; b_out = v_in; break;
case 4: r_out = t; g_out = p; b_out = v_in; break;
case 5: r_out = v_in; g_out = p; b_out = q; break;
default: r_out = v_in; g_out = v_in; b_out = v_in; break; // Should not happen with h_normalized
}
return {
r: Math.round(r_out * 255),
g: Math.round(g_out * 255),
b: Math.round(b_out * 255)
};
}
// 1. Get pixel data from original image
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = w;
offscreenCanvas.height = h;
const offscreenCtx = offscreenCanvas.getContext('2d', { willReadFrequently: true });
offscreenCtx.drawImage(originalImg, 0, 0, w, h);
const imageData = offscreenCtx.getImageData(0, 0, w, h);
const data = imageData.data;
// 2. Create ImageData for Hue, Saturation, Value visualizations
const hueImageData = offscreenCtx.createImageData(w, h);
const satImageData = offscreenCtx.createImageData(w, h);
const valImageData = offscreenCtx.createImageData(w, h);
// 3. Process each pixel
for (let idx = 0; idx < data.length; idx += 4) {
const r_px = data[idx];
const g_px = data[idx + 1];
const b_px = data[idx + 2];
const alpha_px = data[idx + 3];
const { h: hue, s: saturation, v: value } = rgbToHsv(r_px, g_px, b_px);
// Hue visualization: color derived from H, with S=1, V=1. Achromatic pixels (S=0) display as gray based on V.
const hueColor = saturation === 0 ? hsvToRgb(0, 0, value) : hsvToRgb(hue, 1, 1);
hueImageData.data[idx] = hueColor.r;
hueImageData.data[idx + 1] = hueColor.g;
hueImageData.data[idx + 2] = hueColor.b;
hueImageData.data[idx + 3] = alpha_px;
// Saturation visualization (grayscale: S mapped to 0-255)
const satGray = Math.round(saturation * 255);
satImageData.data[idx] = satGray;
satImageData.data[idx + 1] = satGray;
satImageData.data[idx + 2] = satGray;
satImageData.data[idx + 3] = alpha_px;
// Value visualization (grayscale: V mapped to 0-255)
const valGray = Math.round(value * 255);
valImageData.data[idx] = valGray;
valImageData.data[idx + 1] = valGray;
valImageData.data[idx + 2] = valGray;
valImageData.data[idx + 3] = alpha_px;
}
// 4. Prepare for output canvas
const fontSizeMatch = labelFont.match(/(\d+)px/);
const fontSize = fontSizeMatch ? parseInt(fontSizeMatch[1], 10) : 16;
const actualLabelHeight = Math.max(20, fontSize + 14); // Min 20px height for labels area
const actualPadding = Math.max(0, padding); // Ensure padding is not negative
const totalWidth = w * 3 + actualPadding * 2;
const totalHeight = h + actualLabelHeight;
const outputCanvas = document.createElement('canvas');
outputCanvas.width = totalWidth;
outputCanvas.height = totalHeight;
const outputCtx = outputCanvas.getContext('2d');
// Fill background
outputCtx.fillStyle = backgroundColor;
outputCtx.fillRect(0, 0, totalWidth, totalHeight);
// 5. Draw the component images (Hue, Saturation, Value)
// Draw to temporary canvases first to use putImageData, then drawImage to final canvas
const tempHueCanvas = document.createElement('canvas');
tempHueCanvas.width = w; tempHueCanvas.height = h;
tempHueCanvas.getContext('2d').putImageData(hueImageData, 0, 0);
const tempSatCanvas = document.createElement('canvas');
tempSatCanvas.width = w; tempSatCanvas.height = h;
tempSatCanvas.getContext('2d').putImageData(satImageData, 0, 0);
const tempValCanvas = document.createElement('canvas');
tempValCanvas.width = w; tempValCanvas.height = h;
tempValCanvas.getContext('2d').putImageData(valImageData, 0, 0);
const imageY = actualLabelHeight;
outputCtx.drawImage(tempHueCanvas, 0, imageY);
outputCtx.drawImage(tempSatCanvas, w + actualPadding, imageY);
outputCtx.drawImage(tempValCanvas, w * 2 + actualPadding * 2, imageY);
// 6. Add labels
outputCtx.fillStyle = labelColor;
outputCtx.font = labelFont;
outputCtx.textAlign = 'center';
outputCtx.textBaseline = 'middle'; // Vertically center text in the label area
const labelTextY = actualLabelHeight / 2;
outputCtx.fillText(hueLabelText, w / 2, labelTextY);
outputCtx.fillText(saturationLabelText, w + actualPadding + w / 2, labelTextY);
outputCtx.fillText(valueLabelText, w * 2 + actualPadding * 2 + w / 2, labelTextY);
return outputCanvas;
}
Apply Changes