You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, brushSize_param = 10, strokeDensity_param = 1.0, strokeOpacity_param = 0.7, jitter_param = 0.5) {
// Validate and clamp parameters
const brushSize = Math.max(3, Number(brushSize_param)); // Min brush size 3 for performance & aesthetics
const strokeDensity = Math.max(0.01, Number(strokeDensity_param)); // Min density to ensure some strokes
const strokeOpacity = Math.max(0.01, Math.min(1, Number(strokeOpacity_param)));
const jitter = Math.max(0, Math.min(1, Number(jitter_param)));
const outputCanvas = document.createElement('canvas');
const outputCtx = outputCanvas.getContext('2d');
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
if (width === 0 || height === 0) {
// Handle unloaded or zero-size image case
outputCanvas.width = 1;
outputCanvas.height = 1;
outputCtx.fillStyle = 'white';
outputCtx.fillRect(0, 0, 1, 1);
console.warn("Impressionist filter: Original image has zero width or height.");
return outputCanvas;
}
outputCanvas.width = width;
outputCanvas.height = height;
// Fill background. Impressionist paintings are often on a light gesso layer.
outputCtx.fillStyle = 'white';
outputCtx.fillRect(0, 0, width, height);
// Create a source canvas to get pixel data from the original image.
// This is necessary to avoid issues with getImageData on potentially transformed/scaled display images
// and to handle cross-origin issues if the image is not locally hosted or CORS-enabled.
const sourceCanvas = document.createElement('canvas');
sourceCanvas.width = width;
sourceCanvas.height = height;
const sourceCtx = sourceCanvas.getContext('2d');
let imageData;
try {
sourceCtx.drawImage(originalImg, 0, 0, width, height);
imageData = sourceCtx.getImageData(0, 0, width, height);
} catch (e) {
// Handle potential security exceptions (e.g., tainted canvas from cross-origin image)
console.error("Impressionist filter: Could not get image data, possibly due to cross-origin restrictions.", e);
// Fallback: draw original image with an error message overlay
outputCtx.drawImage(originalImg, 0, 0, width, height);
outputCtx.fillStyle = 'rgba(255, 0, 0, 0.3)';
outputCtx.fillRect(0, 0, width, height);
outputCtx.fillStyle = 'white';
outputCtx.strokeStyle = 'black';
outputCtx.lineWidth = 2;
outputCtx.font = 'bold 16px Arial';
outputCtx.textAlign = 'center';
const errorMessage = 'Error: Cannot apply filter due to image security restrictions.';
outputCtx.strokeText(errorMessage, width / 2, height / 2);
outputCtx.fillText(errorMessage, width / 2, height / 2);
return outputCanvas;
}
const pixels = imageData.data;
// Calculate the number of strokes to draw.
// This is based on the image area, desired brush size, and density.
// A higher density means more strokes per unit area.
const numStrokes = Math.floor((width * height / (brushSize * brushSize)) * strokeDensity);
outputCtx.globalAlpha = strokeOpacity;
for (let i = 0; i < numStrokes; i++) {
// Pick a random point in the image to sample color from.
// This point also serves as the base for the stroke's center.
const sampleX = Math.random() * width;
const sampleY = Math.random() * height;
// Get the color of the pixel at (sampleX, sampleY).
// Ensure pixel coordinates are within image bounds.
const pixelX = Math.min(Math.floor(sampleX), width - 1);
const pixelY = Math.min(Math.floor(sampleY), height - 1);
const dataIndex = (pixelY * width + pixelX) * 4;
const r = pixels[dataIndex];
const g = pixels[dataIndex + 1];
const b = pixels[dataIndex + 2];
// const a = pixels[dataIndex + 3]; // Original alpha, not typically used for stroke color directly
outputCtx.fillStyle = `rgb(${r},${g},${b})`;
// Calculate stroke properties, applying jitter for randomness.
// Stroke center: slightly offset from the sample point.
const strokeCenterX = sampleX + (Math.random() - 0.5) * brushSize * jitter;
const strokeCenterY = sampleY + (Math.random() - 0.5) * brushSize * jitter;
// Base size for this stroke: varied by jitter.
// (Math.random() - 0.5) gives a range of [-0.5, 0.5).
// So, size varies from brushSize*(1-jitter/2) to brushSize*(1+jitter/2).
const currentBaseSize = brushSize * (1 + (Math.random() - 0.5) * jitter);
// Ellipse radii: allow for eccentric shapes. Varied independently.
// The 0.8 factor scales the jitter's effect on eccentricity.
const radiusX = Math.max(1, currentBaseSize * (1 + (Math.random() - 0.5) * jitter * 0.8));
const radiusY = Math.max(1, currentBaseSize * (1 + (Math.random() - 0.5) * jitter * 0.8));
// Rotation of the ellipse: scaled by jitter.
// If jitter is 0, rotation is 0. If jitter is 1, full random rotation.
const rotation = Math.random() * Math.PI * 2 * jitter;
// Draw the "brush stroke" (an ellipse in this case).
outputCtx.beginPath();
outputCtx.ellipse(strokeCenterX, strokeCenterY, radiusX, radiusY, rotation, 0, 2 * Math.PI);
outputCtx.fill();
}
// Reset globalAlpha to default, important if context is reused.
outputCtx.globalAlpha = 1.0;
return outputCanvas;
}
Apply Changes