Setting the file. One moment.
Caption Skin · Hyperframes Creative · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page 22.10
Narration
(opens in a new tab)
frame-presets/bold-poster/ caption-skin.html
HTML · 230 lines · 9 KB
15
--cap-ink · --cap-canvas · --cap-accent · --cap-accent-2 · --font-display ·
16 --font-body · --cap-band-top · --cap-band-height
17 Hooks (do not rename): .caption-group / .caption-word + states .is-active / .is-spoken,
18 root data-composition-id="captions", timeline at window.__timelines["captions"].
19 State changes use gsap.set({className}) (applied on the engine's frame-by-frame seek);
20 never tl.call() callbacks (seek does not fire them → wrong state on render).
21
22 Visual: a fresh-paper card with bold poster framing — a heavy 1.5px brown-black ink
23 rule plus a 4px tomato-red leftbar (the brand's cantilevered-card signature), SQUARE
24 corners, NO drop shadow (the system's only shadow is the stacked text-shadow on red
25 display). Serif body face, ink glyphs. Upcoming words sit in faint ink; the current
26 word slams into a tomato-red panel (the brand's signature surface inversion, a hard
27 square block on paper-white reading, no blur) with the stacked text-shadow on it;
28 spoken words settle to solid ink, panel cleared.
29 -->
30 < script src = "https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js" ></ script >
31
32 <!-- captions.mjs injects the project's :root tokens here -->
33 < style data-brand-tokens ></ style >
34
35 < style >
36 #captions-root {
37 position : absolute ;
38 inset : 0 ;
39 pointer-events : none ;
40 }
41 .caption-layer {
42 position : absolute ;
43 inset : 0 ;
44 z-index : 20 ;
45 pointer-events : none ;
46 }
47 /* the keep-out band, from captionBand(H) — frame content stays above it */
48 .caption-stage {
49 position : absolute ;
50 left : 0 ;
51 right : 0 ;
52 top : var ( --cap-band-top , 900 px );
53 height : var ( --cap-band-height , 180 px );
54 display : flex ;
55 align-items : center ;
56 justify-content : center ;
57 }
58 .caption-group {
59 /* every group is absolutely stacked over the SAME band — exactly one shown at a
60 time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
61 position : absolute ;
62 inset : 0 ;
63 display : flex ;
64 align-items : center ;
65 justify-content : center ;
66 opacity : 0 ;
67 }
68
69 /* THE paper card — heavy 1.5px brown-black ink rule + a 4px tomato-red leftbar
70 (bold poster's cantilevered red-leftbar-card signature), square corners, NO
71 drop shadow (the only shadow in the system is the stacked text-shadow on red) */
72 .caption-pill {
73 max-width : 78 % ;
74 padding : 20 px 44 px 24 px ;
75 background : var ( --cap-canvas , #ffffff );
76 border : 1.5 px solid var ( --cap-ink , #1c1410 );
77 border-left : 4 px solid var ( --cap-accent , #d8000f );
78 border-radius : 0 ;
79 }
80 .caption-line {
81 display : flex ;
82 flex-wrap : wrap ;
83 justify-content : center ;
84 gap : 0.1 em 0.34 em ;
85 font-family : var ( --font-display , "Shrikhand" ), Georgia , serif ;
86 font-weight : 400 ;
87 font-size : clamp ( 40 px , 4.4 vw , 58 px );
88 line-height : 1.16 ;
89 letter-spacing : -0.01 em ;
90 }
91
92 /* upcoming — faint brown-black ink, no panel */
93 .caption-word {
94 display : inline-block ;
95 padding : 0 0.06 em ;
96 color : color-mix ( in srgb , var ( --cap-ink , #1c1410 ) 42 % , var ( --cap-canvas , #ffffff ));
97 }
98 /* current word — THE tomato-red panel (box-shadow spread = a hard square block
99 w/o layout shift; no blur), paper-white reading on top carrying the stacked
100 text-shadow — bold poster's surface-inversion + only-shadow signatures */
101 .caption-word.is-active {
102 color : var ( --cap-canvas , #ffffff );
103 background : var ( --cap-accent , #d8000f );
104 box-shadow : 0 0 0 0.06 em var ( --cap-accent , #d8000f );
105 text-shadow :
106 0.03 em 0.03 em 0 rgba ( 28 , 20 , 16 , 0.25 ),
107 0.06 em 0.06 em 0 rgba ( 28 , 20 , 16 , 0.2 ),
108 0.09 em 0.09 em 0 rgba ( 28 , 20 , 16 , 0.15 );
109 }
110 /* already spoken — solid brown-black ink, panel cleared */
111 .caption-word.is-spoken {
112 color : var ( --cap-ink , #1c1410 );
113 background : transparent ;
114 box-shadow : none ;
115 text-shadow : none ;
116 }
117
118 /* optional per-word accents (dormant until a word-classer tags them) */
119 /* brand / key term — Shrikhand display, bold poster's display face inflection */
120 .caption-word.cap-brand {
121 font-family : var ( --font-display , "Shrikhand" ), Georgia , serif ;
122 }
123 /* numerals / data — the mono chrome voice, slightly recessed */
124 .caption-word.cap-num {
125 font-family : var ( --font-body , "Libre Baskerville" ), ui-monospace , monospace ;
126 color : color-mix ( in srgb , var ( --cap-ink , #1c1410 ) 72 % , var ( --cap-canvas , #ffffff ));
127 }
128 /* emphasis — a tomato-red em-dash-weight underline (the lone accent) */
129 .caption-word.cap-cta {
130 text-decoration : underline ;
131 text-decoration-thickness : 2 px ;
132 text-decoration-color : var ( --cap-accent-2 , #f5f2ef );
133 text-underline-offset : 0.1 em ;
134 }
135
136 @media ( max-aspect-ratio : 9 / 16 ) {
137 .caption-pill {
138 max-width : 92 % ;
139 }
140 .caption-line {
141 font-size : clamp ( 34 px , 5.4 vw , 48 px );
142 }
143 }
144 </ style >
145
146 < div
147 id = "captions-root"
148 data-composition-id = "captions"
149 data-timeline-locked
150 data-start = "0"
151 data-duration = "0"
152 data-fps = "30"
153 data-width = "0"
154 data-height = "0"
155 >
156 < div class = "caption-layer" aria-hidden = "true" >
157 < div id = "caption-stage" class = "caption-stage" ></ div >
158 </ div >
159 </ div >
160
161 < script >
162 // captions.mjs injects the engine's scene-aware groups + real duration here.
163 var GROUPS = [];
164 var DURATION = 0 ;
165
166 ( function () {
167 var stage = document. getElementById ( "caption-stage" );
168
169 // build DOM: one .caption-group per group, .caption-word per word
170 GROUPS . forEach ( function ( group , g ) {
171 var groupEl = document. createElement ( "div" );
172 groupEl.className = "caption-group" ;
173 groupEl.id = "caption-group-" + g;
174 var pill = document. createElement ( "div" );
175 pill.className = "caption-pill" ;
176 var line = document. createElement ( "div" );
177 line.className = "caption-line" ;
178 (group.words || []). forEach ( function ( w , i ) {
179 var span = document. createElement ( "span" );
180 span.className = "caption-word" ;
181 span.id = "caption-word-" + g + "-" + i;
182 span.textContent = String (w.text);
183 line. appendChild (span);
184 });
185 pill. appendChild (line);
186 groupEl. appendChild (pill);
187 stage. appendChild (groupEl);
188 });
189
190 window.__timelines = window.__timelines || {};
191 var tl = gsap. timeline ({ paused: true });
192
193 GROUPS . forEach ( function ( group , g ) {
194 var groupEl = document. getElementById ( "caption-group-" + g);
195 var words = group.words || [];
196 var next = GROUPS [g + 1 ];
197 var isLast = g === GROUPS . length - 1 ;
198 var start = Math. max ( 0 , Number (group.start));
199 var end = isLast ? DURATION : Math. min ( Number (next.start), Number (group.end) + 0.3 );
200 if (end <= start) end = start + 0.01 ;
201
202 // group on/off — exactly one group visible at a time
203 tl. set (groupEl, { opacity: 1 }, start);
204 tl. set (groupEl, { opacity: 0 }, end);
205
206 // 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
207 // engine's frame-by-frame seek; .call callbacks would NOT fire).
208 words. forEach ( function ( w , i ) {
209 var el = document. getElementById ( "caption-word-" + g + "-" + i);
210 var at = Math. max (start, Number (w.start));
211 tl. set (el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
212 tl. set (el, { className: "caption-word is-active" }, at); // current
213 tl. fromTo (el, { scale: 0.9 }, { scale: 1 , duration: 0.16 , ease: "back.out(2.2)" }, at);
214 if (i + 1 < words. length ) {
215 var nextAt = Math. max (start, Number (words[i + 1 ].start));
216 tl. set (el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
217 }
218 });
219 if (words. length ) {
220 var lastEl = document. getElementById ( "caption-word-" + g + "-" + (words. length - 1 ));
221 var lastSpoken = Math. min (end, Number (words[words. length - 1 ].end) + 0.1 );
222 tl. set (lastEl, { className: "caption-word is-spoken" }, lastSpoken);
223 }
224 });
225
226 // full-span anchor so the sub-comp timeline spans the whole video
227 tl. to ({}, { duration: DURATION }, 0 );
228 window.__timelines[ "captions" ] = tl;
229 })();
230 </ script >