You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, sadnessIntensity = "50", tearsAmount = "30") {
// Validate and parse parameters
const sadLvl = Math.max(0, Math.min(100, isNaN(Number(sadnessIntensity)) ? 50 : Number(sadnessIntensity)));
const tearsCount = Math.max(0, Math.min(200, isNaN(Number(tearsAmount)) ? 30 : Number(tearsAmount)));
const canvas = document.createElement('canvas');
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
const ctx = canvas.getContext('2d');
// 1. Draw the original image
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// 2. Apply "Sad" color effect (Desaturate & Blue Tint)
try {
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imgData.data;
const factor = sadLvl / 100;
for (let i = 0; i < data.length; i += 4) {
let r = data[i], g = data[i + 1], b = data[i + 2];
// Calculate luminance for desaturation
let l = 0.3 * r + 0.59 * g + 0.11 * b;
// Blend original colors with grayscale based on intensity
r = r + (l - r) * factor;
g = g + (l - g) * factor;
b = b + (l - b) * factor;
// Apply blueish 'sorrowful' cool tint
r = r * (1 - 0.2 * factor);
b = b * (1 + 0.3 * factor);
g = g * (1 - 0.1 * factor);
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
}
ctx.putImageData(imgData, 0, 0);
} catch (e) {
// Fallback for CORS tainted canvas: Use a blended overlay instead
const factor = sadLvl / 100;
ctx.fillStyle = `rgba(10, 30, 70, ${factor * 0.4})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// 3. Apply sorrowful dark vignette around edges
const factorNum = sadLvl / 100;
if (factorNum > 0) {
const gradient = ctx.createRadialGradient(
canvas.width / 2, canvas.height / 2, Math.min(canvas.width, canvas.height) * 0.3,
canvas.width / 2, canvas.height / 2, Math.max(canvas.width, canvas.height) * 0.8
);
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
gradient.addColorStop(1, `rgba(0, 5, 20, ${0.7 * factorNum})`);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Helper function to draw a single realistic teardrop shape
function drawTear(ctx, startX, startY, length, radius) {
const dropCenterY = startY + length - radius;
const cpY1 = startY + length * 0.3;
const cpY2 = Math.max(startY + length * 0.4, dropCenterY - radius * 1.2);
ctx.save();
// Define teardrop outline
ctx.beginPath();
ctx.moveTo(startX, startY);
// Left side curve trailing down
ctx.bezierCurveTo(
startX - radius * 0.2, cpY1,
startX - radius, cpY2,
startX - radius, dropCenterY
);
// Bottom curved droplet
ctx.arc(startX, dropCenterY, radius, Math.PI, 0, true);
// Right side curve trailing up
ctx.bezierCurveTo(
startX + radius, cpY2,
startX + radius * 0.2, cpY1,
startX, startY
);
ctx.closePath();
// Fill with a watery, slightly bluish translucent gradient
let tearGrad = ctx.createLinearGradient(startX, startY, startX, dropCenterY + radius);
tearGrad.addColorStop(0, 'rgba(255, 255, 255, 0.05)');
tearGrad.addColorStop(0.5, 'rgba(220, 235, 255, 0.15)');
tearGrad.addColorStop(1, 'rgba(160, 200, 255, 0.45)');
ctx.fillStyle = tearGrad;
ctx.fill();
// Edge stroke for refraction outline
ctx.lineWidth = Math.max(1, radius * 0.05);
ctx.strokeStyle = 'rgba(10, 30, 60, 0.15)';
ctx.stroke();
// Specular highlight (Bottom rim reflection)
ctx.beginPath();
ctx.arc(startX - radius * 0.3, dropCenterY + radius * 0.4, radius * 0.25, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.fill();
// Top trail subtle highlight
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(startX, dropCenterY - radius * 0.5);
ctx.lineWidth = Math.max(0.5, radius * 0.1);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.stroke();
ctx.restore();
}
// 4. Generate tears scattered across image, clustered roughly at "eye" locations
for (let i = 0; i < tearsCount; i++) {
let startX, startY;
if (i < tearsCount * 0.6) {
// Clustered 60% of tearing near eyes region
let isLeft = i % 2 === 0;
let clusterX = isLeft ? canvas.width * 0.38 : canvas.width * 0.62;
let clusterY = canvas.height * 0.35;
startX = clusterX + (Math.random() - 0.5) * canvas.width * 0.15;
startY = clusterY + (Math.random() - 0.5) * canvas.height * 0.15;
} else {
// The rest are strewn vaguely overhead/mixed
startX = canvas.width * Math.random();
startY = canvas.height * Math.random() * 0.6;
}
// Prevent drawing way off canvas width
startX = Math.max(0, Math.min(canvas.width, startX));
let radius = Math.max(3, Math.min(25, canvas.width * (0.008 + Math.random() * 0.012)));
let length = Math.max(20, canvas.height * (0.05 + Math.random() * 0.25));
length = Math.max(length, radius * 2.5); // Ensure stream isn't too short for the drop
drawTear(ctx, startX, startY, length, radius);
}
// Helper function to draw pooling water/tears at the bottom of the image
function drawWave(ctx, waterHeight, waveCount, phase, color) {
ctx.beginPath();
let y = canvas.height - waterHeight;
ctx.moveTo(0, canvas.height); // Starting at bottom-left corner
ctx.lineTo(0, y);
let waveAmp = Math.max(2, canvas.height * 0.012);
let waveFreq = (waveCount * Math.PI * 2) / canvas.width;
let step = Math.max(5, Math.ceil(canvas.width / 100));
for (let x = 0; x <= canvas.width + step; x += step) {
ctx.lineTo(x, y + Math.sin(x * waveFreq + phase) * waveAmp);
}
ctx.lineTo(canvas.width, canvas.height); // Down to bottom-right corner
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
// 5. Draw the pool of tears rising at the bottom based on amount parameter
const maxWaterHeight = canvas.height * 0.25; // max pool level is 25% of image
const waterHeight = maxWaterHeight * (tearsCount / 100);
if (waterHeight > 0) {
// Draw background wave layer
drawWave(ctx, waterHeight * 0.9, 2.5, 0, 'rgba(120, 180, 255, 0.3)');
// Draw foreground wave layer
drawWave(ctx, waterHeight, 3.5, 3, 'rgba(160, 210, 255, 0.45)');
// Optional subtle white stroke on front wave to signify surface
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
ctx.stroke();
}
return canvas;
}
Apply Changes