You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
chalkColorStr = "255,255,250", // Off-white chalk
boardColorStr = "40,40,60", // Dark slate blue board
edgeThreshold = 120, // For Sobel magnitude (approx range 0-1442)
chalkThickness = 1, // 0: 1px lines, 1: 3x3 thickness, 2: 5x5 thickness
noise = 0.1 // 0-1, intensity of noise texture
) {
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
if (width === 0 || height === 0) {
console.error("Image has zero dimensions.");
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = 1; emptyCanvas.height = 1; // Return a minimal canvas
return emptyCanvas;
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d', { willReadFrequently: true }); // Optimization hint
try {
ctx.drawImage(originalImg, 0, 0, width, height);
} catch (e) {
console.error("Error drawing image (possibly not loaded or invalid):", e);
ctx.font = "16px Arial";
ctx.fillStyle = "red";
ctx.fillText("Error drawing image.", 10, Math.min(20, height - 5));
return canvas;
}
let originalImageData;
try {
originalImageData = ctx.getImageData(0, 0, width, height);
} catch (e) {
console.error("Error getting ImageData (possibly tainted canvas due to cross-origin image):", e);
ctx.clearRect(0,0,width,height); // Clear the potentially drawn image if data can't be read
ctx.font = "16px Arial";
ctx.fillStyle = "red";
ctx.fillText("Error: Cannot process cross-origin image.", 10, Math.min(20, height - 5));
return canvas;
}
const originalData = originalImageData.data;
const parsedChalkColor = chalkColorStr.split(',').map(Number);
const parsedBoardColor = boardColorStr.split(',').map(Number);
// 1. Create grayscale version of the image
const grayScaleData = new Uint8ClampedArray(width * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
const r = originalData[i];
const g = originalData[i + 1];
const b = originalData[i + 2];
grayScaleData[y * width + x] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
}
}
// Sobel kernels
const Kx = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
];
const Ky = [
[-1, -2, -1],
[ 0, 0, 0],
[ 1, 2, 1]
];
// 2. Compute Sobel edge magnitudes
const edgeMagnitudes = new Float32Array(width * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let gx = 0;
let gy = 0;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const cX = Math.max(0, Math.min(x + kx, width - 1));
const cY = Math.max(0, Math.min(y + ky, height - 1));
const pixelValue = grayScaleData[cY * width + cX];
gx += Kx[ky + 1][kx + 1] * pixelValue;
gy += Ky[ky + 1][kx + 1] * pixelValue;
}
}
edgeMagnitudes[y * width + x] = Math.sqrt(gx * gx + gy * gy);
}
}
// 3. Create binary edge mask from magnitudes
let edgesMask = new Uint8ClampedArray(width * height);
for (let i = 0; i < edgeMagnitudes.length; i++) {
if (edgeMagnitudes[i] > edgeThreshold) {
edgesMask[i] = 1;
} else {
edgesMask[i] = 0;
}
}
// 4. Dilate edge mask if chalkThickness requires it
// kernelRadius = 0 means 1x1 (no change beyond original pixel)
// kernelRadius = 1 means 3x3 neighborhood etc.
const kernelRadius = Math.max(0, Math.floor(chalkThickness));
if (kernelRadius > 0) {
const dilatedEdgesMask = new Uint8ClampedArray(width * height); // Initialize with 0s
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let isSet = 0;
for (let dy = -kernelRadius; dy <= kernelRadius; dy++) {
for (let dx = -kernelRadius; dx <= kernelRadius; dx++) {
// Check original edgesMask at (x+dx, y+dy)
const currentX = x + dx;
const currentY = y + dy;
if (currentX >= 0 && currentX < width && currentY >= 0 && currentY < height) {
if (edgesMask[currentY * width + currentX] === 1) {
isSet = 1;
break;
}
}
}
if (isSet) break;
}
dilatedEdgesMask[y * width + x] = isSet;
}
}
edgesMask = dilatedEdgesMask; // Use the dilated mask
}
// 5. Create output image data and render final image
const outputCanvas = document.createElement('canvas');
outputCanvas.width = width;
outputCanvas.height = height;
const outputCtx = outputCanvas.getContext('2d');
const outputImageData = outputCtx.createImageData(width, height);
const outputData = outputImageData.data;
// Max noise deviation (e.g., noise=0.1 -> noise effect range +/- 5)
const noiseEffectStrength = noise * 50;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
const maskVal = edgesMask[y * width + x];
// Generate noise once per pixel, scale differently for chalk/board if needed
const randomNoiseBase = (Math.random() - 0.5) * 2; // Range: -1 to 1
if (maskVal === 1) { // Chalk pixel
const chalkNoise = randomNoiseBase * noiseEffectStrength;
outputData[i] = Math.max(0, Math.min(255, parsedChalkColor[0] + chalkNoise));
outputData[i + 1] = Math.max(0, Math.min(255, parsedChalkColor[1] + chalkNoise));
outputData[i + 2] = Math.max(0, Math.min(255, parsedChalkColor[2] + chalkNoise));
outputData[i + 3] = 255;
} else { // Board pixel
const boardNoise = randomNoiseBase * noiseEffectStrength * 0.5; // Board gets half noise
outputData[i] = Math.max(0, Math.min(255, parsedBoardColor[0] + boardNoise));
outputData[i + 1] = Math.max(0, Math.min(255, parsedBoardColor[1] + boardNoise));
outputData[i + 2] = Math.max(0, Math.min(255, parsedBoardColor[2] + boardNoise));
outputData[i + 3] = 255;
}
}
}
outputCtx.putImageData(outputImageData, 0, 0);
return outputCanvas;
}
Apply Changes