You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, filterName = "None") {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Ensure the original image's dimensions are used.
// It's assumed that originalImg is a loaded HTMLImageElement.
// If naturalWidth/Height are 0, the canvas will be 0x0, which is generally problematic.
// Robust error handling for unloaded images is outside typical scope of a processing function.
canvas.width = originalImg.naturalWidth;
canvas.height = originalImg.naturalHeight;
// Default filter definition structure.
// Properties are defined here to ensure they exist when merging with specific filter specs.
const defaultFilterSpec = {
cssFilter: 'none',
overlay: null, // Example: { color: "rgba(R,G,B,A)", mode: "blend-mode" }
vignette: null // Example: { type: "dark"|"light"|"custom", intensity: 0-1 (for dark/light), innerRadiusFactor:0-1, outerRadiusFactor:0-1, color1:"rgba(...)", color2:"rgba(...)", blendMode:"..." (for custom) }
};
// Filter definitions
// These are approximations of popular Instagram-style filters.
// - cssFilter: Uses CanvasRenderingContext2D.filter property, similar to CSS filters.
// - overlay: Applies a solid color layer over the image using a specified blend mode.
// - vignette: Adds a radial gradient vignette effect.
const filters = {
"None": {}, // Will effectively use defaultFilterSpec
"Clarendon": {
cssFilter: "brightness(1.15) contrast(1.1) saturate(1.3)"
},
"Gingham": {
cssFilter: "brightness(1.05) saturate(0.7) contrast(0.9) sepia(0.05)",
overlay: { color: "rgba(230, 220, 225, 0.15)", mode: "lighten" }
},
"Moon": {
cssFilter: "grayscale(1) contrast(1.3) brightness(1.05)"
},
"Lark": {
cssFilter: "brightness(1.1) contrast(0.9) saturate(1.2) sepia(0.05)"
},
"Reyes": {
cssFilter: "sepia(0.3) brightness(1.1) contrast(0.85) saturate(0.75)",
overlay: { color: "rgba(240, 230, 200, 0.1)", mode: "lighten"}
},
"Juno": {
cssFilter: "contrast(1.2) saturate(1.4) brightness(1.05) hue-rotate(-8deg) sepia(0.1)"
},
"Slumber": {
cssFilter: "saturate(0.6) brightness(1.05) contrast(0.9) sepia(0.1)",
overlay: { color: "rgba(200, 200, 220, 0.1)", mode: "soft-light" }
},
"Crema": {
cssFilter: "sepia(0.25) contrast(1.1) saturate(0.8) brightness(1.15) hue-rotate(-4deg)"
},
"Ludwig": {
cssFilter: "brightness(1.05) contrast(1.05) saturate(0.7) sepia(0.15)"
},
"Aden": {
cssFilter: "hue-rotate(-20deg) contrast(0.9) saturate(0.85) brightness(1.2) sepia(0.1)"
},
"Perpetua": {
cssFilter: "contrast(1.1) brightness(1.1) saturate(1.25) hue-rotate(5deg) sepia(0.05)"
},
"Inkwell": {
cssFilter: "grayscale(1) contrast(1.2) brightness(0.95) sepia(0.05)"
},
"Willow": {
cssFilter: "grayscale(0.7) contrast(0.9) brightness(1.1) sepia(0.05) hue-rotate(320deg)",
vignette: { type: "dark", intensity: 0.2, innerRadiusFactor: 0.5, outerRadiusFactor: 0.9 }
},
"Lo-Fi": {
cssFilter: "saturate(1.4) contrast(1.3) hue-rotate(-5deg) sepia(0.05)"
},
"X-Pro II": {
cssFilter: "contrast(1.3) saturate(1.25) sepia(0.3) hue-rotate(-7deg) brightness(0.95)",
vignette: { type: "dark", intensity: 0.5, innerRadiusFactor: 0.3, outerRadiusFactor: 0.75 }
},
"Nashville": {
cssFilter: "sepia(0.25) contrast(1.2) brightness(1.05) saturate(1.2) hue-rotate(-3deg)",
overlay: { color: "rgba(240, 200, 180, 0.08)", mode: "screen" }
},
"1977": {
cssFilter: "contrast(1.1) brightness(1.1) saturate(1.2) sepia(0.35) hue-rotate(-7deg)",
overlay: { color: "rgba(230, 100, 80, 0.1)", mode: "screen" } // Reddish overlay
},
"Earlybird": {
cssFilter: "sepia(0.35) contrast(1.1) brightness(0.95) saturate(1.1) hue-rotate(-5deg)",
vignette: { type: "dark", intensity: 0.4, innerRadiusFactor: 0.4, outerRadiusFactor: 0.8 }
},
"Brannan": {
cssFilter: "contrast(1.35) sepia(0.45) hue-rotate(-2deg) saturate(0.9)", // sepia gives strong tint
overlay: { color: "rgba(180, 190, 170, 0.1)", mode: "lighten"} // Greyish-green tint
},
"Sutro": {
cssFilter: "brightness(0.75) contrast(1.25) saturate(0.7) sepia(0.35) hue-rotate(15deg) grayscale(0.1)",
vignette: { type: "dark", intensity: 0.6, innerRadiusFactor: 0.3, outerRadiusFactor: 0.7 }
},
"Toaster": { // Aims for a "scorched" look with warm, dark vignette
cssFilter: "sepia(0.3) saturate(1.5) contrast(1.2) hue-rotate(-15deg) brightness(0.9)",
vignette: { type: "custom", color1: "rgba(128,70,0,0.15)", color2: "rgba(50,10,0,0.4)", blendMode: 'multiply', innerRadiusFactor: 0.3, outerRadiusFactor: 0.75 }
},
"Walden": {
cssFilter: "brightness(1.15) hue-rotate(-12deg) sepia(0.25) saturate(1.5)",
overlay: { color: "rgba(255, 255, 180, 0.1)", mode: "screen" } // Light yellow overlay
},
"Hefe": {
cssFilter: "contrast(1.25) saturate(1.35) sepia(0.2) hue-rotate(-7deg) brightness(1.05)",
vignette: { type: "dark", intensity: 0.3, innerRadiusFactor: 0.4, outerRadiusFactor: 0.8 }
},
"Valencia": {
cssFilter: "contrast(1.1) brightness(1.1) sepia(0.1) saturate(1.1) hue-rotate(-3deg)",
overlay: { color: "rgba(240, 220, 180, 0.05)", mode: "lighten" } // Very subtle warm overlay
},
"Kelvin": {
cssFilter: "sepia(0.45) saturate(1.7) contrast(1.35) brightness(1.15) hue-rotate(-15deg)",
overlay: { color: "rgba(255,180,0,0.1)", mode:"overlay" } // Strong warm/orange overlay
}
};
// Get the definition for the selected filter, or "None" if not found.
const selectedFilterSpec = filters[filterName] || filters["None"];
// Merge the selected filter's specifics with the default structure.
// This ensures `filterDef` object has all keys (cssFilter, overlay, vignette).
const filterDef = { ...defaultFilterSpec, ...selectedFilterSpec };
// Step 1: Apply the base CSS-like filter
if (filterDef.cssFilter && filterDef.cssFilter !== 'none') {
ctx.filter = filterDef.cssFilter;
}
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// Reset ctx.filter before drawing overlay or vignette, so they aren't affected by the CSS filter.
ctx.filter = 'none';
// Step 2: Apply color overlay if defined
if (filterDef.overlay && filterDef.overlay.color && filterDef.overlay.mode) {
ctx.globalCompositeOperation = filterDef.overlay.mode;
ctx.fillStyle = filterDef.overlay.color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'source-over'; // Reset blend mode to default
}
// Step 3: Apply vignette if defined
if (filterDef.vignette) {
const v = filterDef.vignette;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
// Radii are based on the shorter dimension of the canvas to maintain a more circular vignette.
const shorterDim = Math.min(canvas.width, canvas.height);
const innerR = shorterDim * (v.innerRadiusFactor !== undefined ? v.innerRadiusFactor : 0.3);
const outerR = shorterDim * (v.outerRadiusFactor !== undefined ? v.outerRadiusFactor : 0.9);
const gradient = ctx.createRadialGradient(centerX, centerY, innerR, centerX, centerY, outerR);
let vignetteBlendMode = 'multiply'; // Default for dark vignette
let vignetteColorStop1 = 'rgba(0,0,0,0)'; // Default: transparent center
let vignetteIntensity = v.intensity !== undefined ? v.intensity : 0.5;
let vignetteColorStop2 = `rgba(0,0,0,${vignetteIntensity})`; // Default: dark edge
if (v.type === 'light') {
vignetteBlendMode = 'screen'; // Or 'lighten' could be used
vignetteColorStop1 = 'rgba(255,255,255,0)'; // Transparent center
vignetteColorStop2 = `rgba(255,255,255,${vignetteIntensity})`; // Light edge
} else if (v.type === 'custom' && v.color1 && v.color2) {
// Custom vignette allows specific inner/outer colors and blend mode
vignetteBlendMode = v.blendMode || 'multiply';
vignetteColorStop1 = v.color1;
vignetteColorStop2 = v.color2;
}
gradient.addColorStop(0, vignetteColorStop1);
gradient.addColorStop(1, vignetteColorStop2);
ctx.globalCompositeOperation = vignetteBlendMode;
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'source-over'; // Reset blend mode
}
return canvas;
}
Apply Changes