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