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/blue-professional/ caption-skin.html
HTML · 223 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 soft cobalt-tinted card on warm cream — a 4% cobalt fill inside a 1.5px
23 20%-cobalt border, soft 14px rounded corners, NO shadow (the system's premium signal
24 is the absence of cast depth; the tinted card does the lift). Space Grotesk, sentence
25 case, near-black glyphs. Upcoming words sit in muted gray; the current word becomes a
26 solid cobalt chip — the brand's signature single-accent gesture, a pill-rounded fill
27 with cream reading on top; spoken words settle to near-black ink, chip 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 tinted card — 4% cobalt fill, 1.5px 20%-cobalt border, soft 14px corners,
69 NO shadow (the tinted card does the lift; cast depth is forbidden) */
70 .caption-pill {
71 max-width : 78 % ;
72 padding : 20 px 44 px 24 px ;
73 background : color-mix ( in srgb , var ( --cap-accent , #1e2bfa ) 4 % , var ( --cap-canvas , #fdfae7 ));
74 border : 1.5 px solid color-mix ( in srgb , var ( --cap-accent , #1e2bfa ) 20 % , transparent );
75 border-radius : 14 px ;
76 }
77 .caption-line {
78 display : flex ;
79 flex-wrap : wrap ;
80 justify-content : center ;
81 gap : 0.1 em 0.34 em ;
82 font-family : var ( --font-display , "Space Grotesk" ), system-ui , sans-serif ;
83 font-weight : 600 ;
84 font-size : clamp ( 40 px , 4.4 vw , 58 px );
85 line-height : 1.16 ;
86 letter-spacing : -0.02 em ;
87 }
88
89 /* upcoming — muted gray, no chip */
90 .caption-word {
91 display : inline-block ;
92 padding : 0 0.18 em ;
93 border-radius : 0.32 em ;
94 color : color-mix ( in srgb , var ( --cap-ink , #111111 ) 52 % , var ( --cap-canvas , #fdfae7 ));
95 }
96 /* current word — THE solid cobalt chip (the single-accent gesture; pill-rounded
97 fill, NO shadow), cream reading on top */
98 .caption-word.is-active {
99 color : var ( --cap-canvas , #fdfae7 );
100 background : var ( --cap-accent , #1e2bfa );
101 box-shadow : none ;
102 }
103 /* already spoken — solid near-black ink, chip cleared */
104 .caption-word.is-spoken {
105 color : var ( --cap-ink , #111111 );
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 — cobalt, the system's one accent on a key word */
112 .caption-word.cap-brand {
113 color : var ( --cap-accent , #1e2bfa );
114 }
115 /* numerals / data — cobalt 700, the figure voice */
116 .caption-word.cap-num {
117 font-family : var ( --font-display , "Space Grotesk" ), system-ui , sans-serif ;
118 font-weight : 700 ;
119 color : var ( --cap-accent , #1e2bfa );
120 }
121 /* emphasis — a directional-red hairline underline (inline only, never a fill) */
122 .caption-word.cap-cta {
123 text-decoration : underline ;
124 text-decoration-thickness : 2 px ;
125 text-decoration-color : var ( --cap-accent-2 , #dc2626 );
126 text-underline-offset : 0.1 em ;
127 }
128
129 @media ( max-aspect-ratio : 9 / 16 ) {
130 .caption-pill {
131 max-width : 92 % ;
132 }
133 .caption-line {
134 font-size : clamp ( 34 px , 5.4 vw , 48 px );
135 }
136 }
137 </ style >
138
139 < div
140 id = "captions-root"
141 data-composition-id = "captions"
142 data-timeline-locked
143 data-start = "0"
144 data-duration = "0"
145 data-fps = "30"
146 data-width = "0"
147 data-height = "0"
148 >
149 < div class = "caption-layer" aria-hidden = "true" >
150 < div id = "caption-stage" class = "caption-stage" ></ div >
151 </ div >
152 </ div >
153
154 < script >
155 // captions.mjs injects the engine's scene-aware groups + real duration here.
156 var GROUPS = [];
157 var DURATION = 0 ;
158
159 ( function () {
160 var stage = document. getElementById ( "caption-stage" );
161
162 // build DOM: one .caption-group per group, .caption-word per word
163 GROUPS . forEach ( function ( group , g ) {
164 var groupEl = document. createElement ( "div" );
165 groupEl.className = "caption-group" ;
166 groupEl.id = "caption-group-" + g;
167 var pill = document. createElement ( "div" );
168 pill.className = "caption-pill" ;
169 var line = document. createElement ( "div" );
170 line.className = "caption-line" ;
171 (group.words || []). forEach ( function ( w , i ) {
172 var span = document. createElement ( "span" );
173 span.className = "caption-word" ;
174 span.id = "caption-word-" + g + "-" + i;
175 span.textContent = String (w.text);
176 line. appendChild (span);
177 });
178 pill. appendChild (line);
179 groupEl. appendChild (pill);
180 stage. appendChild (groupEl);
181 });
182
183 window.__timelines = window.__timelines || {};
184 var tl = gsap. timeline ({ paused: true });
185
186 GROUPS . forEach ( function ( group , g ) {
187 var groupEl = document. getElementById ( "caption-group-" + g);
188 var words = group.words || [];
189 var next = GROUPS [g + 1 ];
190 var isLast = g === GROUPS . length - 1 ;
191 var start = Math. max ( 0 , Number (group.start));
192 var end = isLast ? DURATION : Math. min ( Number (next.start), Number (group.end) + 0.3 );
193 if (end <= start) end = start + 0.01 ;
194
195 // group on/off — exactly one group visible at a time
196 tl. set (groupEl, { opacity: 1 }, start);
197 tl. set (groupEl, { opacity: 0 }, end);
198
199 // 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
200 // engine's frame-by-frame seek; .call callbacks would NOT fire).
201 words. forEach ( function ( w , i ) {
202 var el = document. getElementById ( "caption-word-" + g + "-" + i);
203 var at = Math. max (start, Number (w.start));
204 tl. set (el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
205 tl. set (el, { className: "caption-word is-active" }, at); // current
206 tl. fromTo (el, { scale: 0.985 }, { scale: 1 , duration: 0.18 , ease: "power1.out" }, at);
207 if (i + 1 < words. length ) {
208 var nextAt = Math. max (start, Number (words[i + 1 ].start));
209 tl. set (el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
210 }
211 });
212 if (words. length ) {
213 var lastEl = document. getElementById ( "caption-word-" + g + "-" + (words. length - 1 ));
214 var lastSpoken = Math. min (end, Number (words[words. length - 1 ].end) + 0.1 );
215 tl. set (lastEl, { className: "caption-word is-spoken" }, lastSpoken);
216 }
217 });
218
219 // full-span anchor so the sub-comp timeline spans the whole video
220 tl. to ({}, { duration: DURATION }, 0 );
221 window.__timelines[ "captions" ] = tl;
222 })();
223 </ script >