You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
desaturationLevel = 0.3, // Number: 0.0 to 1.0. 0 for no change, 1 for full grayscale.
contrastLevel = 1.2, // Number: 0.0 to N. 1.0 for no change.
shadowsTintColor = "#003366", // String: Hex color for shadows (e.g., deep blue/teal).
highlightsTintColor = "#FFA500",// String: Hex color for highlights (e.g., warm orange/yellow).
splitToneIntensity = 0.35 // Number: 0.0 to 1.0. Intensity of the split toning effect.
) {
// Helper function to parse hex color string to an {r, g, b} object
// Returns null if hex is invalid
function hexToRgb(hex) {
if (!hex || typeof hex !== 'string' || !hex.startsWith('#')) {
return null;
}
let hexValue = hex.slice(1);
if (hexValue.length === 3) { // Expand shorthand form (e.g., #03F to #0033FF)
hexValue = hexValue[0] + hexValue[0] + hexValue[1] + hexValue[1] + hexValue[2] + hexValue[2];
}
if (hexValue.length !== 6 || !/^[0-9A-Fa-f]{6}$/.test(hexValue)) {
return null; // Invalid hex value
}
const bigint = parseInt(hexValue, 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return { r, g, b };
}
// Ensure the image is loaded
if (!originalImg.complete || originalImg.naturalWidth === 0) {
// If the image src is set and it's still loading, wait for it.
// If src is not set or invalid, this might not resolve or reject as expected.
// However, an `Image` object passed as a parameter usually implies it's intended to be valid.
if (originalImg.src) {
try {
await new Promise((resolve, reject) => {
originalImg.onload = resolve;
originalImg.onerror = () => reject(new Error("Image failed to load. Check the image source or network."));
});
} catch (error) {
console.error(error.message);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 300; errorCanvas.height = 60;
const errCtx = errorCanvas.getContext('2d');
errCtx.font = "12px Arial";
errCtx.fillStyle = "red";
errCtx.fillText(error.message, 10, 25);
errCtx.fillText("Cannot process the image.", 10, 45);
return errorCanvas;
}
}
}
// After attempting to load, check if dimensions are valid
if (originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
console.error("Image has zero dimensions or could not be loaded.");
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 300; errorCanvas.height = 60;
const errCtx = errorCanvas.getContext('2d');
errCtx.font = "12px Arial";
errCtx.fillStyle = "red";
errCtx.fillText("Error: Image has invalid dimensions or failed to load.", 10, 25);
errCtx.fillText("Cannot process the image.", 10, 45);
return errorCanvas;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = originalImg.naturalWidth;
canvas.height = originalImg.naturalHeight;
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const shadowRgb = hexToRgb(shadowsTintColor);
const highlightRgb = hexToRgb(highlightsTintColor);
const epsilon = 1e-6; // For floating point comparisons
for (let i = 0; i < data.length; i += 4) {
let r_val = data[i];
let g_val = data[i+1];
let b_val = data[i+2];
// 1. Desaturation
// Only apply if desaturationLevel is meaningfully greater than 0
if (desaturationLevel > epsilon) {
const gray = 0.299 * r_val + 0.587 * g_val + 0.114 * b_val; // Luma-based grayscale
r_val = r_val * (1 - desaturationLevel) + gray * desaturationLevel;
g_val = g_val * (1 - desaturationLevel) + gray * desaturationLevel;
b_val = b_val * (1 - desaturationLevel) + gray * desaturationLevel;
}
// 2. Contrast
// Only apply if contrastLevel is meaningfully different from 1.0
if (Math.abs(contrastLevel - 1.0) > epsilon) {
// Apply contrast: f(v) = (v/255 - 0.5) * factor + 0.5
// then scale back to 0-255
r_val = ((r_val / 255 - 0.5) * contrastLevel + 0.5) * 255;
g_val = ((g_val / 255 - 0.5) * contrastLevel + 0.5) * 255;
b_val = ((b_val / 255 - 0.5) * contrastLevel + 0.5) * 255;
}
// Clamp values after desaturation & contrast, before split toning
r_val = Math.max(0, Math.min(255, r_val));
g_val = Math.max(0, Math.min(255, g_val));
b_val = Math.max(0, Math.min(255, b_val));
// 3. Split Toning
// Only apply if intensity is meaningfully greater than 0
if (splitToneIntensity > epsilon) {
// Calculate luminance from the current (desaturated, contrasted) color
const currentLuminance = (0.299 * r_val + 0.587 * g_val + 0.114 * b_val) / 255; // Normalized 0-1
// Apply shadow tint
if (shadowRgb) {
const shadowMixFactor = (1 - currentLuminance) * splitToneIntensity;
r_val = r_val * (1 - shadowMixFactor) + shadowRgb.r * shadowMixFactor;
g_val = g_val * (1 - shadowMixFactor) + shadowRgb.g * shadowMixFactor;
b_val = b_val * (1 - shadowMixFactor) + shadowRgb.b * shadowMixFactor;
}
// Apply highlight tint (to the already shadow-tinted color for cumulative effect)
if (highlightRgb) {
const highlightMixFactor = currentLuminance * splitToneIntensity;
r_val = r_val * (1 - highlightMixFactor) + highlightRgb.r * highlightMixFactor;
g_val = g_val * (1 - highlightMixFactor) + highlightRgb.g * highlightMixFactor;
b_val = b_val * (1 - highlightMixFactor) + highlightRgb.b * highlightMixFactor;
}
}
// Final clamp and assignment
data[i] = Math.max(0, Math.min(255, r_val));
data[i+1] = Math.max(0, Math.min(255, g_val));
data[i+2] = Math.max(0, Math.min(255, b_val));
// data[i+3] is alpha, remains unchanged
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes