From 2e7e5823cc92c831ec01618aeb8e79af6ab2a2a7 Mon Sep 17 00:00:00 2001 From: AliReza Date: Thu, 15 Oct 2026 13:59:03 +0330 Subject: [PATCH] lineFit runs before first paint (no 150ms debounce): no flash of unfitted text Co-Authored-By: Claude Sonnet 5.5 --- src/lib/lineFit.ts | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/lib/lineFit.ts b/src/lib/lineFit.ts index b014651..b430607 100644 --- a/src/lib/lineFit.ts +++ b/src/lib/lineFit.ts @@ -20,8 +20,6 @@ const MIN_CHARS = 14; const MAX_GAP = 2.2; const observed = new WeakSet(); -const queue = new Set(); -let frame = 0; /** پهنای طبیعیِ هر خط (با ترازِ start) از مستطیل‌های Range. */ function lineWidths(el: HTMLElement, fontSize: number): number[] { @@ -86,25 +84,26 @@ export function fit(el: HTMLElement) { el.style.wordSpacing = best ? `${best}em` : ''; } -function schedule(el: HTMLElement) { - queue.add(el); - if (frame) return; - frame = window.setTimeout(() => { - frame = 0; - const batch = [...queue]; - queue.clear(); - for (const el of batch) fit(el); - }, 150); -} +// همه‌ی اجراها همزمان و پیش از اولین نقاشیِ صفحه‌اند (MutationObserver/ResizeObserver پیش از paint +// صدا زده می‌شوند)، پس متنِ خام و اصلاح‌نشده هرگز دیده نمی‌شود؛ تأخیرِ قبلی (۱۵۰ms) یک چشمک می‌ساخت. +const lastWidth = new WeakMap(); function watch(el: HTMLElement) { if (observed.has(el)) return; observed.add(el); resizer.observe(el); + fit(el); } const resizer = new ResizeObserver((entries) => { - for (const e of entries) schedule(e.target as HTMLElement); + for (const e of entries) { + const el = e.target as HTMLElement; + const width = e.contentRect.width; + // تغییرِ ارتفاعِ خودِ fit نباید دوباره fit را راه بیندازد + if (lastWidth.get(el) === width) continue; + lastWidth.set(el, width); + fit(el); + } }); function scan(node: ParentNode) { @@ -116,12 +115,14 @@ export function startLineFit() { const root = document.getElementById('root')!; scan(root); new MutationObserver((records) => { + const changed = new Set(); for (const r of records) { r.addedNodes.forEach((n) => { if (n instanceof HTMLElement) scan(n); }); - // متنِ داخلِ پاراگراف عوض شده (مثلاً پاسخِ در حالِ پخش) + // متنِ داخلِ پاراگراف عوض شده (سوالِ بعدی، پاسخِ در حالِ پخش) const host = (r.target instanceof HTMLElement ? r.target : r.target.parentElement)?.closest(SELECTOR); - if (host) schedule(host); + if (host) changed.add(host); } + changed.forEach(fit); }).observe(root, { childList: true, subtree: true, characterData: true }); - void document.fonts.ready.then(() => document.querySelectorAll(SELECTOR).forEach(schedule)); + void document.fonts.ready.then(() => document.querySelectorAll(SELECTOR).forEach(fit)); }