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