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/broadside/ caption-skin.html
HTML · 225 lines · 8 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 protest-poster lower third — an ink-black plate on a single 1px fire-orange
23 hairline, SHARP corners, NO shadow (broadside's plane is flat, never cast). Massive
24 lowercase Barlow 900 as graphic primitive, cream glyphs on ink. Upcoming words sit in
25 faint cream; the current word flips register — a hard flat fire-orange block with ink
26 reading on top (broadside's signature two-register flip, no blur), the brand's lone
27 accent; spoken words settle to solid cream, the orange block cleared.
28 -->
29 < script src = "https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js" ></ script >
30
31 <!-- captions.mjs injects the project's :root tokens here -->
32 < style data-brand-tokens ></ style >
33
34 < style >
35 #captions-root {
36 position : absolute ;
37 inset : 0 ;
38 pointer-events : none ;
39 }
40 .caption-layer {
41 position : absolute ;
42 inset : 0 ;
43 z-index : 20 ;
44 pointer-events : none ;
45 }
46 /* the keep-out band, from captionBand(H) — frame content stays above it */
47 .caption-stage {
48 position : absolute ;
49 left : 0 ;
50 right : 0 ;
51 top : var ( --cap-band-top , 900 px );
52 height : var ( --cap-band-height , 180 px );
53 display : flex ;
54 align-items : center ;
55 justify-content : center ;
56 }
57 .caption-group {
58 /* every group is absolutely stacked over the SAME band — exactly one shown at a
59 time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
60 position : absolute ;
61 inset : 0 ;
62 display : flex ;
63 align-items : center ;
64 justify-content : center ;
65 opacity : 0 ;
66 }
67
68 /* THE ink-black plate — single 1px fire-orange hairline (broadside's lone accent),
69 SHARP corners (0 radius), NO shadow (the plane is flat) */
70 .caption-pill {
71 max-width : 80 % ;
72 padding : 18 px 44 px 22 px ;
73 background : var ( --cap-ink , #111111 );
74 border : 1 px solid var ( --cap-accent , #e85d26 );
75 border-radius : 0 ;
76 }
77 .caption-line {
78 display : flex ;
79 flex-wrap : wrap ;
80 justify-content : center ;
81 gap : 0.04 em 0.3 em ;
82 font-family : var ( --font-display , "Barlow" ), system-ui , sans-serif ;
83 font-weight : 900 ;
84 font-size : clamp ( 38 px , 4.2 vw , 56 px );
85 line-height : 1.02 ;
86 letter-spacing : -0.03 em ;
87 text-transform : lowercase ;
88 }
89
90 /* upcoming — faint cream, no block (massive lowercase Barlow 900 as graphic primitive) */
91 .caption-word {
92 display : inline-block ;
93 padding : 0 0.08 em ;
94 color : color-mix ( in srgb , var ( --cap-canvas , #f0ece5 ) 42 % , var ( --cap-ink , #111111 ));
95 }
96 /* current word — THE register flip: a hard flat fire-orange block (box-shadow spread =
97 a sharp rectangle w/o layout shift; no blur), ink reading on top — the lone accent */
98 .caption-word.is-active {
99 color : var ( --cap-ink , #111111 );
100 background : var ( --cap-accent , #e85d26 );
101 box-shadow : 0 0 0 0.06 em var ( --cap-accent , #e85d26 );
102 }
103 /* already spoken — solid cream, the orange block cleared */
104 .caption-word.is-spoken {
105 color : var ( --cap-canvas , #f0ece5 );
106 background : transparent ;
107 box-shadow : none ;
108 }
109
110 /* optional per-word accents (dormant until a word-classer tags them) */
111 /* brand / key term — fire-orange ink, the cream-on-ink register's only inflection */
112 .caption-word.cap-brand {
113 color : var ( --cap-accent , #e85d26 );
114 }
115 /* numerals / data — the IBM Plex Mono chrome voice, uppercase + tracked, recessed */
116 .caption-word.cap-num {
117 font-family : var ( --font-body , "IBM Plex Mono" ), ui-monospace , monospace ;
118 font-weight : 500 ;
119 text-transform : uppercase ;
120 letter-spacing : 0.06 em ;
121 color : color-mix ( in srgb , var ( --cap-canvas , #f0ece5 ) 72 % , var ( --cap-ink , #111111 ));
122 }
123 /* emphasis — a fire-orange hairline underline (no second hue; the lone accent again) */
124 .caption-word.cap-cta {
125 text-decoration : underline ;
126 text-decoration-thickness : 2 px ;
127 text-decoration-color : var ( --cap-accent-2 , #e85d26 );
128 text-underline-offset : 0.1 em ;
129 }
130
131 @media ( max-aspect-ratio : 9 / 16 ) {
132 .caption-pill {
133 max-width : 92 % ;
134 }
135 .caption-line {
136 font-size : clamp ( 32 px , 5.2 vw , 46 px );
137 }
138 }
139 </ style >
140
141 < div
142 id = "captions-root"
143 data-composition-id = "captions"
144 data-timeline-locked
145 data-start = "0"
146 data-duration = "0"
147 data-fps = "30"
148 data-width = "0"
149 data-height = "0"
150 >
151 < div class = "caption-layer" aria-hidden = "true" >
152 < div id = "caption-stage" class = "caption-stage" ></ div >
153 </ div >
154 </ div >
155
156 < script >
157 // captions.mjs injects the engine's scene-aware groups + real duration here.
158 var GROUPS = [];
159 var DURATION = 0 ;
160
161 ( function () {
162 var stage = document. getElementById ( "caption-stage" );
163
164 // build DOM: one .caption-group per group, .caption-word per word
165 GROUPS . forEach ( function ( group , g ) {
166 var groupEl = document. createElement ( "div" );
167 groupEl.className = "caption-group" ;
168 groupEl.id = "caption-group-" + g;
169 var pill = document. createElement ( "div" );
170 pill.className = "caption-pill" ;
171 var line = document. createElement ( "div" );
172 line.className = "caption-line" ;
173 (group.words || []). forEach ( function ( w , i ) {
174 var span = document. createElement ( "span" );
175 span.className = "caption-word" ;
176 span.id = "caption-word-" + g + "-" + i;
177 span.textContent = String (w.text);
178 line. appendChild (span);
179 });
180 pill. appendChild (line);
181 groupEl. appendChild (pill);
182 stage. appendChild (groupEl);
183 });
184
185 window.__timelines = window.__timelines || {};
186 var tl = gsap. timeline ({ paused: true });
187
188 GROUPS . forEach ( function ( group , g ) {
189 var groupEl = document. getElementById ( "caption-group-" + g);
190 var words = group.words || [];
191 var next = GROUPS [g + 1 ];
192 var isLast = g === GROUPS . length - 1 ;
193 var start = Math. max ( 0 , Number (group.start));
194 var end = isLast ? DURATION : Math. min ( Number (next.start), Number (group.end) + 0.3 );
195 if (end <= start) end = start + 0.01 ;
196
197 // group on/off — exactly one group visible at a time
198 tl. set (groupEl, { opacity: 1 }, start);
199 tl. set (groupEl, { opacity: 0 }, end);
200
201 // 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
202 // engine's frame-by-frame seek; .call callbacks would NOT fire).
203 words. forEach ( function ( w , i ) {
204 var el = document. getElementById ( "caption-word-" + g + "-" + i);
205 var at = Math. max (start, Number (w.start));
206 tl. set (el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
207 tl. set (el, { className: "caption-word is-active" }, at); // current
208 tl. fromTo (el, { scale: 0.92 }, { scale: 1 , duration: 0.1 , ease: "power3.out" }, at);
209 if (i + 1 < words. length ) {
210 var nextAt = Math. max (start, Number (words[i + 1 ].start));
211 tl. set (el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
212 }
213 });
214 if (words. length ) {
215 var lastEl = document. getElementById ( "caption-word-" + g + "-" + (words. length - 1 ));
216 var lastSpoken = Math. min (end, Number (words[words. length - 1 ].end) + 0.1 );
217 tl. set (lastEl, { className: "caption-word is-spoken" }, lastSpoken);
218 }
219 });
220
221 // full-span anchor so the sub-comp timeline spans the whole video
222 tl. to ({}, { duration: DURATION }, 0 );
223 window.__timelines[ "captions" ] = tl;
224 })();
225 </ script >