You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
brushSize = 10, // Size of the brush stroke (diameter/width)
strokeType = "circle", // "circle", "rectangle", "ellipse"
strokeAngleVariation = 30, // Max random angle for rect/ellipse strokes (degrees)
gridDensityFactor = 0.8, // Grid step = brushSize * gridDensityFactor. Lower = denser strokes.
jitterFactor = 0.4, // 0 (no jitter) to 1 (max jitter relative to gridStep/2)
frameTotalWidth = 50, // Total width of one side of the frame
frameOuterRatio = 0.6, // Proportion of frameTotalWidth for the outer "wood" part
frameColorOuter = "#A0522D", // Main color for the outer "wood" frame (Sienna)
frameColorOuterShade = "#8B4513",// Darker shade for wood frame (SaddleBrown)
frameColorMat = "#F5F5DC" // Color for the inner mat (Beige)
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Ensure parameters are numbers where expected
brushSize = Number(brushSize) || 10;
strokeAngleVariation = Number(strokeAngleVariation) || 30;
gridDensityFactor = Number(gridDensityFactor) || 0.8;
jitterFactor = Number(jitterFactor) || 0.4;
frameTotalWidth = Number(frameTotalWidth) || 0; // Default to 0 if not framing
frameOuterRatio = Number(frameOuterRatio) || 0.6;
const outerFrameActualWidth = Math.max(0, frameTotalWidth * frameOuterRatio);
const matActualWidth = Math.max(0, frameTotalWidth * (1 - frameOuterRatio));
canvas.width = originalImg.width + 2 * frameTotalWidth;
canvas.height = originalImg.height + 2 * frameTotalWidth;
// --- 1. Draw the Frame ---
if (frameTotalWidth > 0) {
// 1.A. Draw Outer "Wood" Part
if (outerFrameActualWidth > 0) {
ctx.fillStyle = frameColorOuter;
// Fill the areas for the outer frame
ctx.fillRect(0, 0, canvas.width, outerFrameActualWidth); // Top strip
ctx.fillRect(0, canvas.height - outerFrameActualWidth, canvas.width, outerFrameActualWidth); // Bottom strip
ctx.fillRect(0, outerFrameActualWidth, outerFrameActualWidth, canvas.height - 2 * outerFrameActualWidth); // Left strip
ctx.fillRect(canvas.width - outerFrameActualWidth, outerFrameActualWidth, outerFrameActualWidth, canvas.height - 2 * outerFrameActualWidth); // Right strip
// Shading for Outer "Wood" Part to give a slight 3D effect
const outerEdgeShadeWidth = Math.max(1, Math.floor(outerFrameActualWidth * 0.08)); // For outermost boundary
const innerEdgeShadeWidth = Math.max(1, Math.floor(outerFrameActualWidth * 0.05)); // For edge bordering the mat
if (outerEdgeShadeWidth > 0) {
ctx.fillStyle = frameColorOuterShade; // Darker shade
// Darker bottom and right edges of the outermost frame boundary
ctx.fillRect(0, canvas.height - outerEdgeShadeWidth, canvas.width, outerEdgeShadeWidth); // Bottom boundary
ctx.fillRect(canvas.width - outerEdgeShadeWidth, 0, outerEdgeShadeWidth, canvas.height); // Right boundary
}
if (innerEdgeShadeWidth > 0) { // Shading on the inner edge of the outer frame
ctx.fillStyle = frameColorOuterShade; // Darker shade implies shadow onto mat
// Top inner edge of outer frame
ctx.fillRect(outerFrameActualWidth, outerFrameActualWidth, canvas.width - 2 * outerFrameActualWidth, innerEdgeShadeWidth);
// Left inner edge of outer frame
ctx.fillRect(outerFrameActualWidth, outerFrameActualWidth, innerEdgeShadeWidth, canvas.height - 2 * outerFrameActualWidth);
}
}
// 1.B. Draw Mat Part
if (matActualWidth > 0) {
const matX = outerFrameActualWidth;
const matY = outerFrameActualWidth;
// Mat covers the area inside the outer frame up to the image
const matOverallWidth = originalImg.width + 2 * matActualWidth;
const matOverallHeight = originalImg.height + 2 * matActualWidth;
ctx.fillStyle = frameColorMat;
ctx.fillRect(matX, matY, matOverallWidth, matOverallHeight);
// Inner shadow from Mat onto Image Area (simple version)
// Shadow depth relative to mat width, capped for small mats
const innerShadowDepth = Math.max(1, Math.floor(Math.min(matActualWidth * 0.15, 5)));
if (innerShadowDepth > 0) {
ctx.fillStyle = 'rgba(0,0,0,0.25)'; // Slightly darker shadow
// Shadow cast by top edge of mat on image area (image starts at frameTotalWidth)
ctx.fillRect(frameTotalWidth, frameTotalWidth, originalImg.width, innerShadowDepth);
// Shadow cast by left edge of mat on image area
ctx.fillRect(frameTotalWidth, frameTotalWidth, innerShadowDepth, originalImg.height);
}
}
}
// --- 2. Create Impressionistic Image ---
const impressionisticCanvas = document.createElement('canvas');
impressionisticCanvas.width = originalImg.width;
impressionisticCanvas.height = originalImg.height;
const impCtx = impressionisticCanvas.getContext('2d', { willReadFrequently: true });
const tempCanvas = document.createElement('canvas');
tempCanvas.width = originalImg.width;
tempCanvas.height = originalImg.height;
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
tempCtx.drawImage(originalImg, 0, 0);
const imgData = tempCtx.getImageData(0, 0, originalImg.width, originalImg.height);
const data = imgData.data;
function getAverageColor(imgPixelData, startX, startY, sampleW, sampleH, sourceImgWidth, sourceImgHeight) {
let r = 0, g = 0, b = 0, aSum = 0, count = 0;
// Clamp sample coordinates and dimensions to image bounds
const sX = Math.max(0, Math.floor(startX));
const sY = Math.max(0, Math.floor(startY));
const eX = Math.min(Math.floor(startX + sampleW), sourceImgWidth);
const eY = Math.min(Math.floor(startY + sampleH), sourceImgHeight);
for (let y = sY; y < eY; y++) {
for (let x = sX; x < eX; x++) {
const index = (y * sourceImgWidth + x) * 4;
r += imgPixelData[index];
g += imgPixelData[index + 1];
b += imgPixelData[index + 2];
aSum += imgPixelData[index + 3];
count++;
}
}
return count > 0 ? {
r: Math.round(r / count),
g: Math.round(g / count),
b: Math.round(b / count),
a: Math.round(aSum / count)
} : { r: 0, g: 0, b: 0, a: 0 }; // Default to black transparent if no pixels sampled
}
const gridStep = Math.max(1, brushSize * gridDensityFactor);
for (let y = 0; y < originalImg.height; y += gridStep) {
for (let x = 0; x < originalImg.width; x += gridStep) {
const currentGridCellCenterX = x + gridStep / 2;
const currentGridCellCenterY = y + gridStep / 2;
const avgColor = getAverageColor(data,
currentGridCellCenterX - brushSize / 2,
currentGridCellCenterY - brushSize / 2,
brushSize, brushSize,
originalImg.width, originalImg.height
);
if (avgColor.a < 10) continue; // Skip nearly transparent areas
impCtx.fillStyle = `rgba(${avgColor.r},${avgColor.g},${avgColor.b},${avgColor.a / 255})`;
const jitterMax = gridStep / 2 * jitterFactor;
const jitterX = (Math.random() - 0.5) * 2 * jitterMax;
const jitterY = (Math.random() - 0.5) * 2 * jitterMax;
const strokeX = currentGridCellCenterX + jitterX;
const strokeY = currentGridCellCenterY + jitterY;
impCtx.save();
impCtx.translate(strokeX, strokeY);
if (strokeType === "rectangle" || strokeType === "ellipse") {
const angle = (Math.random() - 0.5) * 2 * strokeAngleVariation * (Math.PI / 180);
impCtx.rotate(angle);
}
const halfBrushSize = brushSize / 2;
if (strokeType === "circle") {
impCtx.beginPath();
impCtx.arc(0, 0, halfBrushSize, 0, 2 * Math.PI);
impCtx.fill();
} else if (strokeType === "rectangle") {
impCtx.fillRect(-halfBrushSize, -halfBrushSize / 1.5, brushSize, brushSize * 2 / 1.5);
} else if (strokeType === "ellipse") {
impCtx.beginPath();
impCtx.ellipse(0, 0, halfBrushSize, halfBrushSize / 1.5, 0, 0, 2 * Math.PI);
impCtx.fill();
}
impCtx.restore();
}
}
// --- 3. Draw the Impressionistic Image onto the Main Canvas ---
ctx.drawImage(impressionisticCanvas, frameTotalWidth, frameTotalWidth);
return canvas;
}
Apply Changes