Setting the file. One moment. Code Slice Hero · Code Slice Hero · heygen-com/hyperframes · Skills Docs(opens in a new tab)
code-slice-hero.html
HTML·530 lines·24 KB
script
src
=
"assets/gsap-3.14.2.min.js"
></
script
>
8 <script src="shadows.js"></script>
9 <script src="surface.js"></script>
10 <style>
11 @font-face {
12 font-family: SliceGeist;
13 src: url("./assets/Geist-Bold.ttf") format("truetype");
14 font-weight: 700;
15 font-display: block;
16 }
17 * {
18 box-sizing: border-box;
19 margin: 0;
20 padding: 0;
21 }
22 html,
23 body {
24 background: #fff;
25 }
26 #csh-root {
27 position: relative;
28 width: 1920px;
29 height: 1080px;
30 overflow: hidden;
31 background: #fff;
32 }
33 #csh-stage {
34 position: absolute;
35 inset: 0;
36 }
37 #csh-flat {
38 position: absolute;
39 inset: 0;
40 background: #fff var(--csh-front) no-repeat 0 0 / 1920px 1080px;
41 }
42 .csh-accessible {
43 position: absolute;
44 width: 1px;
45 height: 1px;
46 overflow: hidden;
47 clip-path: inset(50%);
48 white-space: nowrap;
49 }
50 </style>
51 </head>
52 <body>
53 <div
54 id="csh-root"
55 data-composition-variables='[{"id":"headline","type":"string","label":"Copy - Front headline","default":"MAKE IT","maxLength":32},{"id":"reverseHeadline","type":"string","label":"Copy - Rear headline","default":"MATTER.","maxLength":32},{"id":"direction","type":"enum","label":"Motion - Sweep direction","default":"left-to-right","options":[{"value":"left-to-right","label":"Left to right"},{"value":"right-to-left","label":"Right to left"}]},{"id":"sweepDuration","type":"number","label":"Motion - Sweep duration","default":4.2,"min":2.6,"max":4.5,"step":0.05,"unit":"s"},{"id":"sweepEaseStrength","type":"number","label":"Motion - Sweep easing strength","default":3,"min":0,"max":3,"step":0.05,"description":"0 = linear. Higher values start faster and slow more strongly toward the end."},{"id":"flipDuration","type":"number","label":"Motion - Each tile flip","default":0.75,"min":0.65,"max":1.65,"step":0.05,"unit":"s"},{"id":"cursorRadius","type":"number","label":"Cursor - Influence radius","default":210,"min":160,"max":480,"step":10,"unit":"px"},{"id":"cursorFalloff","type":"number","label":"Cursor - Influence falloff","default":1.4,"min":0.4,"max":3.5,"step":0.05},{"id":"cursorDepth","type":"number","label":"Cursor - Depth (+ toward camera)","default":80,"min":-320,"max":320,"step":10,"unit":"px"},{"id":"tiltStrength","type":"number","label":"Cursor - Pull / tilt strength","default":55,"min":0,"max":55,"step":1,"unit":"deg"},{"id":"noiseStrength","type":"number","label":"Motion - Organic variation","default":2,"min":0,"max":2,"step":0.05},{"id":"flipBounce","type":"number","label":"Motion - Elastic flip bounce","default":0.15,"min":0,"max":0.8,"step":0.05},{"id":"cellSize","type":"number","label":"Slices - Square cell size","default":112,"min":24,"max":120,"step":4,"unit":"px"},{"id":"formationScale","type":"number","label":"Slices - Local formation scale","default":1,"min":0.94,"max":1,"step":0.001},{"id":"fontSize","type":"number","label":"Type - Maximum size","default":310,"min":160,"max":480,"step":5,"unit":"px"},{"id":"behindColor","type":"color","label":"Surface - Behind-tile background","default":"#212121"},{"id":"shadowStrength","type":"number","label":"Surface - Cast shadow strength","default":0.19,"min":0,"max":0.65,"step":0.01},{"id":"shadowSoftness","type":"number","label":"Surface - Cast shadow softness","default":4,"min":0.5,"max":4,"step":0.1,"unit":"px"}]'
56 data-composition-id="code-slice-hero"
57 data-start="0"
58 data-duration="8"
59 data-width="1920"
60 data-height="1080"
61 >
62 <section
63 id="csh-stage"
64 class="clip"
65 data-start="0"
66 data-duration="8"
67 data-track-index="1"
68 aria-hidden="true"
69 ></section>
70 <div id="csh-flat" aria-hidden="true"></div>
71 <h1 id="csh-accessible" class="csh-accessible"></h1>
72 </div>
73 <script>
74 (() => {
75 "use strict";
76 const W = 1920,
77 H = 1080,
78 DURATION = 8,
79 SWEEP_START = 1.35;
80 const schema = JSON.parse(
81 document.getElementById("csh-root").getAttribute("data-composition-variables"),
82 );
83 const defaults = Object.fromEntries(schema.map((v) => [v.id, v.default]));
84 const values = { ...defaults, ...(window.__hyperframes?.getVariables?.() || {}) };
85 const clamp = (v, a = 0, b = 1) => Math.max(a, Math.min(b, v));
86 const number = (key, a, b) =>
87 Number.isFinite(Number(values[key])) ? clamp(Number(values[key]), a, b) : defaults[key];
88 const smooth = (v) => {
89 const u = clamp(v);
90 return u * u * u * (10 + u * (-15 + 6 * u));
91 };
92 const headline = String(values.headline ?? defaults.headline)
93 .replace(/\s+/g, " ")
94 .trim()
95 .slice(0, 32);
96 const reverseHeadline = String(values.reverseHeadline ?? defaults.reverseHeadline)
97 .replace(/\s+/g, " ")
98 .trim()
99 .slice(0, 32);
100 const direction = values.direction === "right-to-left" ? -1 : 1;
101 const cellSize = Math.round(number("cellSize", 24, 120));
102 const columns = Math.ceil(W / cellSize),
103 rows = Math.ceil(H / cellSize);
104 const gridX = Math.floor((W - columns * cellSize) / 2),
105 gridY = Math.floor((H - rows * cellSize) / 2);
106 const formationScale = number("formationScale", 0.94, 1);
107 const sweepPower = 1 + number("sweepEaseStrength", 0, 3);
108 const sweepDuration = number("sweepDuration", 2.6, 4.5),
109 flipDuration = number("flipDuration", 0.65, 1.65);
110 const cursorRadius = number("cursorRadius", 160, 480),
111 tiltStrength = number("tiltStrength", 0, 55);
112 const cursorDepth = number("cursorDepth", -320, 320);
113 const cursorFalloff = number("cursorFalloff", 0.4, 3.5);
114 const fieldReach = cursorRadius * Math.sqrt(-Math.log(0.06) / cursorFalloff);
115 const noiseStrength = number("noiseStrength", 0, 2),
116 flipBounce = number("flipBounce", 0, 0.8);
117 const maxFont = number("fontSize", 160, 480);
118 // Match the workbench color control: both #RGB and #RRGGBB are valid.
119 const rawBehindColor = String(values.behindColor ?? "")
120 .trim()
121 .toLowerCase();
122 const behindColor = /^#[0-9a-f]{3}$/.test(rawBehindColor)
123 ? "#" +
124 rawBehindColor
125 .slice(1)
126 .split("")
127 .map((c) => c + c)
128 .join("")
129 : /^#[0-9a-f]{6}$/.test(rawBehindColor)
130 ? rawBehindColor
131 : "#ffffff";
132 const shadowStrength = number("shadowStrength", 0, 0.65),
133 shadowSoftness = number("shadowSoftness", 0.5, 4);
134 const stage = document.getElementById("csh-stage"),
135 root = document.getElementById("csh-root");
136 const accessible = document.getElementById("csh-accessible"),
137 flat = document.getElementById("csh-flat");
138 const tiles = [],
139 cells = [];
140 const cursorLeft = -cursorRadius * 1.8,
141 cursorSpan = W + cursorRadius * 3.6;
142 let disposed = false,
143 timeline,
144 lastTime = 0,
145 finishTime = 0;
146 let firstMotion = SWEEP_START,
147 lastMotion = DURATION,
148 lastRenderedTime = NaN,
149 frameMode;
150 let surface;
151 const renderStats = { evaluatedTiles: 0, drawCalls: 0, restTiles: 0 };
152
153 // Shared front and rear surfaces, as in Code Slice Hero's UV windows.
154 // Raster alpha selects flip eligibility only, never a moving mask.
155 function textSurface(text) {
156 const canvas = document.createElement("canvas");
157 canvas.width = W;
158 canvas.height = H;
159 const ctx = canvas.getContext("2d", { willReadFrequently: true });
160 let size = maxFont;
161 ctx.font = `700 ${size}px SliceGeist`;
162 const measured = ctx.measureText(text);
163 if (measured.width > W - 320) size *= (W - 320) / measured.width;
164 ctx.font = `700 ${size}px SliceGeist`;
165 ctx.textAlign = "center";
166 ctx.textBaseline = "alphabetic";
167 const box = ctx.measureText(text);
168 const baseline = H / 2 + (box.actualBoundingBoxAscent - box.actualBoundingBoxDescent) / 2;
169 ctx.fillStyle = "#101010";
170 ctx.fillText(text, W / 2, baseline);
171 const ink = ctx.getImageData(0, 0, W, H).data;
172 let inkPixels = 0;
173 for (let i = 3; i < ink.length; i += 4) if (ink[i] > 0) inkPixels++;
174 ctx.globalCompositeOperation = "destination-over";
175 ctx.fillStyle = "#fff";
176 ctx.fillRect(0, 0, W, H);
177 return { canvas, ink, inkPixels, url: canvas.toDataURL("image/png"), size };
178 }
179
180 function countInk(ink, x0, y0, x1, y1) {
181 let count = 0;
182 for (let y = Math.max(0, y0); y < Math.min(H, y1); y++)
183 for (let x = Math.max(0, x0); x < Math.min(W, x1); x++) {
184 if (ink[(y * W + x) * 4 + 3] > 0) count++;
185 }
186 return count;
187 }
188
189 // These independent hashes are the experiment's actual per-purpose seeds.
190 function hash(col, row, a, b) {
191 const v = Math.sin(col * a + row * b) * 43758.5453;
192 return v - Math.floor(v);
193 }
194
195 // Same Gaussian^falloff + smooth cutoff as the source hover (falloff 1.4).
196 // Distances are in composition pixels, so the field stays circular even
197 // when square cell size / grid density changes.
198 function fieldAt(x, y, cursorX, cursorY) {
199 const distanceSquared = (x - cursorX) ** 2 + (y - cursorY) ** 2;
200 const raw = Math.exp((-cursorFalloff * distanceSquared) / (cursorRadius * cursorRadius));
201 const u = clamp((raw - 0.06) / 0.94);
202 return u * u * (3 - 2 * u);
203 }
204
205 // One analytic ease-out and its inverse drive every passage/window time.
206 // Strength zero preserves linear travel exactly; strength one is quadratic.
207 const easeSweep = (u) =>
208 sweepPower === 1 ? clamp(u) : 1 - Math.pow(1 - clamp(u), sweepPower);
209 const inverseSweep = (u) =>
210 sweepPower === 1 ? clamp(u) : 1 - Math.pow(1 - clamp(u), 1 / sweepPower);
211 function cursorTimeAtX(x) {
212 const rank = (direction === 1 ? x - cursorLeft : W - x - cursorLeft) / cursorSpan;
213 return SWEEP_START + sweepDuration * inverseSweep(rank);
214 }
215 function cursorAt(time) {
216 const progress = easeSweep((time - SWEEP_START) / sweepDuration);
217 return {
218 x: cursorLeft + cursorSpan * (direction === 1 ? progress : 1 - progress),
219 y: H / 2,
220 z: cursorDepth,
221 };
222 }
223
224 function poseAt(tile, time) {
225 const t = clamp(time, 0, DURATION);
226 const progress = tile.active ? clamp((t - tile.flipStart) / tile.flipDuration) : 0;
227 const turn = smooth(progress / 0.64);
228 // Analytic damped oscillation: overshoot, return past 180, settle exactly.
229 // The attack/tail both have zero slope, so bounce=0 is a clean half-turn.
230 const ringT = clamp((progress - 0.64) / 0.36);
231 const ringEnvelope =
232 smooth(ringT / 0.12) * Math.exp(-3.3 * ringT) * (1 - smooth((ringT - 0.72) / 0.28));
233 const bounce = flipBounce * 36 * Math.sin(ringT * Math.PI * 3.3) * ringEnvelope;
234 const flight = Math.sin(Math.PI * progress) ** 0.7;
235 const cursor = cursorAt(t);
236 const radial = fieldAt(tile.cx, tile.cy, cursor.x, cursor.y);
237 const automatic = t >= SWEEP_START && t <= SWEEP_START + sweepDuration;
238 // A short approach replaces the long, separate anticipation phase.
239 const approach = smooth((t - tile.passTime + 0.2) / 0.16);
240 const pull =
241 (automatic ? radial * approach * (1 - smooth(progress / 0.32)) : 0) *
242 (tile.active ? 1 : 0.55);
243 const dx = cursor.x - tile.cx,
244 dy = cursor.y - tile.cy;
245 const variation = noiseStrength * Math.max(pull, flight * tile.peak);
246 // Continuous deterministic oscillations, not random-at-render-time jitter.
247 const n1 = Math.sin(t * 5.7 + tile.phase),
248 n2 = Math.sin(t * 10.3 + tile.phase * 1.71);
249 const jitter = variation * (n1 * 0.7 + n2 * 0.3);
250 const formation = Math.max(pull, flight * (0.55 + 0.45 * tile.peak));
251 // CSS positive Z points toward the camera. The prior 2D tilt signs
252 // depressed the cursor-facing edge despite lifting the tile center.
253 // A signed depth field now raises that edge for a positive cursor Z;
254 // moving the cursor through the plane smoothly reverses this torque.
255 const depthGain = cursor.z / 160;
256 const pullTiltX =
257 depthGain *
258 ((dy / cursorRadius) * tiltStrength * 1.6 * pull +
259 tile.seedX * tiltStrength * 0.16 * pull);
260 // Ink yaw cannot reverse against its half-turn after cursor passage.
261 // Release its seeded torque over 80 ms; blank tiles retain signed pull.
262 const yawDx = tile.active ? direction * Math.max(0, -direction * dx) : -dx;
263 const yawHandoff = tile.active ? 1 - smooth((t - tile.passTime + 0.04) / 0.12) : 1;
264 const pullTiltY =
265 depthGain *
266 ((yawDx / cursorRadius) * tiltStrength * 1.8 * pull +
267 tile.seedY * tiltStrength * 0.12 * pull) *
268 yawHandoff;
269 const pullZ = (tiltStrength / 30) * cursor.z * 0.45 * pull;
270 const rx = pullTiltX + tile.seedX * variation * 3 * flight + jitter;
271 const ry = direction * (180 * turn + bounce) + pullTiltY + jitter * 0.7;
272 const rz = tile.seedZ * variation * (2.3 * flight + 1.2 * pull) + jitter * 0.35;
273 return {
274 x: clamp(dx * 0.08 * pull, -tile.width * 0.18, tile.width * 0.18) + jitter * 0.2,
275 y: clamp(dy * 0.07 * pull, -tile.height * 0.11, tile.height * 0.11) + jitter * 0.16,
276 z: pullZ + 17 * flight * tile.peak,
277 cursorDepth: cursor.z,
278 pullTiltX,
279 pullTiltY,
280 pullZ,
281 rx,
282 ry,
283 rz,
284 turn,
285 bounce,
286 radial,
287 pull,
288 peak: tile.peak,
289 formation,
290 scale: 1 - (1 - formationScale) * formation,
291 shade: 1 - 0.12 * Math.abs(Math.sin((ry * Math.PI) / 180)) - 0.018 * formation,
292 };
293 }
294
295 function setRest(tile, rear) {
296 const rest = tile.active && rear ? 1 : 0;
297 if (tile.restState === rest) return;
298 tile.restState = rest;
299 surface.set(tile.index, {
300 x: 0,
301 y: 0,
302 z: 0,
303 rx: 0,
304 ry: rest * direction * 180,
305 rz: 0,
306 scale: 1,
307 formation: 0,
308 shade: 1,
309 });
310 }
311
312 function renderAt(time) {
313 if (disposed) return;
314 lastTime = clamp(time, 0, DURATION);
315 if (lastTime === lastRenderedTime) return;
316 lastRenderedTime = lastTime;
317 renderStats.evaluatedTiles = 0;
318 renderStats.drawCalls = 0;
319 renderStats.restTiles = 0;
320 const mode =
321 lastTime <= firstMotion ? "front" : lastTime >= lastMotion ? "rear" : "moving";
322 if (mode !== frameMode) {
323 frameMode = mode;
324 stage.style.visibility = mode === "moving" ? "visible" : "hidden";
325 flat.style.visibility = mode === "moving" ? "hidden" : "visible";
326 if (mode !== "moving")
327 flat.style.backgroundImage = `var(--csh-${mode === "front" ? "front" : "back"})`;
328 }
329 // One unsegmented atlas is shown only while EVERY tile is flat. This
330 // removes compositor hairlines in the workbench's scaled first frame.
331 // It is hidden for the whole active wave: no backing text or mask fade.
332 for (const tile of tiles) {
333 if (mode !== "moving" || lastTime < tile.motionStart || lastTime > tile.motionEnd) {
334 setRest(tile, mode === "rear" || lastTime > tile.flipStart + tile.flipDuration);
335 renderStats.restTiles++;
336 continue;
337 }
338 tile.restState = null;
339 surface.set(tile.index, poseAt(tile, lastTime));
340 renderStats.evaluatedTiles++;
341 }
342 if (mode === "moving") renderStats.drawCalls = surface.draw();
343 const copy =
344 lastTime < SWEEP_START + sweepDuration / 2 + flipDuration / 2
345 ? headline
346 : reverseHeadline;
347 if (accessible.textContent !== copy) accessible.textContent = copy;
348 stage.dataset.time = lastTime.toFixed(4);
349 }
350
351 function build() {
352 if (disposed) return;
353 const front = textSurface(headline),
354 back = textSurface(reverseHeadline);
355 root.style.setProperty("--csh-front", `url("${front.url}")`);
356 root.style.setProperty("--csh-back", `url("${back.url}")`);
357 let coveredFront = 0,
358 coveredBack = 0;
359 firstMotion = Infinity;
360 lastMotion = 0;
361 for (let row = 0; row < rows; row++)
362 for (let col = 0; col < columns; col++) {
363 // One exhaustive integer pixel partition. No alpha threshold, corner
364 // sampling, skipped row, or per-letter box can punch holes in the type.
365 const x = gridX + col * cellSize,
366 x1 = x + cellSize;
367 const y = gridY + row * cellSize,
368 y1 = y + cellSize;
369 const frontInk = countInk(front.ink, x, y, x1, y1),
370 backInk = countInk(back.ink, x, y, x1, y1);
371 coveredFront += frontInk;
372 coveredBack += backInk;
373 const cell = {
374 col,
375 row,
376 x,
377 y,
378 width: x1 - x,
379 height: y1 - y,
380 frontInk,
381 backInk,
382 active: frontInk + backInk > 0,
383 };
384 cells.push(cell);
385 // Build the whole square surface. White cells participate in radial
386 // lift/tilt at a restrained strength, but progress is locked to zero
387 // above so they never enter the headline's half-turn or bounce.
388 const cx = x + cell.width / 2,
389 cy = y + cell.height / 2;
390 const peak = fieldAt(cx, cy, cx, H / 2);
391 const passTime = cursorTimeAtX(cx);
392 const columnSeconds = Math.abs(
393 cursorTimeAtX(cx + cellSize / 2) - cursorTimeAtX(cx - cellSize / 2),
394 );
395 // The turn starts at passage, with at most 12 ms of seeded stagger.
396 const flipStart =
397 passTime +
398 hash(col, row, 269.5, 183.3) *
399 Math.min(0.012, columnSeconds * 0.08) *
400 Math.min(noiseStrength, 1);
401 const duration = flipDuration * (1 + 0.18 * (1 - peak));
402 const tile = {
403 ...cell,
404 index: tiles.length,
405 cx,
406 cy,
407 peak,
408 passTime,
409 flipStart,
410 flipDuration: duration,
411 phase: hash(col, row, 127.1, 311.7) * Math.PI * 2,
412 seedX: hash(col, row, 419.2, 371.9) * 2 - 1,
413 seedY: hash(col, row, 37.7, 631.5) * 2 - 1,
414 seedZ: hash(col, row, 97.3, 127.9) * 2 - 1,
415 };
416 // The Gaussian is exactly zero outside this circle. Precompute each
417 // cell's entry/exit times once, independent of playback history.
418 const dy = cy - H / 2;
419 let enter = Infinity,
420 exit = -Infinity;
421 if (Math.abs(dy) < fieldReach) {
422 const dx = Math.sqrt(fieldReach * fieldReach - dy * dy);
423 enter = Math.max(
424 SWEEP_START,
425 passTime - 0.2,
426 cursorTimeAtX(direction === 1 ? cx - dx : cx + dx),
427 );
428 exit = Math.min(
429 SWEEP_START + sweepDuration,
430 cursorTimeAtX(direction === 1 ? cx + dx : cx - dx),
431 );
432 }
433 tile.motionStart = Math.min(enter, tile.active ? flipStart : Infinity);
434 tile.motionEnd = Math.max(exit, tile.active ? flipStart + duration : -Infinity);
435 firstMotion = Math.min(firstMotion, tile.motionStart);
436 lastMotion = Math.max(lastMotion, tile.motionEnd);
437 if (tile.active) finishTime = Math.max(finishTime, flipStart + duration);
438 tiles.push(tile);
439 }
440 const inkTiles = tiles.filter((tile) => tile.active);
441 root.style.backgroundColor = behindColor;
442 surface = window.CodeSliceSurface(stage, front.canvas, back.canvas, tiles, {
443 behindColor,
444 shadowStrength,
445 shadowSoftness,
446 });
447 const state = { time: 0 };
448 timeline = gsap.timeline({ paused: true });
449 timeline.to(
450 state,
451 {
452 time: DURATION,
453 duration: DURATION,
454 ease: "none",
455 onUpdate: () => renderAt(state.time),
456 },
457 0,
458 );
459 timeline
460 .addLabel("seamless-front", 0)
461 .addLabel("cursor-sweep", SWEEP_START)
462 .addLabel("cursor-passed", SWEEP_START + sweepDuration)
463 .addLabel("seamless-rear", finishTime);
464 window.__timelines = window.__timelines || {};
465 window.__timelines["code-slice-hero"] = timeline;
466 window.__codeSliceHero = {
467 duration: DURATION,
468 cellSize,
469 columns,
470 rows,
471 activeCount: tiles.length,
472 flipCount: inkTiles.length,
473 backgroundCount: tiles.length - inkTiles.length,
474 copy: [headline, reverseHeadline],
475 fontSizes: [front.size, back.size],
476 finishTime,
477 coverage: {
478 front: { inkPixels: front.inkPixels, covered: coveredFront },
479 back: { inkPixels: back.inkPixels, covered: coveredBack },
480 },
481 cells,
482 cursorAt,
483 cursorTimeAtX,
484 sweepEaseStrength: sweepPower - 1,
485 renderStats,
486 shadowStats: surface.shadowStats,
487 behindColor,
488 renderer: "webgl2-instanced",
489 sample: (time) =>
490 tiles.map((tile) => ({
491 col: tile.col,
492 row: tile.row,
493 flips: tile.active,
494 passTime: tile.passTime,
495 flipStart: tile.flipStart,
496 flipDuration: tile.flipDuration,
497 ...poseAt(tile, time),
498 })),
499 renderAt,
500 };
501 renderAt(0);
502 }
503
504 // The cursor is imaginary and belongs exclusively to the timeline.
505 // No mouse/pointer input or wall-clock accumulation can affect a frame.
506 function seek(event) {
507 if (!timeline || typeof event.detail?.time !== "number") return;
508 const time = clamp(event.detail.time, 0, DURATION);
509 timeline.totalTime(time, true);
510 renderAt(time);
511 }
512 window.addEventListener("hf-seek", seek);
513 window.__codeSliceInstance = {
514 dispose() {
515 disposed = true;
516 timeline?.kill();
517 surface?.dispose();
518 window.removeEventListener("hf-seek", seek);
519 stage.replaceChildren();
520 tiles.length = 0;
521 cells.length = 0;
522 root.style.removeProperty("--csh-front");
523 root.style.removeProperty("--csh-back");
524 },
525 };
526 window.__codeSliceReady = document.fonts.load("700 420px SliceGeist").then(build);
527 })();
528 </script>
529 </body>
530</html>