From 8b57c3a5f25ce1bcf6102bb596804e968c20e80f Mon Sep 17 00:00:00 2001 From: jsboige Date: Sun, 31 May 2026 12:23:30 +0200 Subject: [PATCH] fix(cardpen): auto-shrink overflowing card titles before capture (#316) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long non-breaking titles — notably RU Cyrillic compounds such as "НЕЙРОЛИНГВИСТИЧЕСКОЕ ПРОГРАММИРОВАНИЕ" — overflowed the .title box and were clipped mid-word, because the inner `.title > div` uses `overflow-wrap: normal`. This implements Option B from #303/#353 (auto-shrink JS), chosen over a RU-only CSS class so the fix is language- and template-agnostic. frame.js now runs autoFitCardTitles() on each in generateImages(), right after font preload and before the domtoimage capture. For each `.title` it binary-searches the largest font-size that fits horizontally (floor 8px). Titles that already fit are left untouched (no-op), so non-overflowing Latin titles are unaffected. Validated via Playwright on the real Fallacies face template + CSS: - "AD HOMINEM" (Latin): 84.5px unchanged (no-op) - "НЕЙРОЛИНГВИСТИЧЕСКОЕ ПРОГРАММИРОВАНИЕ": 84.5 -> 39.1px, now fits - "ПСЕВДОНАУЧНОСТЬ": 84.5 -> 51.7px, now fits Co-Authored-By: Claude Opus 4.6 --- Generation/CardPen/js/frame.js | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/Generation/CardPen/js/frame.js b/Generation/CardPen/js/frame.js index 326a35ba8..07e348967 100644 --- a/Generation/CardPen/js/frame.js +++ b/Generation/CardPen/js/frame.js @@ -5,6 +5,65 @@ var cards = []; var nodes = []; +// --------------------------------------------------------------------------- +// Auto-shrink des titres (Option B — issue #316) +// Réduit dynamiquement la taille de police d'un titre qui déborde son conteneur +// AVANT la capture domtoimage. No-op si le titre tient déjà (cas Latin courant) : +// n'agit que sur les débordements horizontaux (mots longs non sécables — p.ex. +// cyrillique RU "НЕЙРОЛИНГВИСТИЧЕСКОЕ ПРОГРАММИРОВАНИЕ" — qui, avec +// overflow-wrap:normal, sont rognés au lieu de revenir à la ligne). +// Indépendant de la langue et du gabarit : ne touche que ce qui déborde. +// --------------------------------------------------------------------------- + +// Vrai si le contenu de l'élément (et de ses descendants) tient sans débordement +// horizontal. Tolérance de 1px pour les arrondis sub-pixels. +function titleContentFits(el) { + if (el.scrollWidth > el.clientWidth + 1) { + return false; + } + var descendants = el.getElementsByTagName('*'); + for (var i = 0; i < descendants.length; i++) { + if (descendants[i].scrollWidth > descendants[i].clientWidth + 1) { + return false; + } + } + return true; +} + +// Recherche dichotomique de la plus grande taille de police qui tient. +function autoFitTitle(title, minFontPx) { + minFontPx = minFontPx || 8; + // Repartir de la taille définie par le template (idempotent si rappelé). + title.style.fontSize = ''; + var naturalPx = parseFloat(window.getComputedStyle(title).fontSize); + if (!naturalPx || titleContentFits(title)) { + return; // Tient déjà : aucun changement (cas le plus fréquent). + } + var lo = minFontPx; + var hi = naturalPx; + var best = minFontPx; + for (var iter = 0; iter < 12; iter++) { + var mid = (lo + hi) / 2; + title.style.fontSize = mid + 'px'; + if (titleContentFits(title)) { + best = mid; // tient → viser plus grand + lo = mid; + } else { + hi = mid; // déborde → réduire + } + } + title.style.fontSize = best + 'px'; +} + +// Applique l'auto-shrink à tous les titres d'une carte avant sa capture. +function autoFitCardTitles(cardNode) { + var titles = cardNode.querySelectorAll('.title'); + for (var t = 0; t < titles.length; t++) { + autoFitTitle(titles[t]); + } +} + + async function generateImages() { var zipButton = document.getElementById('zipButton'); if (zipButton) { @@ -19,6 +78,7 @@ async function generateImages() { nodes = document.getElementsByTagName("card"); for (var n = 0; n < nodes.length; n++) { //imaginer(nodes[n],n); + autoFitCardTitles(nodes[n]); // Option B (#316) : ajuster les titres débordants avant capture await imaginerSync(nodes[n], n); } if (zipButton) {