Setting the file. One moment. Memory Wall · Embedded Captions · heygen-com/hyperframes · Skills Docs14.43
Hershey Script Med
(opens in a new tab)
references/example-renders/memory-wall.html
HTML·334 lines·10 KB
>
7 <style>
8 * {
9 margin: 0;
10 padding: 0;
11 box-sizing: border-box;
12 }
13 html,
14 body {
15 margin: 0;
16 width: 1280px;
17 height: 720px;
18 overflow: hidden;
19 background: #000;
20 font-family: "Inter", system-ui, sans-serif;
21 }
22
23 #a-roll,
24 #fg-overlay {
25 position: absolute;
26 inset: 0;
27 width: 100%;
28 height: 100%;
29 object-fit: cover;
30 }
31
32 /* Foreground matte sits above the captions so the person & mic occlude
33 the text — classic cinematic lower-third layering. */
34 #fg-overlay {
35 z-index: 3;
36 pointer-events: none;
37 }
38
39 /*
40 Caption stage — perspective container so the text plane lies on the
41 acoustic-foam wall in the upper-right of the frame. Small jitter is
42 applied to the stage by GSAP to fake handheld motion tracking.
43 */
44 #stage {
45 position: absolute;
46 top: 0;
47 left: 0;
48 width: 1280px;
49 height: 720px;
50 perspective: 1400px;
51 perspective-origin: 30% 40%;
52 pointer-events: none;
53 z-index: 2;
54 will-change: transform;
55 }
56
57 .wall-plane {
58 position: absolute;
59 /* Right-aligned cluster, but widened and pushed slightly left
60 so emphasis words (EVERYTHING, SHARP AGAIN) reach past the
61 subject's silhouette. Short words still hug the wall. */
62 top: 90px;
63 right: 30px;
64 width: 720px;
65 height: 520px;
66 transform-style: preserve-3d;
67 transform: rotateY(-13deg) rotateX(1deg);
68 transform-origin: right center;
69 padding: 24px 32px;
70 display: flex;
71 flex-direction: column;
72 justify-content: center;
73 align-items: flex-end;
74 text-align: right;
75 gap: 14px;
76 }
77
78 .cap {
79 display: block;
80 position: relative;
81 line-height: 1.05;
82 opacity: 0;
83 /* The "embed in scene" move: overlay blend picks up the wall's
84 luminance and color so the text feels lit BY the scene. */
85 mix-blend-mode: screen;
86 color: #fff4dc;
87 text-shadow:
88 0 0 12px rgba(255, 210, 150, 0.45),
89 0 2px 5px rgba(0, 0, 0, 0.7);
90 filter: brightness(1.1) contrast(1.05);
91 will-change: transform, opacity, filter;
92 }
93
94 .cap .w {
95 display: inline-block;
96 opacity: 0;
97 will-change: opacity, transform, filter;
98 }
99
100 /*
101 Unified cinematic typography — no mode switch between soft & sharp.
102 Elegant serif-inflected sans, italic for the dreamy first half,
103 upright for the second half. One look, one feeling, progressing.
104 */
105 .cap-1 {
106 font-size: 78px;
107 font-weight: 600;
108 font-style: italic;
109 letter-spacing: -0.01em;
110 }
111 .cap-2 {
112 font-size: 66px;
113 font-weight: 500;
114 font-style: italic;
115 letter-spacing: -0.005em;
116 padding-right: 44px; /* hanging indent on right for right-aligned flow */
117 }
118 .cap-3 {
119 font-size: 72px;
120 font-weight: 700;
121 font-style: normal;
122 letter-spacing: -0.015em;
123 }
124 .cap-4 {
125 font-size: 90px;
126 font-weight: 900;
127 font-style: normal;
128 letter-spacing: -0.03em;
129 text-transform: uppercase;
130 line-height: 1;
131 }
132 </style>
133 </head>
134 <body>
135 <div
136 id="root"
137 data-composition-id="main"
138 data-start="0"
139 data-duration="8.04"
140 data-width="1280"
141 data-height="720"
142 >
143 <!-- Layer 0: background plate (full video) -->
144 <video
145 id="a-roll"
146 class="clip"
147 src="source.mp4"
148 muted
149 playsinline
150 data-duration="8.04"
151 data-track-index="0"
152 style="z-index: 1"
153 ></video>
154
155 <!-- Layer 2: caption stage (between background and foreground matte) -->
156 <div id="stage">
157 <div class="wall-plane">
158 <div id="cg-0" class="cap cap-1">
159 <span class="w" data-i="0">Some</span>
160 <span class="w" data-i="1">memories</span>
161 <span class="w" data-i="2">feel</span>
162 <span class="w" data-i="3">soft</span>
163 </div>
164 <div id="cg-1" class="cap cap-2">
165 <span class="w" data-i="0">like</span>
166 <span class="w" data-i="1">old</span>
167 <span class="w" data-i="2">film</span>
168 </div>
169 <div id="cg-2" class="cap cap-3">
170 <span class="w" data-i="0">but</span>
171 <span class="w" data-i="1">suddenly</span>
172 </div>
173 <div id="cg-3" class="cap cap-4">
174 <span class="w" data-i="0">everything</span>
175 <span class="w" data-i="1">is</span>
176 <span class="w" data-i="2">sharp</span>
177 <span class="w" data-i="3">again</span>
178 </div>
179 </div>
180 </div>
181
182 <!-- Foreground occlusion (person + mic) is applied in post via ffmpeg
183 overlay. Browser-side alpha webm is unreliable across encoders. -->
184
185 <!-- Audio -->
186 <audio
187 id="a-roll-audio"
188 src="source.mp4"
189 data-start="0"
190 data-duration="8.04"
191 data-track-index="3"
192 data-volume="1"
193 ></audio>
194 </div>
195
196 <script>
197 window.__timelines = window.__timelines || {};
198 const tl = gsap.timeline({ paused: true });
199 const DUR = 8.04;
200
201 /* ─────────────────────────────────────────────────────────────────
202 Word-level transcript — ElevenLabs timestamps.
203 ───────────────────────────────────────────────────────────────── */
204 const GROUPS = [
205 {
206 id: "cg-0",
207 words: [
208 { text: "Some", start: 0.24, end: 0.44 },
209 { text: "memories", start: 0.48, end: 0.82 },
210 { text: "feel", start: 0.9, end: 1.14 },
211 { text: "soft", start: 1.2, end: 1.64 },
212 ],
213 in: 0.2,
214 out: 4.85,
215 tone: "soft",
216 },
217 {
218 id: "cg-1",
219 words: [
220 { text: "like", start: 2.66, end: 2.78 },
221 { text: "old", start: 2.86, end: 3.02 },
222 { text: "film", start: 3.14, end: 3.46 },
223 ],
224 in: 2.55,
225 out: 4.85,
226 tone: "soft",
227 },
228 {
229 id: "cg-2",
230 words: [
231 { text: "but", start: 5.02, end: 5.16 },
232 { text: "suddenly", start: 5.22, end: 5.76 },
233 ],
234 in: 4.9,
235 out: 8.04,
236 tone: "present",
237 },
238 {
239 id: "cg-3",
240 words: [
241 { text: "everything", start: 6.8, end: 7.14 },
242 { text: "is", start: 7.16, end: 7.28 },
243 { text: "sharp", start: 7.3, end: 7.52 },
244 { text: "again", start: 7.56, end: 7.92 },
245 ],
246 in: 6.7,
247 out: 8.04,
248 tone: "present",
249 },
250 ];
251
252 /* ─────────────────────────────────────────────────────────────────
253 Each caption group: cinematic word-by-word reveal. No flashes, no
254 grain — purely text animation. Soft groups drift up gently, later
255 groups resolve firmly into place to match the sentence turn.
256 ───────────────────────────────────────────────────────────────── */
257 GROUPS.forEach(function (g) {
258 var sel = "#" + g.id;
259 var words = g.words.map(function (_, i) {
260 return sel + " .w[data-i='" + i + "']";
261 });
262
263 var isSoft = g.tone === "soft";
264
265 /* Container stays fully visible — each word animates its own fade.
266 This avoids a double-fade (container × word) that produced a
267 non-linear pop in the first ~100ms of the first caption. */
268 tl.set(sel, { opacity: 1, y: 0 }, Math.max(0, g.in - 0.01));
269
270 /* Per-word reveal — opacity + translate only. No letter-spacing
271 or filter:blur here: both can perturb inline-block metrics
272 mid-animation and cause the cap line-box to reflow, which
273 makes words like "Some" visibly shift between rows when a
274 sibling word enters. transform-only = no reflow. */
275 g.words.forEach(function (w, i) {
276 if (isSoft) {
277 tl.fromTo(
278 words[i],
279 { opacity: 0, y: 8 },
280 {
281 opacity: 1,
282 y: 0,
283 duration: 0.45,
284 ease: "power2.out",
285 overwrite: "auto",
286 },
287 w.start,
288 );
289 } else {
290 tl.fromTo(
291 words[i],
292 { opacity: 0, y: 6, scale: 1.04 },
293 {
294 opacity: 1,
295 y: 0,
296 scale: 1.0,
297 duration: 0.22,
298 ease: "power3.out",
299 overwrite: "auto",
300 transformOrigin: "50% 50%",
301 },
302 w.start,
303 );
304 }
305 });
306
307 /* Group exit — soft groups linger and fade as the sentence turns,
308 present groups hold to the end. Clean fades, no trickery. */
309 if (g.out < DUR) {
310 tl.to(
311 sel,
312 { opacity: 0, duration: 0.5, ease: "power2.in", overwrite: "auto" },
313 g.out - 0.5,
314 );
315 }
316 tl.set(sel, { opacity: 0, visibility: "hidden" }, g.out);
317 });
318
319 /* Self-lint */
320 GROUPS.forEach(function (g) {
321 var el = document.getElementById(g.id);
322 if (!el) return;
323 tl.seek(g.out + 0.01);
324 var cs = window.getComputedStyle(el);
325 if (cs.opacity !== "0" && cs.visibility !== "hidden") {
326 console.warn("[caption-lint] " + g.id + " visible at t=" + (g.out + 0.01).toFixed(2));
327 }
328 });
329 tl.seek(0);
330
331 window.__timelines["main"] = tl;
332 </script>
333 </body>
334</html>