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/creative-mode/ caption-skin.html
HTML · 227 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 warm-cream card on a thick 4px ink border with a HARD offset shadow (a solid
23 same-direction ink+orange duplicate, no blur — creative-mode's only depth device),
24 square corners. Archivo Black, UPPERCASE, 0.92 line-height, ink glyphs. Upcoming words
25 sit in faint ink; the current word becomes a solid yellow inked block (the brand's
26 signature hard-offset-shadow accent block, no blur) with ink reading on top; spoken
27 words settle to solid ink, the accent 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 cream card — thick 4px ink border + the signature HARD offset shadow
69 (a solid same-direction orange+ink duplicate, no blur), square corners */
70 .caption-pill {
71 max-width : 80 % ;
72 padding : 18 px 40 px 22 px ;
73 background : var ( --cap-canvas , #efe9d9 );
74 border : 4 px solid var ( --cap-ink , #0f0f0f );
75 border-radius : 0 ;
76 box-shadow :
77 14 px 14 px 0 var ( --cap-accent-2 , #e85a1f ),
78 14 px 14 px 0 4 px var ( --cap-ink , #0f0f0f );
79 }
80 .caption-line {
81 display : flex ;
82 flex-wrap : wrap ;
83 justify-content : center ;
84 gap : 0.16 em 0.3 em ;
85 font-family : var ( --font-display , "Archivo Black" ), sans-serif ;
86 font-weight : 400 ;
87 text-transform : uppercase ;
88 font-size : clamp ( 34 px , 3.8 vw , 52 px );
89 line-height : 0.92 ;
90 letter-spacing : -0.01 em ;
91 }
92
93 /* upcoming — faint ink, no accent block */
94 .caption-word {
95 display : inline-block ;
96 padding : 0.02 em 0.1 em ;
97 color : color-mix ( in srgb , var ( --cap-ink , #0f0f0f ) 38 % , var ( --cap-canvas , #efe9d9 ));
98 }
99 /* current word — THE hard yellow inked block (box-shadow spread = a hard square
100 block w/o layout shift; no blur), ink reading on top */
101 .caption-word.is-active {
102 color : var ( --cap-ink , #0f0f0f );
103 background : var ( --cap-accent , #f5c518 );
104 box-shadow : 0 0 0 0.08 em var ( --cap-accent , #f5c518 );
105 }
106 /* already spoken — solid ink, accent block cleared */
107 .caption-word.is-spoken {
108 color : var ( --cap-ink , #0f0f0f );
109 background : transparent ;
110 box-shadow : none ;
111 }
112
113 /* optional per-word accents (dormant until a word-classer tags them) */
114 /* brand / key term — an inverted ink chip (the kicker-block voice) */
115 .caption-word.cap-brand {
116 color : var ( --cap-canvas , #efe9d9 );
117 background : var ( --cap-ink , #0f0f0f );
118 box-shadow : 0 0 0 0.08 em var ( --cap-ink , #0f0f0f );
119 }
120 /* numerals / data — the mono voice, slightly recessed */
121 .caption-word.cap-num {
122 font-family : var ( --font-body , "Space Grotesk" ), ui-monospace , monospace ;
123 color : color-mix ( in srgb , var ( --cap-ink , #0f0f0f ) 72 % , var ( --cap-canvas , #efe9d9 ));
124 }
125 /* emphasis — a thick orange underline (the counter accent) */
126 .caption-word.cap-cta {
127 text-decoration : underline ;
128 text-decoration-thickness : 3 px ;
129 text-decoration-color : var ( --cap-accent-2 , #e85a1f );
130 text-underline-offset : 0.12 em ;
131 }
132
133 @media ( max-aspect-ratio : 9 / 16 ) {
134 .caption-pill {
135 max-width : 92 % ;
136 }
137 .caption-line {
138 font-size : clamp ( 30 px , 5 vw , 44 px );
139 }
140 }
141 </ style >
142
143 < div
144 id = "captions-root"
145 data-composition-id = "captions"
146 data-timeline-locked
147 data-start = "0"
148 data-duration = "0"
149 data-fps = "30"
150 data-width = "0"
151 data-height = "0"
152 >
153 < div class = "caption-layer" aria-hidden = "true" >
154 < div id = "caption-stage" class = "caption-stage" ></ div >
155 </ div >
156 </ div >
157
158 < script >
159 // captions.mjs injects the engine's scene-aware groups + real duration here.
160 var GROUPS = [];
161 var DURATION = 0 ;
162
163 ( function () {
164 var stage = document. getElementById ( "caption-stage" );
165
166 // build DOM: one .caption-group per group, .caption-word per word
167 GROUPS . forEach ( function ( group , g ) {
168 var groupEl = document. createElement ( "div" );
169 groupEl.className = "caption-group" ;
170 groupEl.id = "caption-group-" + g;
171 var pill = document. createElement ( "div" );
172 pill.className = "caption-pill" ;
173 var line = document. createElement ( "div" );
174 line.className = "caption-line" ;
175 (group.words || []). forEach ( function ( w , i ) {
176 var span = document. createElement ( "span" );
177 span.className = "caption-word" ;
178 span.id = "caption-word-" + g + "-" + i;
179 span.textContent = String (w.text);
180 line. appendChild (span);
181 });
182 pill. appendChild (line);
183 groupEl. appendChild (pill);
184 stage. appendChild (groupEl);
185 });
186
187 window.__timelines = window.__timelines || {};
188 var tl = gsap. timeline ({ paused: true });
189
190 GROUPS . forEach ( function ( group , g ) {
191 var groupEl = document. getElementById ( "caption-group-" + g);
192 var words = group.words || [];
193 var next = GROUPS [g + 1 ];
194 var isLast = g === GROUPS . length - 1 ;
195 var start = Math. max ( 0 , Number (group.start));
196 var end = isLast ? DURATION : Math. min ( Number (next.start), Number (group.end) + 0.3 );
197 if (end <= start) end = start + 0.01 ;
198
199 // group on/off — exactly one group visible at a time
200 tl. set (groupEl, { opacity: 1 }, start);
201 tl. set (groupEl, { opacity: 0 }, end);
202
203 // 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
204 // engine's frame-by-frame seek; .call callbacks would NOT fire).
205 words. forEach ( function ( w , i ) {
206 var el = document. getElementById ( "caption-word-" + g + "-" + i);
207 var at = Math. max (start, Number (w.start));
208 tl. set (el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
209 tl. set (el, { className: "caption-word is-active" }, at); // current
210 tl. fromTo (el, { scale: 0.92 }, { scale: 1 , duration: 0.16 , ease: "back.out(2.2)" }, at);
211 if (i + 1 < words. length ) {
212 var nextAt = Math. max (start, Number (words[i + 1 ].start));
213 tl. set (el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
214 }
215 });
216 if (words. length ) {
217 var lastEl = document. getElementById ( "caption-word-" + g + "-" + (words. length - 1 ));
218 var lastSpoken = Math. min (end, Number (words[words. length - 1 ].end) + 0.1 );
219 tl. set (lastEl, { className: "caption-word is-spoken" }, lastSpoken);
220 }
221 });
222
223 // full-span anchor so the sub-comp timeline spans the whole video
224 tl. to ({}, { duration: DURATION }, 0 );
225 window.__timelines[ "captions" ] = tl;
226 })();
227 </ script >