You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, contrastWeight = "30", colorWeight = "30", sharpWeight = "40") {
// Parse weight configurations
const cW = parseFloat(contrastWeight) || 30;
const colW = parseFloat(colorWeight) || 30;
const sW = parseFloat(sharpWeight) || 40;
const totalWeight = cW + colW + sW;
// Downscale for fast algorithmic processing while preserving relative characteristics
const MAX_DIM = 500;
let scale = Math.min(MAX_DIM / originalImg.width, MAX_DIM / originalImg.height);
scale = Math.min(scale, 1); // Only downscale, never upscale
const w = Math.max(3, Math.floor(originalImg.width * scale));
const h = Math.max(3, Math.floor(originalImg.height * scale));
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Draw and extract pixel data
ctx.drawImage(originalImg, 0, 0, w, h);
const imgData = ctx.getImageData(0, 0, w, h);
const data = imgData.data;
const numPixels = w * h;
let lumaValues = new Float32Array(numPixels);
let rgVals = new Float32Array(numPixels);
let ybVals = new Float32Array(numPixels);
let totalLuma = 0;
let sumRg = 0;
let sumYb = 0;
// First Pass: Calculate Luma, RG, YB channels + Mean values
let i = 0;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const idx = (y * w + x) * 4;
const r = data[idx];
const g = data[idx + 1];
const b = data[idx + 2];
// Standard relative luminance (sRGB)
const luma = 0.299 * r + 0.587 * g + 0.114 * b;
lumaValues[i] = luma;
totalLuma += luma;
// Opponent color spaces for Hasler & Süsstrunk's colorfulness metric
const rg = r - g;
const yb = 0.5 * (r + g) - b;
rgVals[i] = rg;
ybVals[i] = yb;
sumRg += rg;
sumYb += yb;
i++;
}
}
const meanLuma = totalLuma / numPixels;
const meanRg = sumRg / numPixels;
const meanYb = sumYb / numPixels;
// Second Pass: Variances
let sumLumaVar = 0;
let sumRgVar = 0;
let sumYbVar = 0;
for (let j = 0; j < numPixels; j++) {
sumLumaVar += (lumaValues[j] - meanLuma) ** 2;
sumRgVar += (rgVals[j] - meanRg) ** 2;
sumYbVar += (ybVals[j] - meanYb) ** 2;
}
// Standard Deviations
const stdLuma = Math.sqrt(sumLumaVar / numPixels);
const stdRg = Math.sqrt(sumRgVar / numPixels);
const stdYb = Math.sqrt(sumYbVar / numPixels);
// Colorfulness Metric
const colorfulness = Math.sqrt(stdRg ** 2 + stdYb ** 2) + 0.3 * Math.sqrt(meanRg ** 2 + meanYb ** 2);
// Calculate Sharpness (Laplacian Variance)
let sumLap = 0;
let lapIndex = 0;
let lapValues = new Float32Array((w - 2) * (h - 2));
for (let y = 1; y < h - 1; y++) {
for (let x = 1; x < w - 1; x++) {
const top = lumaValues[(y - 1) * w + x];
const bottom = lumaValues[(y + 1) * w + x];
const left = lumaValues[y * w + x - 1];
const right = lumaValues[y * w + x + 1];
const center = lumaValues[y * w + x];
// 3x3 Laplacian filter kernel response
const lap = top + bottom + left + right - 4 * center;
lapValues[lapIndex] = lap;
sumLap += lap;
lapIndex++;
}
}
const meanLap = sumLap / lapIndex;
let sumLapVar = 0;
for (let j = 0; j < lapIndex; j++) {
sumLapVar += (lapValues[j] - meanLap) ** 2;
}
const sharpness = lapIndex > 0 ? sumLapVar / lapIndex : 0;
// Normalize out of 100 for each component (empirical thresholds)
// Contrast: stddev luminance of ~60 is very high (perfect contrast).
const contrastNorm = Math.min(100, Math.max(0, (stdLuma / 60) * 100));
// Colorfulness: ~80+ is highly vivid/colorful.
const colorNorm = Math.min(100, Math.max(0, (colorfulness / 80) * 100));
// Sharpness: variance > 800 correlates to highly sharp contours/focus.
const sharpnessNorm = Math.min(100, Math.max(0, (sharpness / 800) * 100));
// Final Weighted Score (Бал)
const finalScore = ((contrastNorm * cW) + (colorNorm * colW) + (sharpnessNorm * sW)) / totalWeight;
// Prepare visual display
// Get original image data url to show in the UI
const origCanvas = document.createElement('canvas');
origCanvas.width = originalImg.width;
origCanvas.height = originalImg.height;
origCanvas.getContext('2d').drawImage(originalImg, 0, 0);
const imgDataUrl = origCanvas.toDataURL("image/jpeg", 0.8);
// Build SVG Gauge
const radius = 55;
const circumference = 2 * Math.PI * radius;
const offset = circumference - (finalScore / 100) * circumference;
const gaugeColor = finalScore >= 80 ? '#2ecc71' : finalScore >= 50 ? '#f1c40f' : '#e74c3c';
const renderBar = (label, score, color) => `
<div style="margin-bottom: 12px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">
<div style="display: flex; justify-content: space-between; font-size: 13px; font-weight: 600; color: #444; margin-bottom: 6px;">
<span>${label}</span>
<span>${Math.round(score)}/100</span>
</div>
<div style="background: #dfe6e9; border-radius: 6px; height: 8px; overflow: hidden;">
<div style="background: ${color}; width: ${score}%; height: 100%; border-radius: 6px; transition: width 1s ease-in-out;"></div>
</div>
</div>
`;
const container = document.createElement('div');
container.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 24px;
background: #ffffff;
padding: 24px;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
max-width: 800px;
margin: 0 auto;
border: 1px solid #f1f2f6;
`;
container.innerHTML = `
<div style="flex: 1; min-width: 250px; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #f8f9fa; border-radius: 8px; padding: 10px;">
<img src="${imgDataUrl}" style="max-width: 100%; max-height: 350px; object-fit: contain; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);" alt="Original Image" />
</div>
<div style="flex: 1; min-width: 280px; display: flex; flex-direction: column; justify-content: center;">
<div style="text-align: center; margin-bottom: 20px;">
<h3 style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0 0 5px 0; color: #2c3e50; font-size: 22px;">Image Score Calculator</h3>
<p style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; color: #7f8c8d; font-size: 14px; text-transform: uppercase; letter-spacing: 1px;">Загальний Бал</p>
<div style="margin-top: 15px; position: relative; display: inline-block;">
<svg width="140" height="140" viewbox="0 0 140 140">
<circle cx="70" cy="70" r="${radius}" fill="none" stroke="#f1f2f6" stroke-width="12" />
<circle cx="70" cy="70" r="${radius}" fill="none" stroke="${gaugeColor}" stroke-width="12"
stroke-dasharray="${circumference}" stroke-dashoffset="${offset}"
transform="rotate(-90 70 70)" stroke-linecap="round" />
</svg>
<div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center;">
<span style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 32px; font-weight: 800; color: #2d3436;">
${Math.round(finalScore)}
</span>
</div>
</div>
</div>
<div style="padding: 15px; background: #f8f9fa; border-radius: 8px;">
${renderBar('Sharpness (Чіткість)', sharpnessNorm, '#3498db')}
${renderBar('Contrast (Контраст)', contrastNorm, '#9b59b6')}
${renderBar('Colorfulness (Насиченість)', colorNorm, '#e67e22')}
</div>
</div>
`;
return container;
}
Apply Changes