You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg) {
const width = originalImg.width;
const height = originalImg.height;
// Handle cases where the image might be empty or invalid
if (width === 0 || height === 0) {
console.warn("Original image has zero width or height.");
const placeholderCanvas = document.createElement('canvas');
// Create a small placeholder rather than a 0x0 or excessively large canvas
placeholderCanvas.width = (width > 0 ? width : 100) * 3;
placeholderCanvas.height = (height > 0 ? height : 100);
const pCtx = placeholderCanvas.getContext('2d');
pCtx.fillStyle = '#eee';
pCtx.fillRect(0,0,placeholderCanvas.width, placeholderCanvas.height);
pCtx.fillStyle = 'black';
pCtx.font = "16px Arial";
pCtx.textAlign = "center";
pCtx.fillText("Empty or invalid image", placeholderCanvas.width / 2, placeholderCanvas.height / 2);
return placeholderCanvas;
}
// Create a temporary canvas to get image data
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
try {
tempCtx.drawImage(originalImg, 0, 0, width, height);
} catch (e) {
console.error("Error drawing original image to temporary canvas:", e);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = width * 3 > 0 ? width * 3 : 300;
errorCanvas.height = height > 0 ? height : 100;
const errCtx = errorCanvas.getContext('2d');
errCtx.font = "16px Arial"; errCtx.fillStyle = "red"; errCtx.textAlign = "center";
errCtx.fillText("Error: Could not draw image.", errorCanvas.width/2, errorCanvas.height/2);
return errorCanvas;
}
let imageData;
try {
imageData = tempCtx.getImageData(0, 0, width, height);
} catch (e) {
console.error("Error getting image data (possibly tainted canvas):", e);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = width * 3 > 0 ? width * 3 : 450; // Adjusted for potentially longer message
errorCanvas.height = height > 0 ? height : 100;
const errCtx = errorCanvas.getContext('2d');
errCtx.font = "16px Arial"; errCtx.fillStyle = "red"; errCtx.textAlign = "center";
let msg1 = "Error: Could not read pixel data.";
let msg2 = "Image might be cross-origin or format not supported for reading.";
errCtx.fillText(msg1, errorCanvas.width/2, errorCanvas.height/2 - 10);
errCtx.fillText(msg2, errorCanvas.width/2, errorCanvas.height/2 + 10);
return errorCanvas;
}
const data = imageData.data;
// Create the output canvas
const outputCanvas = document.createElement('canvas');
// Check for excessively large canvas dimensions
// Max typical browser canvas dimension limit is 32767, some use 16384 or less.
const MAX_CANVAS_SIDE = 16384;
if (width * 3 > MAX_CANVAS_SIDE || height > MAX_CANVAS_SIDE || width > MAX_CANVAS_SIDE) {
console.warn(`Output canvas dimensions (${width*3}x${height}) may exceed limits or perform poorly.`);
// We'll proceed, browser might cap it or throw error.
}
try {
outputCanvas.width = width * 3;
outputCanvas.height = height;
} catch (e) {
console.error("Error setting output canvas dimensions (possibly too large):", e);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 300; errorCanvas.height = 100;
const errCtx = errorCanvas.getContext('2d');
errCtx.font = "16px Arial"; errCtx.fillStyle = "red"; errCtx.textAlign = "center";
errCtx.fillText("Error: Output canvas too large.", errorCanvas.width/2, errorCanvas.height/2);
return errorCanvas;
}
const outCtx = outputCanvas.getContext('2d');
const hueImageData = outCtx.createImageData(width, height);
const satImageData = outCtx.createImageData(width, height);
const intImageData = outCtx.createImageData(width, height);
const hueData = hueImageData.data;
const satData = satImageData.data;
const intData = intImageData.data;
const PI = Math.PI;
const EPSILON_INTENSITY = 1e-9; // Threshold for intensity to be considered zero (black)
const EPSILON_SATURATION = 1e-5; // Threshold for saturation to be considered zero (achromatic)
const EPSILON_DENOMINATOR = 1e-9; // Threshold for H denominator squared
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i+1];
const b = data[i+2];
const alpha = data[i+3];
const r_norm = r / 255;
const g_norm = g / 255;
const b_norm = b / 255;
const intensity = (r_norm + g_norm + b_norm) / 3;
let saturation;
if (intensity < EPSILON_INTENSITY) {
saturation = 0; // Black or near-black
} else {
const min_rgb = Math.min(r_norm, g_norm, b_norm);
saturation = 1 - (min_rgb / intensity);
}
// Clamp saturation due to potential floating point inaccuracies for achromatic colors
if (saturation < EPSILON_SATURATION) {
saturation = 0;
}
let hue = 0; // Default hue for achromatic colors (saturation is 0)
if (saturation > EPSILON_SATURATION) { // Only calculate hue for chromatic colors
// Using R-G, R-B, G-B for intermediate calculations
const val_R_G = r_norm - g_norm;
const val_R_B = r_norm - b_norm;
const val_G_B = g_norm - b_norm; // Not explicitly used in numerator in this form
// Numerator: 0.5 * ((R-G) + (R-B)) = R - 0.5*(G+B)
const numerator_H = 0.5 * (val_R_G + val_R_B);
// Denominator_sq: (R-G)^2 + (R-B)*(G-B)
const denominator_H_sq = (val_R_G * val_R_G) + (val_R_B * val_G_B);
if (denominator_H_sq > EPSILON_DENOMINATOR) {
const denominator_H = Math.sqrt(denominator_H_sq);
let acos_arg = numerator_H / denominator_H;
// Clamp argument of acos to [-1, 1] to avoid NaN from precision errors
acos_arg = Math.max(-1, Math.min(1, acos_arg));
let angle_rad = Math.acos(acos_arg); // Result in [0, PI]
if (b_norm > g_norm) {
hue = 2 * PI - angle_rad; // Adjust to [0, 2*PI]
} else {
hue = angle_rad;
}
}
// If denominator_H_sq is effectively zero, hue remains 0.
// This case (R=G=B) should be caught by saturation being zero.
}
// Scale H, S, I to [0, 255] for display
// Hue is in [0, 2*PI] radians, scale to [0, 255]
const h_disp = Math.round((hue / (2 * PI)) * 255);
// Saturation is in [0, 1], scale to [0, 255]
const s_disp = Math.round(saturation * 255);
// Intensity is in [0, 1], scale to [0, 255]
const i_disp = Math.round(intensity * 255);
// Set pixel data for each component image
// Index `i` is the start of RGBA components for the current pixel
hueData[i] = h_disp; hueData[i+1] = h_disp; hueData[i+2] = h_disp; hueData[i+3] = alpha;
satData[i] = s_disp; satData[i+1] = s_disp; satData[i+2] = s_disp; satData[i+3] = alpha;
intData[i] = i_disp; intData[i+1] = i_disp; intData[i+2] = i_disp; intData[i+3] = alpha;
}
// Put the H, S, I ImageData onto the output canvas
outCtx.putImageData(hueImageData, 0, 0);
outCtx.putImageData(satImageData, width, 0);
outCtx.putImageData(intImageData, width * 2, 0);
// Add labels (H, S, I) to each panel
let fontSize = 16;
if (width > 0) { // make font size somewhat responsive to image width
fontSize = Math.min(24, Math.max(10, Math.floor(width / 25)));
}
const labelBoxWidth = fontSize + 14;
const labelBoxHeight = fontSize + 8;
const margin = Math.max(5, Math.floor(fontSize/3)); // Margin from panel edge to label box
outCtx.font = `bold ${fontSize}px Arial`;
outCtx.textAlign = 'center';
outCtx.textBaseline = 'middle';
const panelPositions = [0, width, width * 2];
const panelLabels = ['H', 'S', 'I'];
panelLabels.forEach((label, index) => {
const panelXoffset = panelPositions[index];
const boxX = panelXoffset + margin;
const boxY = margin;
// Draw background for text for better visibility
outCtx.fillStyle = 'rgba(0, 0, 0, 0.65)';
outCtx.fillRect(boxX, boxY, labelBoxWidth, labelBoxHeight);
// Draw text
outCtx.fillStyle = 'white';
outCtx.fillText(label, boxX + labelBoxWidth / 2, boxY + labelBoxHeight / 2);
});
return outputCanvas;
}
Apply Changes