Setting the file. One moment.
Fit Fonts · Embedded Captions · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page Hershey Script1
Script Theme
scripts/ fit-fonts.cjs
JavaScript · 158 lines · 6 KB
13 const fs = require ( "fs" );
14
15 // rough per-family advance (em per char, caps-ish). Condensed faces are narrow; pixel
16 // faces wide. Default is conservative. (Estimate only — gate confirms.)
17 const ADV = {
18 anton: 0.44 ,
19 oswald: 0.45 ,
20 teko: 0.4 ,
21 "saira stencil one" : 0.46 ,
22 "bebas neue" : 0.4 ,
23 "press start 2p" : 1.05 ,
24 vt323: 0.52 ,
25 monoton: 0.62 ,
26 "special elite" : 0.55 ,
27 "jetbrains mono" : 0.6 ,
28 "space mono" : 0.6 ,
29 "ibm plex mono" : 0.6 ,
30 "source code pro" : 0.6 ,
31 "archivo black" : 0.62 ,
32 bangers: 0.5 ,
33 "bodoni moda" : 0.5 ,
34 "playfair display" : 0.52 ,
35 cinzel: 0.62 ,
36 "cormorant garamond" : 0.45 ,
37 fredoka: 0.55 ,
38 "baloo 2" : 0.55 ,
39 "permanent marker" : 0.55 ,
40 caveat: 0.4 ,
41 orbitron: 0.66 ,
42 sora: 0.54 ,
43 "space grotesk" : 0.54 ,
44 };
45 const DEFAULT_ADV = 0.56 ;
46
47 const cssVal = ( css , prop ) => {
48 const m = String (css || "" ). match ( new RegExp (prop + " \\ s*: \\ s*([^;}]+)" , "i" ));
49 return m ? m[ 1 ]. trim () : null ;
50 };
51 function familyOf ( css ) {
52 const v = cssVal (css, "font-family" );
53 if ( ! v) return "inter" ;
54 return v
55 . split ( "," )[ 0 ]
56 . trim ()
57 . replace ( / ^ ['"] | ['"] $ / g , "" )
58 . toLowerCase ();
59 }
60 function sizePxOf ( css , H ) {
61 const v = cssVal (css, "font-size" );
62 if ( ! v) return null ;
63 let m;
64 if ((m = v. match ( /calc \( \s * ( [\d.] + ) \s * \* \s * var \( --h \) \s * \) / i ))) return parseFloat (m[ 1 ]) * H ;
65 if ((m = v. match ( /( [\d.] + ) \s * cqh/ i ))) return ( parseFloat (m[ 1 ]) / 100 ) * H ;
66 if ((m = v. match ( /( [\d.] + ) \s * vh/ i ))) return ( parseFloat (m[ 1 ]) / 100 ) * H ;
67 if ((m = v. match ( /( [\d.] + ) \s * px/ i ))) return parseFloat (m[ 1 ]);
68 return null ;
69 }
70 function planeWidthPx ( plan , plane , W ) {
71 const p = plan.planes && plan.planes[plane];
72 const css = p && ( typeof p === "string" ? p : p.css);
73 const v = cssVal (css, "width" );
74 if ( ! v) return null ;
75 let m;
76 if ((m = v. match ( /( [\d.] + ) \s * %/ ))) return ( parseFloat (m[ 1 ]) / 100 ) * W ;
77 if ((m = v. match ( /( [\d.] + ) \s * px/ ))) return parseFloat (m[ 1 ]);
78 return null ;
79 }
80 function setFontSize ( css , fracOfH ) {
81 const decl = `font-size: calc(${ fracOfH . toFixed ( 3 ) } * var(--h))` ;
82 if ( /font-size \s * :/ i . test (css || "" )) return String (css). replace ( /font-size \s * : \s * [ ^ ;}] + / i , decl);
83 return (css ? css. replace ( / \s *$ / , "" ). replace ( /; ? \s *$ / , "; " ) : "" ) + decl + ";" ;
84 }
85
86 function main () {
87 const project = path. resolve (process.argv[ 2 ] || "" );
88 if ( ! process.argv[ 2 ]) {
89 console. error ( "usage: fit-fonts.cjs <project-dir>" );
90 process. exit ( 1 );
91 }
92 const planPath = path. join (project, "plan.json" );
93 let plan;
94 try {
95 plan = JSON . parse (fs. readFileSync (planPath, "utf8" ));
96 } catch {
97 console. error ( "[fit-fonts] no plan.json (Cinematic only) — skipping" );
98 process. exit ( 0 );
99 }
100 const W = plan.width || 1920 ,
101 H = plan.height || 1080 ;
102 const groups = (plan.groups || []). concat (plan.crown_group ? [plan.crown_group] : []);
103 const MARGIN = 0.92 ; // leave 8% breathing room inside the box
104 const HERO_MIN_FRAC = 0.18 ; // a hero below ~0.18·h isn't "big" — keep it punchy, widen its box instead
105
106 let changed = 0 ;
107 for ( const g of groups) {
108 const text = (g.words || []). map (( w ) => w.text). join ( " " );
109 const nChars = text. replace ( / \s / g , "" ). length + (g.words || []). length * 0.5 ; // glyphs + ~half-space gaps
110 if (nChars < 1 ) continue ;
111 const fam = familyOf (g.css);
112 let adv = ADV [fam] ?? DEFAULT_ADV ;
113 if ( /text-transform \s * : \s * uppercase/ i . test (g.css || "" ) || text === text. toUpperCase ())
114 adv *= 1.05 ;
115 const sizePx = sizePxOf (g.css, H );
116 if ( ! sizePx) continue ; // no explicit size → template default handles it
117 const boxW = (g.plane ? planeWidthPx (plan, g.plane, W ) : null ) || W ;
118 const usable = boxW * MARGIN ;
119 // A WRAPPING group only overflows if a SINGLE word is wider than the box (the rest
120 // wraps). A NOWRAP group must fit its whole line. So don't shrink wrapping narration
121 // down to one line — only force the whole line for nowrap; else just the longest word.
122 const nowrap = /white-space \s * : \s * nowrap/ i . test (g.css || "" );
123 const longestWord = (g.words || []). reduce (
124 ( m , w ) => Math. max (m, String (w.text || "" ). replace ( / \s / g , "" ). length ),
125 0 ,
126 );
127 const constrainChars = nowrap ? nChars : longestWord;
128 if (constrainChars < 1 ) continue ;
129 const estW = constrainChars * adv * sizePx;
130 if (estW > usable) {
131 const newPx = usable / (constrainChars * adv);
132 let frac = Math. max ( 0.02 , newPx / H );
133 const isHero = g.hero === true || / ^ (hero | crown) $ / i . test (g.plane || "" );
134 if (isHero && frac < HERO_MIN_FRAC ) {
135 // The hero must stay BIG. If it won't fit its box while staying punchy, the BOX is
136 // too narrow (the hero should span the subject) — keep it at the floor + flag it,
137 // rather than silently shrinking the peak into a small word.
138 frac = HERO_MIN_FRAC ;
139 g.css = setFontSize (g.css, frac);
140 changed ++ ;
141 console. log (
142 `[fit-fonts] ⚠ ${ g . id || "(group)"} HERO "${ text . slice ( 0 , 28 ) }" won't fit box ${ Math . round ( usable ) }px while staying big → KEPT at floor ${ frac }·h. WIDEN the hero plane (it should span the subject, centered), not shrink the peak.` ,
143 );
144 continue ;
145 }
146 g.css = setFontSize (g.css, frac);
147 changed ++ ;
148 console. log (
149 `[fit-fonts] ${ g . id || "(group)"}: "${ text . slice ( 0 , 28 ) }" (${ nowrap ? "nowrap line" : "longest word"}) est ${ Math . round ( estW ) }px > box ${ Math . round ( usable ) }px → shrink to ${ frac . toFixed ( 3 ) }·h (${ Math . round ( newPx ) }px)` ,
150 );
151 }
152 }
153 if (changed) {
154 fs. writeFileSync (planPath, JSON . stringify (plan, null , 2 ));
155 console. log ( `[fit-fonts] shrank ${ changed } group(s) to fit; wrote plan.json` );
156 } else console. log ( "[fit-fonts] all groups fit their box — no change" );
157 }
158 main ();