// Spritz ORP algorithm - position where the eye naturally fixates. // Based on Optimal Viewing Position research (20-35% from left). export function getORPIndex(wordLength: number): number { if (wordLength <= 0) return 0; if (wordLength === 1) return 0; // 1 char: 1st letter if (wordLength <= 5) return 1; // 2-5 chars: 2nd letter if (wordLength <= 9) return 2; // 6-9 chars: 3rd letter if (wordLength <= 13) return 3; // 10-13 chars: 4th letter return 4; // 14+ chars: 5th letter } function stripTrailingClosers(s: string): string { // Handle punctuation followed by quotes/brackets like `word."` or `word.)`. return s.replace(/[)"'\]]+$/g, ""); } export function getTimingMultiplier(text: string, adaptiveTiming: boolean): number { if (!adaptiveTiming) return 1; const tokens = text.trim().split(/\s+/).filter(Boolean); if (tokens.length === 0) return 1; // Length-based slowdown: use the longest token in the display chunk. let maxLen = 0; for (const t of tokens) maxLen = Math.max(maxLen, t.length); let multiplier = 1 + Math.sqrt(maxLen) * 0.04; // Punctuation-based pauses: based on the last token. const last = stripTrailingClosers(tokens[tokens.length - 1]); if (/[.!?]$/.test(last)) { multiplier = Math.max(multiplier, 2.5); } else if (/[,;:]$/.test(last)) { multiplier = Math.max(multiplier, 1.8); } return multiplier; } export function getTimingMultiplierForWords( words: string[], startIndex: number, endIndex: number, adaptiveTiming: boolean, ): number { if (!adaptiveTiming) return 1; if (endIndex <= startIndex) return 1; let maxLen = 0; for (let i = startIndex; i < endIndex; i++) { const w = words[i]; if (w && w.length > maxLen) maxLen = w.length; } let multiplier = 1 + Math.sqrt(maxLen) * 0.04; const lastToken = words[endIndex - 1] || ""; const last = stripTrailingClosers(lastToken); if (/[.!?]$/.test(last)) { multiplier = Math.max(multiplier, 2.5); } else if (/[,;:]$/.test(last)) { multiplier = Math.max(multiplier, 1.8); } return multiplier; } export function getWordDelay( text: string, baseDelayMs: number, adaptiveTiming: boolean, ): number { return baseDelayMs * getTimingMultiplier(text, adaptiveTiming); } export function getWordDelayForWords( words: string[], startIndex: number, endIndex: number, baseDelayMs: number, adaptiveTiming: boolean, ): number { return ( baseDelayMs * getTimingMultiplierForWords(words, startIndex, endIndex, adaptiveTiming) ); } export function formatReadingTime(minutes: number): string { minutes = Math.round(minutes); if (minutes < 1) return "< 1 min"; if (minutes < 60) return `${Math.round(minutes)} min`; const hours = Math.floor(minutes / 60); const mins = Math.round(minutes % 60); if (mins === 0) return `${hours}h`; return `${hours}h ${mins}m`; }