返回 html-video
captions.html
1 <template id="captions-template">
2 <div
3 data-composition-id="captions"
4 data-width="1080"
5 data-height="1920"
6 data-duration="__VIDEO_DURATION__"
7 >
8 <div id="captions-container"></div>
9
10 <style>
11 [data-composition-id="captions"] {
12 position: absolute;
13 top: 0;
14 left: 0;
15 width: 1080px;
16 height: 1920px;
17 font-family: "Helvetica", "Helvetica Neue", Arial, sans-serif;
18 font-weight: 900; /* Helvetica Black/Bold */
19 text-transform: uppercase;
20 pointer-events: none;
21 }
22
23 [data-composition-id="captions"] #captions-container {
24 position: absolute;
25 inset: 0;
26 }
27
28 [data-composition-id="captions"] .caption-group {
29 position: absolute;
30 left: 50%;
31 transform: translateX(-50%);
32 width: auto;
33 max-width: 920px; /* 1080 - 2*80 */
34 bottom: 672px;
35 display: flex;
36 justify-content: center;
37 align-items: center;
38 text-align: center;
39 opacity: 0;
40 z-index: 10;
41 }
42
43 [data-composition-id="captions"] .caption-text {
44 font-size: 64px;
45 line-height: 1.1;
46 color: #000000;
47 background-color: #ffffff;
48 padding: 15px 30px;
49 border-top: 8px solid #cc0000; /* Vignelli Red top border accent */
50 display: inline-block;
51 letter-spacing: -0.02em; /* Tight Helvetica spacing */
52 white-space: nowrap; /* Prevent wrapping at the element level */
53 max-width: 900px;
54 overflow: hidden;
55 }
56 </style>
57
58 <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
59 <script>
60 (function () {
61 const TRANSCRIPT = [
62 { text: "We", start: 0.14, end: 0.239 },
63 { text: "asked", start: 0.28, end: 0.459 },
64 { text: "what", start: 0.5, end: 0.619 },
65 { text: "you", start: 0.659, end: 0.779 },
66 { text: "needed.", start: 0.8, end: 1.179 },
67 { text: "Forty-seven", start: 1.199, end: 1.619 },
68 { text: "percent", start: 1.699, end: 1.96 },
69 { text: "of", start: 1.979, end: 2.059 },
70 { text: "you", start: 2.099, end: 2.299 },
71 { text: "said", start: 2.319, end: 2.499 },
72 { text: "motion", start: 2.559, end: 2.839 },
73 { text: "graphics,", start: 2.899, end: 3.759 },
74 { text: "sixty-two", start: 3.799, end: 4.159 },
75 { text: "percent", start: 4.239, end: 4.559 },
76 { text: "said", start: 4.639, end: 4.799 },
77 { text: "static", start: 4.859, end: 5.199 },
78 { text: "content", start: 5.239, end: 5.639 },
79 { text: "was", start: 5.679, end: 5.779 },
80 { text: "costing", start: 5.859, end: 6.219 },
81 { text: "you", start: 6.259, end: 6.339 },
82 { text: "attention,", start: 6.379, end: 7.299 },
83 { text: "and", start: 7.319, end: 7.439 },
84 { text: "three", start: 7.48, end: 7.599 },
85 { text: "out", start: 7.679, end: 7.779 },
86 { text: "of", start: 7.799, end: 7.879 },
87 { text: "four", start: 7.94, end: 8.139 },
88 { text: "said", start: 8.279, end: 8.46 },
89 { text: "you", start: 8.519, end: 8.6 },
90 { text: "know", start: 8.619, end: 8.739 },
91 { text: "the", start: 8.8, end: 8.88 },
92 { text: "look", start: 8.92, end: 9.079 },
93 { text: "you", start: 9.119, end: 9.259 },
94 { text: "want", start: 9.279, end: 9.619 },
95 { text: "but", start: 9.779, end: 9.88 },
96 { text: "don't", start: 9.92, end: 10.1 },
97 { text: "have", start: 10.159, end: 10.3 },
98 { text: "the", start: 10.3, end: 10.42 },
99 { text: "editing", start: 10.5, end: 10.779 },
100 { text: "skills", start: 10.86, end: 11.139 },
101 { text: "to", start: 11.159, end: 11.239 },
102 { text: "get", start: 11.279, end: 11.439 },
103 { text: "there.", start: 11.46, end: 12.239 },
104 { text: "So", start: 12.3, end: 12.38 },
105 { text: "we", start: 12.399, end: 12.519 },
106 { text: "built", start: 12.539, end: 12.759 },
107 { text: "Hyperframes", start: 12.84, end: 13.119 },
108 ];
109 const tl = gsap.timeline({ paused: true });
110 const container = document.getElementById("captions-container");
111
112 // Group words into single lines (max words or based on pauses/sentence ends)
113 function groupTranscript(transcript, maxWords = 3) {
114 const groups = [];
115 let currentGroup = [];
116
117 transcript.forEach((word, index) => {
118 currentGroup.push(word);
119
120 const nextWord = transcript[index + 1];
121 const isPause = nextWord && nextWord.start - word.end > 0.15;
122 const isSentenceEnd =
123 word.text.includes(".") || word.text.includes("?") || word.text.includes("!");
124
125 if (currentGroup.length >= maxWords || isPause || isSentenceEnd || !nextWord) {
126 groups.push({
127 words: currentGroup,
128 start: currentGroup[0].start,
129 end: currentGroup[currentGroup.length - 1].end,
130 text: currentGroup.map((w) => w.text).join(" "),
131 });
132 currentGroup = [];
133 }
134 });
135 return groups;
136 }
137
138 const captionGroups = groupTranscript(TRANSCRIPT);
139
140 captionGroups.forEach((group, index) => {
141 const div = document.createElement("div");
142 div.className = "caption-group";
143 div.id = `group-${index}`;
144
145 const textSpan = document.createElement("span");
146 textSpan.className = "caption-text";
147 textSpan.innerText = group.text;
148
149 div.appendChild(textSpan);
150 container.appendChild(div);
151
152 // Animation: Modern "expo.out" transitions
153 // Entrance
154 tl.fromTo(
155 div,
156 { opacity: 0, y: 20 },
157 {
158 opacity: 1,
159 y: 0,
160 duration: 0.4,
161 ease: "expo.out",
162 },
163 group.start,
164 );
165
166 // Exit
167 tl.to(
168 div,
169 {
170 opacity: 0,
171 y: -10,
172 duration: 0.3,
173 ease: "expo.out",
174 },
175 group.end - 0.3,
176 );
177
178 // Hard kill to ensure caption is hidden
179 tl.set(div, { opacity: 0, visibility: "hidden" }, group.end);
180 });
181
182 window.__timelines = window.__timelines || {};
183 window.__timelines["captions"] = tl;
184 })();
185 </script>
186 </div>
187 </template>
188
188 lines HTML