diff --git a/backend/fonts/DejaVuSans-Bold.ttf b/backend/fonts/DejaVuSans-Bold.ttf new file mode 100644 index 0000000..06db62c --- /dev/null +++ b/backend/fonts/DejaVuSans-Bold.ttf Binary files differ diff --git a/backend/fonts/DejaVuSans.ttf b/backend/fonts/DejaVuSans.ttf new file mode 100644 index 0000000..2fbbe69 --- /dev/null +++ b/backend/fonts/DejaVuSans.ttf Binary files differ diff --git a/backend/og.py b/backend/og.py index 7901ba6..20813d0 100644 --- a/backend/og.py +++ b/backend/og.py @@ -1,8 +1,10 @@ """VerIQ — OG image generation (1200x630 PNG cards for social scrapers). Scrapers (X/Twitter, Telegram, Facebook) don't render SVG and need a real -PNG. We draw the card with Pillow: dark brutalist background, big IQ number, -percentile line, signature badge. Cached on disk per result_id. +PNG. We draw the card with Pillow: dark brutalist background, huge IQ number, +percentile line, bell-curve strip with a marker, signature badge, verify URL. +Fonts are bundled in backend/fonts/ so the card renders identically anywhere. +Cached on disk per result_id (design version prefixed). """ import os from pathlib import Path @@ -14,6 +16,7 @@ W, H = 1200, 630 BG = (13, 13, 15) LINE = (42, 42, 51) +CURVE_FILL = (38, 47, 66) TEXT = (232, 232, 234) MUTED = (138, 138, 148) ACCENT = (122, 162, 247) @@ -21,7 +24,10 @@ BAD = (247, 118, 142) WARN = (224, 175, 104) +DESIGN = "r2" # bump to invalidate the on-disk cache after card redesign + _FONT_DIRS = [ + str(Path(__file__).resolve().parent / "fonts"), # bundled (repo) "/usr/share/fonts/truetype/dejavu/", "/usr/share/fonts/dejavu/", ] @@ -37,11 +43,13 @@ def _fonts(): return { - "logo": _font("DejaVuSans-Bold.ttf", 44), - "big": _font("DejaVuSans-Bold.ttf", 240), - "sub": _font("DejaVuSans.ttf", 40), - "small": _font("DejaVuSans.ttf", 28), - "badge": _font("DejaVuSans-Bold.ttf", 30), + "logo": _font("DejaVuSans-Bold.ttf", 40), + "domain": _font("DejaVuSans.ttf", 26), + "big": _font("DejaVuSans-Bold.ttf", 250), + "sub": _font("DejaVuSans-Bold.ttf", 42), + "small": _font("DejaVuSans.ttf", 26), + "badge": _font("DejaVuSans-Bold.ttf", 28), + "url": _font("DejaVuSans-Bold.ttf", 32), } @@ -51,6 +59,38 @@ draw.text(((W - w) / 2, y), text, font=font, fill=fill) +def _bell_strip(d, iq, y, h): + """Bell-curve strip: subtle filled gaussian + accent marker at iq.""" + x0, x1 = 200, W - 200 + lo, hi = 55.0, 145.0 + mu, sd = 100.0, 15.0 + + def fx(iqv): + t = (iqv - lo) / (hi - lo) + return x0 + t * (x1 - x0) + + def gauss(iqv): + return ((iqv - mu) / sd) ** 2 + + g_edge = gauss(lo) # 9.0 — density at strip edges + pts = [(x0, y + h)] + steps = 120 + for i in range(steps + 1): + iqv = lo + (hi - lo) * i / steps + t = 1.0 - gauss(iqv) / g_edge # 1 at center, 0 at edges + pts.append((fx(iqv), y + h - t * (h - 6))) + pts.append((x1, y + h)) + d.polygon(pts, fill=CURVE_FILL) + + # baseline + d.line([x0, y + h, x1, y + h], fill=LINE, width=2) + + # marker at user's iq + mx = fx(max(lo, min(hi, iq))) + d.line([mx, y - 6, mx, y + h], fill=ACCENT, width=5) + d.polygon([(mx - 9, y - 16), (mx + 9, y - 16), (mx, y - 4)], fill=ACCENT) + + def generate_card(iq, percentile, signature_valid, flagged, result_id): """Render the OG card, return path to cached PNG.""" try: @@ -60,7 +100,7 @@ import tempfile cache = Path(tempfile.gettempdir()) / "veriq_og" cache.mkdir(parents=True, exist_ok=True) - out = cache / (result_id + ".png") + out = cache / (DESIGN + "-" + result_id + ".png") if out.exists(): return out @@ -76,20 +116,26 @@ d.rectangle([W - 9 - 56, H - 9 - 10, W - 9, H - 9], fill=ACCENT) d.rectangle([W - 9 - 10, H - 9 - 56, W - 9, H - 9], fill=ACCENT) - # logo - d.text((48, 44), "Ver", font=f["logo"], fill=TEXT) + # logo top-left, domain top-right + d.text((48, 40), "Ver", font=f["logo"], fill=TEXT) logo_w = d.textbbox((0, 0), "Ver", font=f["logo"])[2] - d.text((48 + logo_w, 44), "IQ", font=f["logo"], fill=ACCENT) + d.text((48 + logo_w, 40), "IQ", font=f["logo"], fill=ACCENT) + dom = "veriq.gnexus.space" + dbbox = d.textbbox((0, 0), dom, font=f["domain"]) + d.text((W - 48 - (dbbox[2] - dbbox[0]), 48), dom, font=f["domain"], fill=MUTED) - # big score - _center(d, str(iq), 130, f["big"], ACCENT) + # huge score — the hero + _center(d, str(iq), 88, f["big"], TEXT) # percentile line - _center(d, "higher than " + str(percentile) + "% of test takers", 400, - f["sub"], TEXT) + _center(d, "higher than " + str(percentile) + "% of test takers", 352, + f["sub"], ACCENT) + + # bell-curve strip with marker + _bell_strip(d, iq, 418, 74) # badges row - y = 480 + y = 516 if signature_valid: badge_text = "SIGNATURE VALID" badge_color = GOOD @@ -99,18 +145,17 @@ bbox = d.textbbox((0, 0), badge_text, font=f["badge"]) bw = bbox[2] - bbox[0] + 48 bx = (W - bw) / 2 - d.rectangle([bx, y, bx + bw, y + 56], outline=badge_color, width=3) - d.text((bx + 24, y + 10), badge_text, font=f["badge"], fill=badge_color) + d.rectangle([bx, y, bx + bw, y + 52], outline=badge_color, width=3) + d.text((bx + 24, y + 9), badge_text, font=f["badge"], fill=badge_color) if flagged: fx = bx + bw + 16 fb = d.textbbox((0, 0), "FLAGGED", font=f["badge"]) fw = fb[2] - fb[0] + 40 - d.rectangle([fx, y, fx + fw, y + 56], outline=WARN, width=3) - d.text((fx + 20, y + 10), "FLAGGED", font=f["badge"], fill=WARN) + d.rectangle([fx, y, fx + fw, y + 52], outline=WARN, width=3) + d.text((fx + 20, y + 9), "FLAGGED", font=f["badge"], fill=WARN) - # footer - _center(d, "veriq · verify at /v/" + result_id + " · signed with Ed25519", - 560, f["small"], MUTED) + # verify URL — the call to action + _center(d, "veriq.gnexus.space/v/" + result_id, 582, f["url"], TEXT) img.save(out, "PNG", optimize=True) - return out + return out \ No newline at end of file diff --git a/frontend/assets/result.js b/frontend/assets/result.js index d15d250..e67933e 100644 --- a/frontend/assets/result.js +++ b/frontend/assets/result.js @@ -98,13 +98,14 @@ }); - var msg = "My VerIQ score: " + d.iq + - " (higher than " + pct + "%). Verify it: " + link; + var msg = "I scored " + d.iq + " on VerIQ — higher than " + pct + + "% of test takers. Beat me: " + link; $("shareX").href = "https://twitter.com/intent/tweet?text=" + encodeURIComponent(msg); $("shareTg").href = "https://t.me/share/url?url=" + encodeURIComponent(link) + "&text=" + - encodeURIComponent("My VerIQ score: " + d.iq + " — verify it yourself"); + encodeURIComponent("I scored " + d.iq + " on VerIQ — higher than " + pct + + "% of test takers. Beat me:"); }).catch(function (e) { showError("Could not load result: " + e.message); });