You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg,
halationStrength = 0.5, // Range: 0 to 1. Strength of the halation effect.
halationRadius = 10, // Range: 0 to 50 (pixels). Blur radius for halation.
coolingAmount = 0.15, // Range: 0 to 1. Intensity of the cooling (blue shift) effect.
saturationFactor = 0.9, // Range: 0 to 2. 1 is original saturation, 0 is grayscale.
contrastFactor = 1.1, // Range: 0 to 2. 1 is original contrast.
grainAmount = 15, // Range: 0 to 50. Intensity of film grain.
tealShadowsAmount = 0.1, // Range: 0 to 0.5. Amount of teal tint in shadows.
orangeHighlightsAmount = 0.1 // Range: 0 to 0.5. Amount of orange tint in highlights.
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
// Handle cases where image might not be loaded or has no dimensions
if (canvas.width === 0 || canvas.height === 0) {
console.error("Image has zero width or height.");
return canvas;
}
// Draw original image
ctx.drawImage(originalImg, 0, 0);
// Get ImageData for pixel manipulation
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const len = data.length;
// Constants for luminance calculation
const lumR = 0.299;
const lumG = 0.587;
const lumB = 0.114;
for (let i = 0; i < len; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// 1. Cooling Effect (Simulating tungsten balance in daylight)
if (coolingAmount > 0) {
r *= (1 - coolingAmount * 0.5); // Reduce red
g *= (1 - coolingAmount * 0.25); // Slightly reduce green
b *= (1 + coolingAmount); // Increase blue
}
// 2. Contrast
if (contrastFactor !== 1.0) {
// Adjust contrast: f * (c - 128) + 128
r = contrastFactor * (r - 128) + 128;
g = contrastFactor * (g - 128) + 128;
b = contrastFactor * (b - 128) + 128;
}
// 3. Saturation
if (saturationFactor !== 1.0) {
const gray = r * lumR + g * lumG + b * lumB;
r = gray + saturationFactor * (r - gray);
g = gray + saturationFactor * (g - gray);
b = gray + saturationFactor * (b - gray);
}
// Clamp intermediate values to [0, 255]
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
// 4. Teal Shadows / Orange Highlights (Split Toning)
const currentLuminance = r * lumR + g * lumG + b * lumB;
// Teal shadows
const shadowThreshold = 85; // Pixels with luminance below this are considered shadows
if (tealShadowsAmount > 0 && currentLuminance < shadowThreshold) {
// Calculate mix factor, stronger for darker shadows
const shadowMix = Math.pow((shadowThreshold - currentLuminance) / shadowThreshold, 1.5);
b += 60 * tealShadowsAmount * shadowMix; // Add blue for teal
g += 30 * tealShadowsAmount * shadowMix; // Add some green for teal
r -= 30 * tealShadowsAmount * shadowMix; // Reduce red complementary to cyan/teal
}
// Orange highlights
const highlightThresholdSplitTone = 170; // Pixels with luminance above this are highlights
if (orangeHighlightsAmount > 0 && currentLuminance > highlightThresholdSplitTone) {
// Calculate mix factor, stronger for brighter highlights
const highlightMix = Math.pow((currentLuminance - highlightThresholdSplitTone) / (255 - highlightThresholdSplitTone), 1.5);
r += 60 * orangeHighlightsAmount * highlightMix; // Add red for orange
g += 30 * orangeHighlightsAmount * highlightMix; // Add some green for orange
b -= 30 * orangeHighlightsAmount * highlightMix; // Reduce blue complementary to orange
}
// Clamp again after split toning
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
// 5. Film Grain
if (grainAmount > 0) {
const grain = (Math.random() - 0.5) * grainAmount;
r += grain;
g += grain;
b += grain;
}
// Final Clamp for pixel data
data[i] = Math.max(0, Math.min(255, r));
data[i + 1] = Math.max(0, Math.min(255, g));
data[i + 2] = Math.max(0, Math.min(255, b));
}
// Put modified image data back to the main canvas
ctx.putImageData(imageData, 0, 0);
// 6. Halation
if (halationStrength > 0 && halationRadius > 0) {
const halationCanvas = document.createElement('canvas');
halationCanvas.width = canvas.width;
halationCanvas.height = canvas.height;
const hCtx = halationCanvas.getContext('2d', { willReadFrequently: true });
// Draw the current image (color-graded, grained) onto halation canvas to extract highlights
hCtx.drawImage(canvas, 0, 0);
const hImageData = hCtx.getImageData(0, 0, halationCanvas.width, halationCanvas.height);
const hData = hImageData.data;
const hLen = hData.length;
const halationColorR = 255;
const halationColorG = 70;
const halationColorB = 50; // Reddish-orange halation color
const halationHighlightThreshold = 200; // Brightness threshold for glow Ccinestill has prominent red halation
for (let i = 0; i < hLen; i += 4) {
const rPx = hData[i]; // Red component of the current pixel on main canvas
const gPx = hData[i+1];
const bPx = hData[i+2];
const brightness = (rPx + gPx + bPx) / 3;
let intensityFactor = 0;
// Trigger halation for overall bright areas or very red areas
if (brightness > halationHighlightThreshold || rPx > 220) {
let exceedAmount = (brightness - halationHighlightThreshold);
if (rPx > 220) { // Boost intensity for very red pixels
exceedAmount += (rPx - 220) * 0.5;
}
intensityFactor = Math.min(1, Math.max(0, exceedAmount) / (255 - halationHighlightThreshold + 30));
}
if (intensityFactor > 0) {
hData[i] = halationColorR;
hData[i + 1] = halationColorG;
hData[i + 2] = halationColorB;
hData[i + 3] = intensityFactor * 255; // Alpha is proportional to highlight intensity
} else {
hData[i + 3] = 0; // Fully transparent if not a highlight
}
}
hCtx.putImageData(hImageData, 0, 0); // Put the generated halation mask onto halationCanvas
// Blur the halation mask
// Create a temporary canvas for blurring because ctx.filter applies to future drawings
const tempBlurCanvas = document.createElement('canvas');
tempBlurCanvas.width = halationCanvas.width;
tempBlurCanvas.height = halationCanvas.height;
const tempBlurCtx = tempBlurCanvas.getContext('2d');
tempBlurCtx.filter = `blur(${halationRadius}px)`;
tempBlurCtx.drawImage(halationCanvas, 0, 0); // Draw halationCanvas onto tempBlurCanvas, applying blur
// Composite blurred halation layer onto the main canvas
ctx.save();
ctx.globalCompositeOperation = 'lighter'; // Use 'lighter' (additive) or 'screen' for glow
ctx.globalAlpha = halationStrength; // Control overall halation intensity
ctx.drawImage(tempBlurCanvas, 0, 0); // Draw the blurred halation map
ctx.restore(); // Restore canvas context state
}
return canvas;
}
Apply Changes