console.log("🧪 JS is updated: 2024-05-20"); let setStatus = function () {}; // Summary calculator function summarizeResults() { console.log("▶ summarizeResults() invoked"); const ratingCells = document.querySelectorAll('td[id^="assessment-"]'); const counts = { perfect: 0, deviation0: 0, deviation1: 0, deviation2: 0, deviation3: 0, deviation4: 0, }; const weights = { perfect: 2, deviation0: 1, deviation1: 0, deviation2: -1, deviation3: -2, deviation4: -2, }; ratingCells.forEach(cell => { const html = cell.innerHTML; if (html.includes("perfect")) counts.perfect++; else if (html.includes("deviation-0")) counts.deviation0++; else if (html.includes("deviation-1")) counts.deviation1++; else if (html.includes("deviation-2")) counts.deviation2++; else if (html.includes("deviation-3")) counts.deviation3++; else if (html.includes("deviation-4")) counts.deviation4++; }); const totalScore = counts.perfect * weights.perfect + counts.deviation0 * weights.deviation0 + counts.deviation1 * weights.deviation1 + counts.deviation2 * weights.deviation2 + counts.deviation3 * weights.deviation3 + counts.deviation4 * weights.deviation4; const maxScore = 22; const normalized = Math.max(0, Math.min(8, Math.round((totalScore / maxScore) * 9))); const psl = `PSL${normalized}`; const labels = { 0: "Subhuman", 1: "Sub5", 2: "Low-tier Normie", 3: "Normie", 4: "Upper Normie", 5: "Chadlite", 6: "Chad", 7: "Gigachad", 8: "Terachad" }; const label = labels[normalized] || "Unknown"; const hierarchyLabels = { 8: { titles: ["Era Defining Man", "Immortal", "God Among Men"], description: "Men destined to define eras and change history, who will be remembered for millennia to come", color: "#A0D8EF" }, 7: { titles: ["King", "Explorer", "Philosopher"], description: "Philosophers, Kings, Explorers, and Inventors", color: "#4682B4" }, 6: { titles: ["Influential Artist", "Writer", "General"], description: "Influential Artists, Writers, and Generals", color: "#5F9EA0" }, 5: { titles: ["Diplomat", "Official", "Officer"], description: "Officers, Officials, Diplomats", color: "#8FBC8F" }, 4: { titles: ["Middle Management", "Soldier", "Craftsman"], description: "Middle Management, Soldiers, Craftsmen", color: "#8B5A2B" }, 3: { titles: ["Manual Laborerer", "Peasant", "Street Merchant"], description: "Manual Labourers, Peasants, Street Merchants", color: "#A52A2A" }, 2: { titles: ["Street Sweeper", "Drain Cleaner", "Waste Collector"], description: "Street Cleaners", color: "#B22222" }, 1: { titles: ["Sanitation Worker", "Toilet Scrubber", "Gutterman"], description: "Sanitation Workers", color: "#800000" }, 0: { titles: ["Untouchable", "Subhuman", "Bottom of the Barrel"], description: "Untouchables", color: "#2F2F2F" } }; const badgeClass = normalized <= 3 ? "danger" : normalized <= 6 ? "warning" : "success"; const gradingToggle = document.getElementById("grading-toggle"); const usePSL = gradingToggle ? gradingToggle.checked : true; const scoreCell = document.getElementById("total-score"); const breakdownCell = document.getElementById("total-breakdown"); const resultCell = document.getElementById("total-psl"); if (scoreCell) scoreCell.innerHTML = `${totalScore}`; if (breakdownCell) breakdownCell.innerHTML = ` Perfect: ${counts.perfect}, Slight: ${counts.deviation0}, Noticeable: ${counts.deviation1}, Significant: ${counts.deviation2}, Horrible: ${counts.deviation3}, Extreme: ${counts.deviation4} `; if (resultCell) { if (usePSL) { resultCell.innerHTML = `${psl}
${label}`; } else { const h = hierarchyLabels[normalized] || hierarchyLabels[1]; const title = Array.isArray(h.titles) ? h.titles[Math.floor(Math.random() * h.titles.length)] : h.titles; const lightColors = ["#A0D8EF", "#8FBC8F", "#A52A2A"]; const isLight = lightColors.includes(h.color); const textColor = isLight ? "black" : "white"; resultCell.innerHTML = ` ${title}
${h.description} `; } } } async function main() { const imageInputFile = document.getElementById("image-file"); const imageInputUrl = document.getElementById("image-url"); const renderContainer = document.getElementById("render"); // Disable inputs until database loads imageInputFile.disabled = true; imageInputUrl.disabled = true; const _model = await faceLandmarksDetection.load(faceLandmarksDetection.SupportedPackages.mediapipeFacemesh, { maxFaces: 1 }); window.database = await setupDatabase(); // Enable inputs after database loads imageInputFile.disabled = false; imageInputUrl.disabled = false; const introductionElement = document.getElementById("introduction"); const analyzingElement = document.getElementById("analyzing"); const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); let data = void 0; const gradingToggle = document.getElementById("grading-toggle"); if (gradingToggle) { gradingToggle.addEventListener("change", summarizeResults); } imageInputFile.addEventListener("change", async () => { if (imageInputFile.files[0]) { introductionElement.style.display = "none"; analyzingElement.classList.remove("d-none"); data && clearData(); setStatus("Reading image"); imageInputUrl.value = ""; let url = URL.createObjectURL(imageInputFile.files[0]); await onChange(url); } }); imageInputUrl.addEventListener("change", async () => { if (imageInputUrl.value) { introductionElement.style.display = "none"; analyzingElement.classList.remove("d-none"); data && clearData(); setStatus("Downloading image"); imageInputFile.value = ""; let file = await (await fetch(imageInputUrl.value)).blob(); let url = URL.createObjectURL(file); await onChange(url); } }); async function onChange(url) { setStatus("Analysing"); // Show canvas container with loading overlay renderContainer.classList.remove("d-none"); renderContainer.classList.add("loading"); let analysis = await analyze(canvas, ctx, url); data = analysis.criteria = { midfaceRatio: {...createBindings(analysis.criteria.midfaceRatio, "midface-ratio")}, facialWidthToHeightRatio: {...createBindings(analysis.criteria.facialWidthToHeightRatio, "facial-width-to-height-ratio")}, chinToPhiltrumRatio: {...createBindings(analysis.criteria.chinToPhiltrumRatio, "chin-to-philtrum-ratio")}, canthalTilt: {...createBindings(analysis.criteria.canthalTilt, "canthal-tilt")}, mouthToNoseRatio: {...createBindings(analysis.criteria.mouthToNoseRatio, "mouth-to-nose-ratio")}, bigonialWidth: {...createBindings(analysis.criteria.bigonialWidth, "bigonial-width")}, lipRatio: {...createBindings(analysis.criteria.lipRatio, "lip-ratio")}, eyeSeparationRatio: {...createBindings(analysis.criteria.eyeSeparationRatio, "eye-separation-ratio")}, eyeToMouthAngle: {...createBindings(analysis.criteria.eyeToMouthAngle, "eye-to-mouth-angle")}, lowerThirdHeight: {...createBindings(analysis.criteria.lowerThirdHeight, "lower-third-height")}, palpebralFissureLength: {...createBindings(analysis.criteria.palpebralFissureLength, "palpebral-fissure-length")}, eyeColor: {...createBindings(analysis.criteria.eyeColor, "eye-color")}, }; function createBindings(metric, id) { return { analysis: metric, render: document.getElementById(`value-${id}`), toggle: document.getElementById(`toggle-${id}`), ideal: document.getElementById(`ideal-${id}`), assessment: document.getElementById(`assessment-${id}`), }; } // Helper function to capitalize first letter const capitalizeFirst = (str) => { if (!str) return str; // Handle HTML strings - capitalize first letter of text content after tags if (str.includes('<')) { // Capitalize first letter after opening tag (handle whitespace) return str.replace(/>\s*([a-z])/g, (match, letter) => { return match.replace(letter, letter.toUpperCase()); }); } // Handle plain strings - capitalize first letter if it's alphabetic // Skip if it starts with a number or special character if (str.length > 0 && /^[a-z]/.test(str)) { return str.charAt(0).toUpperCase() + str.slice(1); } return str; }; let calculate = () => { for (let i of Object.values(analysis.criteria)) { i.analysis.calculate(); i.render.innerHTML = capitalizeFirst(i.analysis.render()); i.ideal.innerHTML = capitalizeFirst(i.analysis.ideal()); i.assessment.innerHTML = capitalizeFirst(i.analysis.assess()); } analysis.criteria.eyeColor.analysis.detect( analysis.image, Array.from(analysis.criteria.eyeColor.render.children).map(i => i.getContext("2d")) ); summarizeResults(); // 👈 Score update } let render = () => { analysis.resetToImage(); // Reset watermark flag for new frame ctx._watermarkDrawn = false; for (let i of Object.values(analysis.criteria)) { if (i.toggle.checked) { i.analysis.draw(ctx); } } } for (let i of Object.values(analysis.criteria)) { i.toggle.onchange = () => render(); } let moving = false; canvas.onmousedown = ({ offsetX: x, offsetY: y }) => { let necessaryPoints = Object.values(analysis.criteria).filter(i => i.toggle.checked).map(i => i.analysis.necessaryPoints()).flat(); for (let i in analysis.points) { if (analysis.points.hasOwnProperty(i) && necessaryPoints.includes(i)) { if (Math.sqrt( (analysis.points[i][0] - x) ** 2 + (analysis.points[i][1] - y) ** 2 ) <= analysis.arcRadius) { moving = i; return; } } } } canvas.ontouchstart = (e) => { let bcr = e.target.getBoundingClientRect(); let x = e.targetTouches[0].clientX - bcr.x; let y = e.targetTouches[0].clientY - bcr.y; canvas.onmousedown({ offsetX: x, offsetY: y }); } canvas.onmouseup = () => { moving = false; } canvas.ontouchend = canvas.ontouchcancel = (e) => { canvas.onmouseup(); } canvas.onmousemove = ({ offsetX: x, offsetY: y }) => { if (moving) { analysis.points[moving] = [x, y]; calculate(); render(); } else { let necessaryPoints = Object.values(analysis.criteria).filter(i => i.toggle.checked).map(i => i.analysis.necessaryPoints()).flat(); for (let i in analysis.points) { if (analysis.points.hasOwnProperty(i) && necessaryPoints.includes(i)) { if (Math.sqrt( (analysis.points[i][0] - x) ** 2 + (analysis.points[i][1] - y) ** 2 ) <= analysis.arcRadius) { render(); ctx.beginPath(); ctx.strokeStyle = "gray"; let oldLineWidth = ctx.lineWidth; ctx.lineWidth = 0.5; ctx.arc(analysis.points[i][0], analysis.points[i][1], ctx.arcRadius + 1.5, 0, 2 * Math.PI); ctx.stroke(); ctx.lineWidth = oldLineWidth; return; } } } render(); } } canvas.ontouchmove = (e) => { let bcr = e.target.getBoundingClientRect(); let x = e.targetTouches[0].clientX - bcr.x; let y = e.targetTouches[0].clientY - bcr.y; canvas.onmousemove({ offsetX: x, offsetY: y }); } analyzingElement.classList.add("d-none"); // Remove loading overlay and show canvas renderContainer.classList.remove("loading"); calculate(); render(); } function clearData() { canvas.width = 0; canvas.height = 0; renderContainer.classList.add("d-none"); renderContainer.classList.remove("loading"); if (data) { for (let i of Object.values(data)) { i.render.innerHTML = ""; i.ideal.innerHTML = ""; i.assessment.innerHTML = ""; } } } setStatus = (text) => document.getElementById("analyzing-status").innerHTML = text; document.querySelector("#loading").style.display = "none"; document.querySelector(".container").classList.remove("d-none"); } async function analyze(canvas, ctx, url) { setStatus("Loading image"); let image = await loadImage(url); canvas.width = image.width; canvas.height = image.height; resetToImage(ctx, image); ctx.lineWidth = Math.sqrt((image.width * image.height) / 100000); ctx.arcRadius = Math.sqrt((image.width * image.height) / 100000); setStatus("Analysing"); const model = await faceLandmarksDetection.load(faceLandmarksDetection.SupportedPackages.mediapipeFacemesh, { maxFaces: 1 }); let face = await findLandmarks(model, image); let [points, criteria] = analyseCriteria(face); return { image, resetToImage: () => resetToImage(ctx, image), points, criteria, arcRadius: ctx.arcRadius, }; } function loadImage(url) { return new Promise((resolve) => { const image = new Image(); image.src = url; image.addEventListener("load", () => resolve(image)); }); } function resetToImage(ctx, image) { ctx.drawImage(image, 0, 0); } async function findLandmarks(model, image) { const predictions = await model.estimateFaces({ input: image }); if (predictions.length > 0) { return predictions[0]; } else { throw new Error("No face detected"); } } (async function () { await main(); })();