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/cartesian/ caption-skin.html
HTML · 222 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 museum-catalog lower-third — a warm-stone card on a single 1px taupe
23 hairline border (cartesian's universal structural device), SHARP corners, ZERO
24 shadow, ZERO fill beyond the stone canvas. Playfair Display 400, sentence case, ink
25 glyphs. Upcoming words sit in faint ink; the current word is marked QUIETLY — a faint
26 stone wash with a 1px taupe hairline underline (restraint, not a loud block); spoken
27 words settle to solid ink, the wash and underline 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 stone card — single 1px taupe hairline border (cartesian's universal
69 structural device), SHARP corners, ZERO shadow, ZERO fill beyond the stone canvas */
70 .caption-pill {
71 max-width : 78 % ;
72 padding : 20 px 44 px 24 px ;
73 background : var ( --cap-canvas , #ede8e0 );
74 border : 1 px solid var ( --cap-accent-2 , #b8b0a4 );
75 border-radius : 0 ;
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 , "Playfair Display" ), Georgia , serif ;
83 font-weight : 400 ;
84 font-size : clamp ( 40 px , 4.4 vw , 58 px );
85 line-height : 1.16 ;
86 letter-spacing : -0.01 em ;
87 }
88
89 /* upcoming — faint ink, no mark */
90 .caption-word {
91 display : inline-block ;
92 padding : 0 0.06 em ;
93 border-bottom : 1 px solid transparent ;
94 color : color-mix ( in srgb , var ( --cap-ink , #1a1a1a ) 42 % , var ( --cap-canvas , #ede8e0 ));
95 }
96 /* current word — a QUIET marker: a faint warm-stone wash behind solid ink, plus a
97 1px taupe hairline underline (restraint, not a loud block; SHARP, NO shadow) */
98 .caption-word.is-active {
99 color : var ( --cap-ink , #1a1a1a );
100 background : color-mix ( in srgb , var ( --cap-accent , #8a8178 ) 18 % , var ( --cap-canvas , #ede8e0 ));
101 border-bottom : 1 px solid var ( --cap-accent , #8a8178 );
102 }
103 /* already spoken — solid ink, wash + underline cleared */
104 .caption-word.is-spoken {
105 color : var ( --cap-ink , #1a1a1a );
106 background : transparent ;
107 border-bottom : 1 px solid transparent ;
108 }
109
110 /* optional per-word accents (dormant until a word-classer tags them) */
111 /* brand / key term — Playfair Display italic, cartesian's only display inflection */
112 .caption-word.cap-brand {
113 font-style : italic ;
114 }
115 /* numerals / data — the body sans voice, slightly recessed */
116 .caption-word.cap-num {
117 font-family : var ( --font-body , "Inter" ), ui-sans-serif , sans-serif ;
118 color : color-mix ( in srgb , var ( --cap-ink , #1a1a1a ) 72 % , var ( --cap-canvas , #ede8e0 ));
119 }
120 /* emphasis — a 1px taupe hairline underline (no second color; stone-and-ink only) */
121 .caption-word.cap-cta {
122 text-decoration : underline ;
123 text-decoration-thickness : 1 px ;
124 text-decoration-color : var ( --cap-accent , #8a8178 );
125 text-underline-offset : 0.12 em ;
126 }
127
128 @media ( max-aspect-ratio : 9 / 16 ) {
129 .caption-pill {
130 max-width : 92 % ;
131 }
132 .caption-line {
133 font-size : clamp ( 34 px , 5.4 vw , 48 px );
134 }
135 }
136 </ style >
137
138 < div
139 id = "captions-root"
140 data-composition-id = "captions"
141 data-timeline-locked
142 data-start = "0"
143 data-duration = "0"
144 data-fps = "30"
145 data-width = "0"
146 data-height = "0"
147 >
148 < div class = "caption-layer" aria-hidden = "true" >
149 < div id = "caption-stage" class = "caption-stage" ></ div >
150 </ div >
151 </ div >
152
153 < script >
154 // captions.mjs injects the engine's scene-aware groups + real duration here.
155 var GROUPS = [];
156 var DURATION = 0 ;
157
158 ( function () {
159 var stage = document. getElementById ( "caption-stage" );
160
161 // build DOM: one .caption-group per group, .caption-word per word
162 GROUPS . forEach ( function ( group , g ) {
163 var groupEl = document. createElement ( "div" );
164 groupEl.className = "caption-group" ;
165 groupEl.id = "caption-group-" + g;
166 var pill = document. createElement ( "div" );
167 pill.className = "caption-pill" ;
168 var line = document. createElement ( "div" );
169 line.className = "caption-line" ;
170 (group.words || []). forEach ( function ( w , i ) {
171 var span = document. createElement ( "span" );
172 span.className = "caption-word" ;
173 span.id = "caption-word-" + g + "-" + i;
174 span.textContent = String (w.text);
175 line. appendChild (span);
176 });
177 pill. appendChild (line);
178 groupEl. appendChild (pill);
179 stage. appendChild (groupEl);
180 });
181
182 window.__timelines = window.__timelines || {};
183 var tl = gsap. timeline ({ paused: true });
184
185 GROUPS . forEach ( function ( group , g ) {
186 var groupEl = document. getElementById ( "caption-group-" + g);
187 var words = group.words || [];
188 var next = GROUPS [g + 1 ];
189 var isLast = g === GROUPS . length - 1 ;
190 var start = Math. max ( 0 , Number (group.start));
191 var end = isLast ? DURATION : Math. min ( Number (next.start), Number (group.end) + 0.3 );
192 if (end <= start) end = start + 0.01 ;
193
194 // group on/off — exactly one group visible at a time
195 tl. set (groupEl, { opacity: 1 }, start);
196 tl. set (groupEl, { opacity: 0 }, end);
197
198 // 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
199 // engine's frame-by-frame seek; .call callbacks would NOT fire).
200 words. forEach ( function ( w , i ) {
201 var el = document. getElementById ( "caption-word-" + g + "-" + i);
202 var at = Math. max (start, Number (w.start));
203 tl. set (el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
204 tl. set (el, { className: "caption-word is-active" }, at); // current
205 tl. fromTo (el, { scale: 0.985 }, { scale: 1 , duration: 0.2 , ease: "power1.out" }, at);
206 if (i + 1 < words. length ) {
207 var nextAt = Math. max (start, Number (words[i + 1 ].start));
208 tl. set (el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
209 }
210 });
211 if (words. length ) {
212 var lastEl = document. getElementById ( "caption-word-" + g + "-" + (words. length - 1 ));
213 var lastSpoken = Math. min (end, Number (words[words. length - 1 ].end) + 0.1 );
214 tl. set (lastEl, { className: "caption-word is-spoken" }, lastSpoken);
215 }
216 });
217
218 // full-span anchor so the sub-comp timeline spans the whole video
219 tl. to ({}, { duration: DURATION }, 0 );
220 window.__timelines[ "captions" ] = tl;
221 })();
222 </ script >