You can edit the below JavaScript code to customize the image tool.
function processImage(originalImg, brushRadius = 8, lightAngleDeg = 135, lightIntensity = 0.3) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Ensure original image has valid dimensions
if (originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
console.error("Original image not loaded or has no dimensions.");
// Create a dummy canvas to avoid errors if originalImg is broken
canvas.width = 1;
canvas.height = 1;
return canvas;
}
canvas.width = originalImg.width;
canvas.height = originalImg.height;
// Create a source canvas to get pixel data from the original image
// Using willReadFrequently for potential performance optimization if getImageData is called often
const sourceCanvas = document.createElement('canvas');
sourceCanvas.width = originalImg.width;
sourceCanvas.height = originalImg.height;
const sourceCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
sourceCtx.drawImage(originalImg, 0, 0);
// Light direction calculation
const lightAngleRad = parseFloat(lightAngleDeg) * Math.PI / 180;
// Standard angle: 0 is right, 90 is up. Canvas Y is inverted.
const lightX = Math.cos(lightAngleRad);
const lightY = -Math.sin(lightAngleRad);
// Normalize LightIntensity to be between 0 and 1
const normalizedLightIntensity = Math.max(0, Math.min(1, parseFloat(lightIntensity)));
// Map normalizedLightIntensity to practical ranges for effects
const colorModStrength = normalizedLightIntensity * 0.45; // Modulates color by +-0% up to +-45%
const displacementBase = parseFloat(brushRadius) * normalizedLightIntensity * 0.3; // Max displacement 30% of radius
const R = parseFloat(brushRadius);
const jitterRange = R * 0.5; // Jitter for stroke placement to make it more organic
const step = Math.max(1, Math.floor(R * 0.65)); // Step for grid, ensures good overlap for coverage
// Loop over a grid covering the canvas
// Extend loop slightly beyond canvas dimensions to ensure edges are well covered by strokes
for (let y = -R; y < canvas.height + R; y += step) {
for (let x = -R; x < canvas.width + R; x += step) {
// Add jitter to stroke center for a more natural, less grid-like appearance
const jitterX = (Math.random() - 0.5) * jitterRange;
const jitterY = (Math.random() - 0.5) * jitterRange;
const centerX = x + jitterX;
const centerY = y + jitterY;
// Get color from original image at the (possibly jittered) stroke center
// Clamp coordinates to be within the source image bounds
const imgX = Math.max(0, Math.min(originalImg.width - 1, Math.floor(centerX)));
const imgY = Math.max(0, Math.min(originalImg.height - 1, Math.floor(centerY)));
const pixelData = sourceCtx.getImageData(imgX, imgY, 1, 1).data;
const r = pixelData[0];
const g = pixelData[1];
const b = pixelData[2];
const a = pixelData[3];
// Skip fully or mostly transparent areas to avoid painting strokes for nothing
if (a < 32) { // Alpha threshold (0-255), e.g., ignore if less than ~12% opaque
continue;
}
// 1. Draw the shadow part of the stroke
const sr = Math.max(0, Math.round(r * (1 - colorModStrength)));
const sg = Math.max(0, Math.round(g * (1 - colorModStrength)));
const sb = Math.max(0, Math.round(b * (1 - colorModStrength)));
ctx.fillStyle = `rgb(${sr},${sg},${sb})`;
ctx.beginPath();
// Shadow is shifted away from the light source relative to the main stroke
ctx.arc(centerX - lightX * displacementBase,
centerY - lightY * displacementBase,
R, 0, 2 * Math.PI);
ctx.fill();
// 2. Draw the main paint color part of the stroke
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.beginPath();
ctx.arc(centerX, centerY, R, 0, 2 * Math.PI);
ctx.fill();
// 3. Draw the highlight part of the stroke
// Highlights can be slightly more intense
const hr = Math.min(255, Math.round(r * (1 + colorModStrength * 1.1)));
const hg = Math.min(255, Math.round(g * (1 + colorModStrength * 1.1)));
const hb = Math.min(255, Math.round(b * (1 + colorModStrength * 1.1)));
ctx.fillStyle = `rgb(${hr},${hg},${hb})`;
ctx.beginPath();
// Highlight is smaller and shifted towards the light source
// Use a slightly smaller displacement and radius for the highlight
ctx.arc(centerX + lightX * (displacementBase * 0.7),
centerY + lightY * (displacementBase * 0.7),
R * 0.75, // Highlight radius is smaller
0, 2 * Math.PI);
ctx.fill();
}
}
return canvas;
}
Free Image Tool Creator
Can't find the image tool you're looking for? Create one based on your own needs now!
The Image Impasto Filter Effect Tool allows users to apply an artistic impasto effect to images, simulating the appearance of thick paint strokes. This tool can enhance images by adding texture and depth, making them appear more dynamic and expressive. Ideal for artists, graphic designers, or anyone looking to create unique visual styles, the tool permits customization through parameters such as brush radius, light angle, and light intensity, enabling a wide range of creative outcomes.