Setting the file. One moment. Index · Music To Video · heygen-com/hyperframes · Skills DocsGsap Min
(opens in a new tab)
references/templates/held-text-strobe-burst/index.html
HTML·302 lines·11 KB
12 {"id":"decor", "type":"select", "label":"Blueprint grid + crop border", "options":["no","yes"], "default":"no"}
13 ]'
14>
15 <head>
16 <meta charset="utf-8" />
17 <meta name="viewport" content="width=1920, height=1080" />
18 <title>Held Text / Strobe Burst (Texture Flip) — Template</title>
19 <script src="../../motion-primitives/assets/gsap.min.js"></script>
20 <style>
21 *,
22 *::before,
23 *::after {
24 margin: 0;
25 padding: 0;
26 box-sizing: border-box;
27 }
28 html,
29 body {
30 width: 100%;
31 height: 100%;
32 background: #000;
33 overflow: hidden;
34 }
35
36 #root {
37 position: relative;
38 width: 1920px;
39 height: 1080px;
40 overflow: hidden;
41 background: #000;
42 font-family: Arial, sans-serif;
43 }
44
45 /* Persistent base: the held word on the idle plate. Always visible; the
46 texture looks flip ON TOP of it during bursts, so the word reads as
47 continuously held (anchor) and falls back to a solid color on black
48 between bursts. */
49 .base {
50 position: absolute;
51 inset: 0;
52 background: var(--idle-bg, #000);
53 }
54
55 /* One full-frame layer per texture frame. Only opacity is toggled — a
56 seek-safe flip-book (deterministic under frame-by-frame render). */
57 .look {
58 position: absolute;
59 inset: 0;
60 overflow: hidden;
61 opacity: 0;
62 background: var(--bg);
63 }
64 /* optional blueprint grid */
65 .look.decor::before,
66 .base.decor::before {
67 content: "";
68 position: absolute;
69 inset: 0;
70 background-image:
71 linear-gradient(rgba(255, 255, 255, 0.12) 1px, transparent 1px),
72 linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
73 background-size: 72px 72px;
74 opacity: 0.18;
75 mix-blend-mode: soft-light;
76 }
77 /* optional inner crop border */
78 .look.decor::after,
79 .base.decor::after {
80 content: "";
81 position: absolute;
82 inset: 44px;
83 border: 5px solid rgba(255, 255, 255, 0.22);
84 border-radius: 4px;
85 }
86
87 .word-wrap {
88 position: absolute;
89 inset: 0;
90 z-index: 8;
91 display: flex;
92 align-items: center;
93 justify-content: center;
94 filter: drop-shadow(0 18px 26px rgba(0, 0, 0, 0.28));
95 }
96
97 /* THE technique: the word's letterforms are a window — a solid tint
98 (multiply-blended) over a texture image, clipped to the text. */
99 .word {
100 color: transparent;
101 background-color: transparent;
102 background-image: var(--solid-fill), var(--texture-url);
103 background-blend-mode: multiply;
104 background-size:
105 100% 100%,
106 var(--texture-size, 150% 150%);
107 background-position: center, var(--texture-position, center);
108 background-repeat: repeat;
109 -webkit-background-clip: text;
110 background-clip: text;
111 text-align: center;
112 white-space: nowrap;
113 }
114 /* solid-fill variant for the idle base word (no texture) */
115 .word.solid {
116 background-image: none;
117 color: var(--ink, #fff);
118 -webkit-text-fill-color: var(--ink, #fff);
119 }
120
121 .word.display {
122 font-family: Impact, "Arial Black", Arial, sans-serif;
123 font-size: 470px;
124 font-weight: 900;
125 /* line-height ≥ 1 so the background-paint box encloses the full glyph;
126 with line-height < 1 the cap tops/bottoms overflow the box and the
127 texture (clipped to text) leaves untextured slivers there. */
128 line-height: 1.05;
129 text-transform: uppercase;
130 transform: scaleX(0.78);
131 -webkit-text-stroke: 1px rgba(255, 255, 255, 0.22);
132 }
133 .word.serif {
134 font-family: Georgia, "Times New Roman", serif;
135 font-size: 360px;
136 font-weight: 500;
137 font-style: italic;
138 line-height: 1.05;
139 }
140 </style>
141 </head>
142 <body>
143 <div
144 id="root"
145 data-composition-id="held-text-strobe-burst"
146 data-width="1920"
147 data-height="1080"
148 data-start="0"
149 data-duration="3"
150 data-root="true"
151 >
152 <!-- .base (held word on idle plate) and .look layers are injected by JS -->
153 </div>
154
155 <script>
156 (function () {
157 // Built-in texture frames (texture-flip palette). texture = file name
158 // (no .png) under assets/texture-mask-text/masks/.
159 var BUILTIN_FRAMES =
160 '[{"texture":"tiles-138","bg":"#ff70b0","fill":"#f0a020","ink":"#2a0f1d"},' +
161 '{"texture":"paving-stones-150","bg":"#f0d020","fill":"#d07838","ink":"#2a2410"},' +
162 '{"texture":"metal-041-a","bg":"#1050e0","fill":"#9fd0ff","ink":"#eaf2ff"},' +
163 '{"texture":"lava","bg":"#101127","fill":"#ff5a2c","ink":"#ffd9c2"},' +
164 '{"texture":"ground-080","bg":"#0f7a5a","fill":"#ffd27a","ink":"#04241a"},' +
165 '{"texture":"onyx","bg":"#e8e2d6","fill":"#3a3a44","ink":"#1a1a22"},' +
166 '{"texture":"bricks-075-a","bg":"#ff9000","fill":"#7fb0c0","ink":"#241400"}]';
167
168 // Built-in plan: as a BASE template the default is one continuous burst
169 // across the whole composition — flips every 0.125s (≈3f@24fps), never
170 // stops, never cuts to idle. (For the gruop_4-faithful two-burst look,
171 // pass strobePlan = [{start:1.125,end:1.542,step:.125},{start:2.042,end:2.708,step:.125}].)
172 var BUILTIN_PLAN = '[{"start":0,"end":3,"step":0.125}]';
173
174 var defaults = {
175 markText: "NAMSHI",
176 fontStyle: "display",
177 markScale: 1,
178 idleColor: "#000000",
179 idleInk: "#ffffff",
180 frames: "",
181 strobePlan: "",
182 decor: "no",
183 };
184 var vars = Object.assign(
185 {},
186 defaults,
187 window.__hyperframes && window.__hyperframes.getVariables
188 ? window.__hyperframes.getVariables()
189 : {},
190 );
191
192 var root = document.getElementById("root");
193 var masksDir = "assets/texture-mask-text/masks";
194 var style = vars.fontStyle === "serif" ? "serif" : "display";
195 var decor = vars.decor === "yes";
196 var scale = Number(vars.markScale) || 1;
197
198 function parse(str, fallback) {
199 var v = str && String(str).trim() ? str : fallback;
200 try {
201 var p = JSON.parse(v);
202 return Array.isArray(p) ? p : [];
203 } catch (e) {
204 return [];
205 }
206 }
207 var frames = parse(vars.frames, BUILTIN_FRAMES);
208 var plan = parse(vars.strobePlan, BUILTIN_PLAN);
209
210 // Apply word size scale (relative to the per-style base size).
211 var baseSize = style === "serif" ? 360 : 470;
212
213 function makeWord(extraClass) {
214 var word = document.createElement("div");
215 word.className = "word " + style + (extraClass ? " " + extraClass : "");
216 word.textContent = String(vars.markText);
217 word.style.fontSize = baseSize * scale + "px";
218 return word;
219 }
220 function wrap(word) {
221 var w = document.createElement("div");
222 w.className = "word-wrap";
223 w.appendChild(word);
224 return w;
225 }
226
227 // ── Persistent base: held word on the idle plate ──────────────────
228 var base = document.createElement("div");
229 base.className = "base" + (decor ? " decor" : "");
230 base.style.setProperty("--idle-bg", vars.idleColor);
231 var baseWord = makeWord("solid");
232 baseWord.style.setProperty("--ink", vars.idleInk);
233 base.appendChild(wrap(baseWord));
234 root.appendChild(base);
235
236 // ── One stacked .look per texture frame ───────────────────────────
237 var looks = frames.map(function (f) {
238 var look = document.createElement("div");
239 look.className = "look" + (decor ? " decor" : "");
240 look.style.setProperty("--bg", f.bg || "#000");
241 var word = makeWord("");
242 word.style.setProperty("--texture-url", 'url("' + masksDir + "/" + f.texture + '.png")');
243 word.style.setProperty(
244 "--solid-fill",
245 "linear-gradient(" + (f.fill || "#fff") + "," + (f.fill || "#fff") + ")",
246 );
247 if (f.pos) word.style.setProperty("--texture-position", f.pos);
248 look.appendChild(wrap(word));
249 root.appendChild(look);
250 return look;
251 });
252
253 // ── Deterministic, beat-quantized timeline ────────────────────────
254 window.__timelines = window.__timelines || {};
255 var tl = gsap.timeline({ paused: true });
256 gsap.set(looks, { opacity: 0 });
257 var compDur = Number(root.getAttribute("data-duration")) || 3;
258
259 function gridJumps(start, end, stp) {
260 var s = Number(start) || 0,
261 e = Number(end) || s,
262 d = Number(stp) || 0.125;
263 var out = [],
264 n = Math.max(0, Math.floor((e - s) / d + 1e-6) + 1);
265 for (var i = 0; i < n; i++) out.push(+(s + i * d).toFixed(4));
266 return out;
267 }
268
269 // For each burst: flip a fresh texture look on every jump (0ms), cycling
270 // frames by a global step index; clear back to the idle base at end.
271 var step = 0;
272 var prev = null;
273 plan.forEach(function (burst) {
274 var jumps =
275 Array.isArray(burst.jumps) && burst.jumps.length
276 ? burst.jumps.slice()
277 : gridJumps(burst.start, burst.end, burst.step);
278 jumps.forEach(function (t) {
279 if (!looks.length) return;
280 var cur = looks[step % looks.length];
281 if (prev && prev !== cur) tl.set(prev, { opacity: 0 }, t);
282 tl.set(cur, { opacity: 1 }, t);
283 prev = cur;
284 step += 1;
285 });
286 // clean cut back to idle base (held word on black) at burst end —
287 // but NOT when the burst runs to the composition end, so the strobe
288 // never blacks out / stops on the last frame.
289 if (prev && burst.end < compDur - 1e-6) {
290 tl.set(prev, { opacity: 0 }, burst.end);
291 prev = null;
292 }
293 });
294
295 // pin total duration
296 tl.set({}, {}, compDur);
297
298 window.__timelines["held-text-strobe-burst"] = tl;
299 })();
300 </script>
301 </body>
302</html>