Please bookmark this page to avoid losing your image tool!

Image Crying Face Filter

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
async function processImage(originalImg, intensity = 100) {
    let intensityNum = Number(intensity);
    if (isNaN(intensityNum)) {
        intensityNum = 100;
    }
    
    // Create the main drawing surface
    const canvas = document.createElement('canvas');
    canvas.width = originalImg.width;
    canvas.height = originalImg.height;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(originalImg, 0, 0);

    // Helpers
    function distance(p1, p2) {
        return Math.hypot(p1.x - p2.x, p1.y - p2.y) || 0;
    }

    function drawFlush(cx, cy, radius, r, g, b, alpha) {
        if (radius <= 0 || isNaN(radius)) return;
        ctx.save();
        ctx.beginPath();
        ctx.arc(cx, cy, radius, 0, Math.PI * 2);
        const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
        grad.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${alpha})`);
        grad.addColorStop(0.5, `rgba(${r}, ${g}, ${b}, ${alpha * 0.4})`);
        grad.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
        ctx.fillStyle = grad;
        ctx.fill();
        ctx.restore();
    }

    function getJawYAtX(jaw, x) {
        let closestY = null;
        for (let i = 0; i < jaw.length - 1; i++) {
            let p1 = jaw[i];
            let p2 = jaw[i+1];
            if ((x >= p1.x && x <= p2.x) || (x >= p2.x && x <= p1.x)) {
                let t = p1.x === p2.x ? 0 : (x - p1.x) / (p2.x - p1.x);
                let y = p1.y + t * (p2.y - p1.y);
                if (closestY === null || y > closestY) {
                    closestY = y;
                }
            }
        }
        return closestY !== null ? closestY : jaw[8].y;
    }

    function drawTearStream(startX, startY, endY, eyeWidth, direction) {
        const length = endY - startY;
        if (length <= 0) return;
        const width = eyeWidth * 0.25; 
        if (width <= 0 || isNaN(width)) return;
        
        ctx.save();
        ctx.beginPath();
        ctx.moveTo(startX, startY);
        
        const cp1x = startX + direction * width * 1.5;
        const cp1y = startY + length * 0.3;
        const cp2x = startX - direction * width * 0.5;
        const cp2y = startY + length * 0.7;
        const endX = startX + direction * width * 0.5;

        ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endX, endY);
        
        ctx.lineWidth = width * 0.7;
        ctx.lineCap = "round";
        
        const gradient = ctx.createLinearGradient(startX, startY, startX, endY);
        // Using a semi-transparent glossy blueish white to match tears
        gradient.addColorStop(0, 'rgba(230, 245, 255, 0.2)');
        gradient.addColorStop(0.5, 'rgba(230, 245, 255, 0.5)');
        gradient.addColorStop(1, 'rgba(230, 245, 255, 0.85)');
        
        ctx.strokeStyle = gradient;
        ctx.shadowColor = 'rgba(255, 255, 255, 0.6)';
        ctx.shadowBlur = width;
        ctx.stroke();

        // Droplet at the bottom
        ctx.beginPath();
        ctx.arc(endX, endY, width * 0.8, 0, Math.PI * 2);
        ctx.fillStyle = 'rgba(230, 245, 255, 0.9)';
        ctx.shadowColor = 'rgba(0, 0, 0, 0.1)';
        ctx.shadowBlur = width * 0.5;
        ctx.fill();

        // Inner droplet highlight
        ctx.beginPath();
        ctx.arc(endX - width * 0.2, endY - width * 0.2, width * 0.25, 0, Math.PI * 2);
        ctx.fillStyle = 'rgba(255, 255, 255, 1)';
        ctx.shadowBlur = 0;
        ctx.fill();

        ctx.restore();
    }

    function drawTearDrop(x, y, size) {
        if (size <= 0 || isNaN(size)) return;
        ctx.save();
        ctx.beginPath();
        ctx.moveTo(x, y - size);
        ctx.quadraticCurveTo(x + size/1.5, y - size/2, x + size/1.5, y);
        ctx.arc(x, y, size/1.5, 0, Math.PI);
        ctx.quadraticCurveTo(x - size/1.5, y - size/2, x, y - size);
        ctx.closePath();
        
        ctx.fillStyle = 'rgba(230, 245, 255, 0.85)';
        ctx.shadowColor = 'rgba(255, 255, 255, 0.5)';
        ctx.shadowBlur = size / 2;
        ctx.fill();
        
        ctx.beginPath();
        ctx.arc(x - size/4, y + size/6, size/4, 0, Math.PI*2);
        ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
        ctx.fill();
        ctx.restore();
    }

    function drawRainBackground() {
        ctx.save();
        ctx.fillStyle = 'rgba(10, 30, 60, 0.35)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.strokeStyle = 'rgba(200, 220, 255, 0.5)';
        ctx.lineCap = 'round';
        
        const numDrops = Math.floor((canvas.width * canvas.height) / (1500 - (intensityNum * 5))); 
        for(let i = 0; i < numDrops; i++) {
            let lx = Math.random() * canvas.width;
            let ly = Math.random() * canvas.height;
            let len = 10 + Math.random() * 20;
            let angle = (Math.random() - 0.2) * 0.3; 
            
            ctx.beginPath();
            ctx.moveTo(lx, ly);
            ctx.lineTo(lx - Math.sin(angle)*len, ly + Math.cos(angle)*len);
            ctx.lineWidth = 1 + Math.random() * 1.5;
            ctx.stroke();
        }
        ctx.restore();
    }

    // Load Vlad Mandic's well-maintained and active face-api fork dynamically
    if (typeof window.faceapi === 'undefined') {
        try {
            await new Promise((resolve, reject) => {
                const script = document.createElement('script');
                script.src = 'https://cdn.jsdelivr.net/npm/@vladmandic/face-api/dist/face-api.js';
                script.onload = resolve;
                script.onerror = reject;
                document.head.appendChild(script);
            });
        } catch (e) {
            console.error("Failed to load face-api script", e);
            drawRainBackground();
            return canvas;
        }
    }

    let detections;
    try {
        const MODEL_URL = 'https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model/';
        
        if (!window.faceapi.nets.tinyFaceDetector.isLoaded) {
            await window.faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL);
        }
        if (!window.faceapi.nets.faceLandmark68Net.isLoaded) {
            await window.faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL);
        }

        const options = new window.faceapi.TinyFaceDetectorOptions({ inputSize: 512, scoreThreshold: 0.3 });
        detections = await window.faceapi.detectAllFaces(canvas, options).withFaceLandmarks();
    } catch (e) {
        console.error("Model loading or detection failed", e);
        drawRainBackground(); // Fallback styling
        return canvas;
    }

    // Give a melancholy filter tone if faces are detected, else fallback to rainy vibe
    if (detections.length === 0) {
        drawRainBackground();
        return canvas;
    } else {
        ctx.fillStyle = `rgba(10, 30, 50, ${0.1 + (intensityNum / 1000)})`;
        ctx.fillRect(0, 0, canvas.width, canvas.height);
    }

    // Filter Processing per Face
    detections.forEach(detection => {
        const landmarks = detection.landmarks;
        const jaw = landmarks.getJawOutline();
        const leftEye = landmarks.getLeftEye();
        const rightEye = landmarks.getRightEye();
        const nose = landmarks.getNose();

        const leftEyeWidth = distance(leftEye[0], leftEye[3]);
        const rightEyeWidth = distance(rightEye[0], rightEye[3]);
        
        const leftEyeCenter = {
            x: leftEye.reduce((s, p) => s + p.x, 0) / leftEye.length,
            y: leftEye.reduce((s, p) => s + p.y, 0) / leftEye.length
        };
        const rightEyeCenter = {
            x: rightEye.reduce((s, p) => s + p.x, 0) / rightEye.length,
            y: rightEye.reduce((s, p) => s + p.y, 0) / rightEye.length
        };
        const noseTip = nose[3];

        const valScale = Math.max(0.1, intensityNum / 100);
        const flushAlpha = 0.5 * valScale;

        // Render Red flushed puffy eyes
        drawFlush(leftEyeCenter.x, leftEyeCenter.y, leftEyeWidth * 1.5, 255, 50, 50, flushAlpha);
        drawFlush(rightEyeCenter.x, rightEyeCenter.y, rightEyeWidth * 1.5, 255, 50, 50, flushAlpha);
        
        // Render Red nose (as if recovering from crying)
        drawFlush(noseTip.x, noseTip.y, leftEyeWidth * 1.3, 255, 70, 70, flushAlpha + 0.1);
        
        // Render Rosy teary cheeks
        drawFlush(leftEyeCenter.x, leftEyeCenter.y + leftEyeWidth * 1.8, leftEyeWidth * 2.2, 255, 100, 100, flushAlpha);
        drawFlush(rightEyeCenter.x, rightEyeCenter.y + rightEyeWidth * 1.8, rightEyeWidth * 2.2, 255, 100, 100, flushAlpha);

        // Render 2 tears coming from left eye
        const lx1 = leftEye[0].x, ly1 = leftEye[0].y;
        const lx2 = leftEye[3].x, ly2 = leftEye[3].y;
        drawTearStream(lx1, ly1, getJawYAtX(jaw, lx1), leftEyeWidth * valScale, -1);
        drawTearStream(lx2, ly2, getJawYAtX(jaw, lx2), leftEyeWidth * valScale, 1);
        
        // Render 2 tears coming from right eye
        const rx1 = rightEye[0].x, ry1 = rightEye[0].y; 
        const rx2 = rightEye[3].x, ry2 = rightEye[3].y;
        drawTearStream(rx1, ry1, getJawYAtX(jaw, rx1), rightEyeWidth * valScale, -1);
        drawTearStream(rx2, ry2, getJawYAtX(jaw, rx2), rightEyeWidth * valScale, 1);

        // Occasional extra stray drops
        if (intensityNum > 40) {
            drawTearDrop(lx1 - leftEyeWidth * 0.4, ly1 + leftEyeWidth * 1.8, leftEyeWidth * 0.25 * valScale);
            drawTearDrop(rx2 + rightEyeWidth * 0.8, ry2 + rightEyeWidth * 1.3, rightEyeWidth * 0.2 * valScale);
            if (intensityNum > 80) drawTearDrop(rx1 + rightEyeWidth * 0.4, ry1 + rightEyeWidth * 2.3, rightEyeWidth * 0.25 * valScale);
        }
    });

    return canvas;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Crying Face Filter is an AI-powered image editing tool that applies a melancholy, tearful effect to portraits. Using facial landmark detection, the tool automatically identifies eyes, noses, and cheeks to add realistic digital tears, reddish eye and nose flushing, and rosy cheeks. It also includes a customizable intensity setting and can apply a rainy background overlay to enhance the dramatic mood. This tool is ideal for creating humorous or dramatic social media content, memes, or themed digital art.

Leave a Reply

Your email address will not be published. Required fields are marked *