You can edit the below JavaScript code to customize the image tool.
Apply Changes
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;
}
Apply Changes