You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, brightness = 10, contrast = 130, highlightColorStr = "#FF00FF", highlightIntensity = 0.5, shadowColorStr = "#00FFFF", shadowIntensity = 0.5, saturationBoost = 20) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true }); // Optimized for frequent getImageData
// Ensure the image is loaded before trying to get its dimensions or draw it
// This is more robust if originalImg might not be fully loaded yet,
// though problem states it's an Image object, implying it might be.
// However, naturalWidth/Height are best checks.
const imgIsLoaded = originalImg.complete && originalImg.naturalWidth !== 0;
if (!imgIsLoaded && originalImg.src) {
// If not loaded, create a promise to wait for it
await new Promise((resolve, reject) => {
originalImg.onload = resolve;
originalImg.onerror = reject;
// If originalImg.src is already set and it failed, onerror might have fired.
// If src not set, this won't help. The caller should provide a loaded image.
});
}
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// Helper function to parse hex color string to RGB object
function hexToRgb(hex) {
let r = 0, g = 0, b = 0;
hex = hex.replace(/^#/, ''); // Remove # if present
if (hex.length === 3) { // #RGB shorthand
r = parseInt(hex[0] + hex[0], 16);
g = parseInt(hex[1] + hex[1], 16);
b = parseInt(hex[2] + hex[2], 16);
} else if (hex.length === 6) { // #RRGGBB
r = parseInt(hex.substring(0, 2), 16);
g = parseInt(hex.substring(2, 4), 16);
b = parseInt(hex.substring(4, 6), 16);
}
return { r, g, b };
}
const parsedHighlightColor = hexToRgb(highlightColorStr);
const parsedShadowColor = hexToRgb(shadowColorStr);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// Contrast: 100 means no change. 0 means total gray. 200 means double contrast.
// (contrast / 100.0) gives a factor like 1.3 for contrast = 130.
const contrastFactor = contrast / 100.0;
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// 1. Apply Brightness
// brightness is -100 to 100.
r += brightness;
g += brightness;
b += brightness;
// 2. Apply Contrast
// Formula: NewColor = ((OldColor/255 - 0.5) * ContrastFactor + 0.5) * 255
// Or simpler: Center around 127.5, scale, then shift back.
r = ((r - 127.5) * contrastFactor) + 127.5;
g = ((g - 127.5) * contrastFactor) + 127.5;
b = ((b - 127.5) * contrastFactor) + 127.5;
// Clamp after brightness/contrast before further processing
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
// Store adjusted RGB for luminance calculation and tinting
let adjR = r;
let adjG = g;
let adjB = b;
// 3. Calculate Luminance (using Rec. 709 coefficients for perceived brightness)
const lum = 0.2126 * adjR + 0.7152 * adjG + 0.0722 * adjB;
// 4. Apply Split Toning (Neon Colors)
let currentR = adjR;
let currentG = adjG;
let currentB = adjB;
// Determine mix factor for highlights or shadows
// highlightIntensity and shadowIntensity are 0 to 1
if (lum > 128) { // Apply highlight tint to brighter pixels
// blendFactor goes from 0 (at lum=128) to 1 (at lum=255)
const blendFactor = (lum - 128) / 127.0; // Normalize to 0-1 range
const mix = blendFactor * highlightIntensity;
currentR = adjR * (1 - mix) + parsedHighlightColor.r * mix;
currentG = adjG * (1 - mix) + parsedHighlightColor.g * mix;
currentB = adjB * (1 - mix) + parsedHighlightColor.b * mix;
} else { // Apply shadow tint to darker pixels
// blendFactor goes from 0 (at lum=128) to 1 (at lum=0)
const blendFactor = (128 - lum) / 128.0; // Normalize to 0-1 range
const mix = blendFactor * shadowIntensity;
currentR = adjR * (1 - mix) + parsedShadowColor.r * mix;
currentG = adjG * (1 - mix) + parsedShadowColor.g * mix;
currentB = adjB * (1 - mix) + parsedShadowColor.b * mix;
}
// 5. Saturation Boost
// saturationBoost is 0 to 100 (0 = no change, 100 = significant boost like double)
if (saturationBoost !== 0) {
// satBoostFactor: 1.0 for 0 boost, 2.0 for 100 boost.
const satBoostFactor = 1.0 + (saturationBoost / 100.0);
// Calculate grayscale value of the current (tinted) pixel using Rec. 709
const gray = currentR * 0.2126 + currentG * 0.7152 + currentB * 0.0722;
// Interpolate towards/away from gray
currentR = gray + (currentR - gray) * satBoostFactor;
currentG = gray + (currentG - gray) * satBoostFactor;
currentB = gray + (currentB - gray) * satBoostFactor;
}
// Clamp final RGB values to [0, 255]
data[i] = Math.max(0, Math.min(255, Math.round(currentR)));
data[i + 1] = Math.max(0, Math.min(255, Math.round(currentG)));
data[i + 2] = Math.max(0, Math.min(255, Math.round(currentB)));
// Alpha (data[i + 3]) remains unchanged
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes