mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-08-31 02:41:44 +00:00
* fix(docs): load the player from latest, not a pinned minor The catalog pages pinned the player CDN URL to a minor line, and that pin sat one line behind after the last release. Every page kept rendering, on the older build, so nothing surfaced it: the only symptom was that a fix published to npm never appeared on the docs. The generator derived its pin from the player's package.json, which is correct only if every page is regenerated on the release that moves it. That is the step that did not happen, and it has to happen across 175 generated pages plus three hand-written files for the pin to be true. A version carried in step across 178 places will be stale, and stale here is silent. Ask for latest instead and there is nothing to carry. This costs the ability to hold the docs back from a bad player release. Paid deliberately: the pin did not buy that either, it only delayed the good releases too. A test asserts no pinned version comes back, and fails if it stops finding the references at all, so it cannot pass by matching nothing. * refactor(scripts): list tracked files instead of walking the tree The pin guard hand-rolled a recursive directory walk with its own skip list and size cap, which the audit flagged: helpers living in a test file earn no coverage, so their complexity lands straight on the CRAP score. git already knows which files to read, and ignores node_modules and build output for us, so one call replaces the walker and both findings go away.
792 lines
26 KiB
Plaintext
792 lines
26 KiB
Plaintext
---
|
|
title: "Code Highlight Sweep"
|
|
description: "A highlight band sweeps across a target line while the surrounding context dims — draws the eye to one line."
|
|
---
|
|
|
|
import { InstallCommand } from "/snippets/install-command.jsx";
|
|
|
|
<iframe
|
|
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
|
title="code-highlight preview"
|
|
loading="lazy"
|
|
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@latest/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-highlight.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-highlight.png");document.body.appendChild(p)});<\/script></body></html>`}
|
|
/>
|
|
|
|
## Install
|
|
|
|
<InstallCommand command="npx hyperframes add code-highlight" item="code-highlight" />
|
|
|
|
That writes one file: `compositions/code-highlight.html`.
|
|
|
|
## Source
|
|
|
|
<Accordion title={`code-highlight.html`}>
|
|
|
|
```html
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=1920, height=1080" />
|
|
<title>Code Highlight Sweep</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
html,
|
|
body {
|
|
width: 1920px;
|
|
height: 1080px;
|
|
overflow: hidden;
|
|
background: #05070b;
|
|
}
|
|
#root {
|
|
position: relative;
|
|
width: 1920px;
|
|
height: 1080px;
|
|
font-family: "JetBrains Mono", monospace;
|
|
color: #e6edf3;
|
|
}
|
|
.bg-field {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: radial-gradient(1200px 700px at 50% 18%, #0e1726 0%, #070b12 55%, #05070b 100%);
|
|
}
|
|
.bg-grid {
|
|
position: absolute;
|
|
inset: 0;
|
|
background-image:
|
|
linear-gradient(rgba(88, 166, 255, 0.05) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(88, 166, 255, 0.05) 1px, transparent 1px);
|
|
background-size: 64px 64px;
|
|
mask-image: radial-gradient(1100px 620px at 50% 42%, #000 0%, transparent 78%);
|
|
}
|
|
.bg-glow {
|
|
position: absolute;
|
|
width: 900px;
|
|
height: 900px;
|
|
border-radius: 50%;
|
|
filter: blur(120px);
|
|
opacity: 0.5;
|
|
}
|
|
.bg-glow.a {
|
|
background: #1f6feb55;
|
|
left: -180px;
|
|
top: -200px;
|
|
}
|
|
.bg-glow.b {
|
|
background: #2ea04355;
|
|
right: -200px;
|
|
bottom: -260px;
|
|
}
|
|
.editor {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 1380px;
|
|
height: 800px;
|
|
background: #0b0f17;
|
|
border: 1px solid #1d2733;
|
|
border-radius: 16px;
|
|
box-shadow:
|
|
0 40px 120px rgba(0, 0, 0, 0.6),
|
|
0 2px 0 rgba(255, 255, 255, 0.03) inset;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.titlebar {
|
|
height: 56px;
|
|
flex: 0 0 56px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 0 22px;
|
|
background: linear-gradient(#11161f, #0c111a);
|
|
border-bottom: 1px solid #1b2430;
|
|
}
|
|
.dots {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
.dot {
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 50%;
|
|
}
|
|
.dot.r {
|
|
background: #ff5f57;
|
|
}
|
|
.dot.y {
|
|
background: #febc2e;
|
|
}
|
|
.dot.g {
|
|
background: #28c840;
|
|
}
|
|
.filename {
|
|
font-size: 19px;
|
|
color: #8b98a9;
|
|
letter-spacing: 0.2px;
|
|
}
|
|
.filename .accent {
|
|
color: #d6e2f0;
|
|
}
|
|
.code-surface {
|
|
position: relative;
|
|
flex: 1 1 auto;
|
|
overflow: hidden;
|
|
}
|
|
.scene {
|
|
position: absolute;
|
|
inset: 0;
|
|
opacity: 0;
|
|
will-change: opacity;
|
|
}
|
|
.code {
|
|
position: absolute;
|
|
top: 38px;
|
|
left: 0;
|
|
right: 0;
|
|
padding: 0 44px 0 104px;
|
|
font-size: 30px;
|
|
line-height: 46px;
|
|
font-variant-ligatures: none;
|
|
tab-size: 2;
|
|
}
|
|
.gutter {
|
|
position: absolute;
|
|
top: 38px;
|
|
left: 0;
|
|
width: 84px;
|
|
padding-right: 18px;
|
|
font-size: 30px;
|
|
line-height: 46px;
|
|
text-align: right;
|
|
color: #828c9b;
|
|
user-select: none;
|
|
}
|
|
.ln {
|
|
display: block;
|
|
height: 46px;
|
|
}
|
|
.line {
|
|
display: block;
|
|
min-height: 46px;
|
|
white-space: pre;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
.tok {
|
|
display: inline-block;
|
|
white-space: pre;
|
|
will-change: transform, opacity;
|
|
}
|
|
.caret {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 3px;
|
|
height: 32px;
|
|
margin-top: 6px;
|
|
background: #58a6ff;
|
|
border-radius: 1px;
|
|
z-index: 3;
|
|
}
|
|
.hl-box {
|
|
position: absolute;
|
|
background: rgba(88, 166, 255, 0.16);
|
|
border-left: 3px solid #58a6ff;
|
|
border-radius: 6px;
|
|
z-index: 0;
|
|
overflow: hidden;
|
|
}
|
|
.diff-line {
|
|
display: block;
|
|
min-height: 0;
|
|
line-height: 46px;
|
|
white-space: pre;
|
|
position: relative;
|
|
overflow: hidden;
|
|
padding-left: 14px;
|
|
}
|
|
.diff-sign {
|
|
display: inline-block;
|
|
width: 22px;
|
|
color: #41506220;
|
|
}
|
|
.diff-del {
|
|
background: rgba(248, 81, 73, 0.12);
|
|
box-shadow: inset 3px 0 #f85149;
|
|
}
|
|
.diff-add {
|
|
background: rgba(63, 185, 80, 0.12);
|
|
box-shadow: inset 3px 0 #3fb950;
|
|
}
|
|
.diff-del .diff-sign {
|
|
color: #f85149;
|
|
}
|
|
.diff-add .diff-sign {
|
|
color: #3fb950;
|
|
}
|
|
.flight-block {
|
|
position: absolute;
|
|
white-space: pre;
|
|
font-size: 27px;
|
|
line-height: 40px;
|
|
padding: 11px 22px;
|
|
border-radius: 11px;
|
|
background: #0f1722;
|
|
border: 1px solid #233044;
|
|
box-shadow: 0 16px 44px rgba(0, 0, 0, 0.5);
|
|
will-change: transform, opacity;
|
|
}
|
|
.flight-target {
|
|
position: absolute;
|
|
border: 1.5px dashed #2a3a4f;
|
|
border-radius: 14px;
|
|
opacity: 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div
|
|
id="root"
|
|
data-composition-id="code-highlight"
|
|
data-start="0"
|
|
data-duration="5"
|
|
data-width="1920"
|
|
data-height="1080"
|
|
>
|
|
<div class="bg-field" data-layout-ignore></div>
|
|
<div class="bg-grid" data-layout-ignore></div>
|
|
<div class="bg-glow a" data-layout-ignore></div>
|
|
<div class="bg-glow b" data-layout-ignore></div>
|
|
<div class="editor" id="editor" data-layout-ignore>
|
|
<div class="titlebar">
|
|
<div class="dots">
|
|
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
|
|
</div>
|
|
<span class="filename"
|
|
><span class="accent">loadConfig.js</span> — Code Highlight Sweep</span
|
|
>
|
|
</div>
|
|
<div class="code-surface" id="surface"></div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
window.__TOKENS = {
|
|
feature: {
|
|
lang: "js",
|
|
theme: "github-dark",
|
|
bg: "#24292e",
|
|
fg: "#e1e4e8",
|
|
states: [
|
|
{
|
|
code: 'async function loadConfig(path) {\n const raw = await readFile(path, "utf8")\n const config = JSON.parse(raw)\n return validate(config)\n}',
|
|
tokens: [
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-0",
|
|
content: "async",
|
|
color: "#F97583",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-1",
|
|
content: " ",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-2",
|
|
content: "function",
|
|
color: "#F97583",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-3",
|
|
content: " ",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-4",
|
|
content: "loadConfig",
|
|
color: "#B392F0",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-5",
|
|
content: "(",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-6",
|
|
content: "path",
|
|
color: "#FFAB70",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-7",
|
|
content: ") {",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-8",
|
|
content: "\n",
|
|
color: "#e1e4e8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-9",
|
|
content: " ",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-10",
|
|
content: "const",
|
|
color: "#F97583",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-11",
|
|
content: " ",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-12",
|
|
content: "raw",
|
|
color: "#79B8FF",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-13",
|
|
content: " ",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-14",
|
|
content: "=",
|
|
color: "#F97583",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-15",
|
|
content: " ",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-16",
|
|
content: "await",
|
|
color: "#F97583",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-17",
|
|
content: " ",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-18",
|
|
content: "readFile",
|
|
color: "#B392F0",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-19",
|
|
content: "(path,",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-20",
|
|
content: " ",
|
|
color: "#e1e4e8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-21",
|
|
content: '"utf8"',
|
|
color: "#9ECBFF",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-22",
|
|
content: ")",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-23",
|
|
content: "\n",
|
|
color: "#e1e4e8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-24",
|
|
content: " ",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-25",
|
|
content: "const",
|
|
color: "#F97583",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-26",
|
|
content: " ",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-27",
|
|
content: "config",
|
|
color: "#79B8FF",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-28",
|
|
content: " ",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-29",
|
|
content: "=",
|
|
color: "#F97583",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-30",
|
|
content: " ",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-31",
|
|
content: "JSON",
|
|
color: "#79B8FF",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-32",
|
|
content: ".",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-33",
|
|
content: "parse",
|
|
color: "#B392F0",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-34",
|
|
content: "(raw)",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-35",
|
|
content: "\n",
|
|
color: "#e1e4e8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-36",
|
|
content: " ",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-37",
|
|
content: "return",
|
|
color: "#F97583",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-38",
|
|
content: " ",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-39",
|
|
content: "validate",
|
|
color: "#B392F0",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-40",
|
|
content: "(config)",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-41",
|
|
content: "\n",
|
|
color: "#e1e4e8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-42",
|
|
content: "}",
|
|
color: "#E1E4E8",
|
|
fontStyle: 0,
|
|
},
|
|
{
|
|
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-43",
|
|
content: "\n",
|
|
color: "#e1e4e8",
|
|
fontStyle: 0,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
window.__BLOCK = {
|
|
id: "code-highlight",
|
|
effect: "highlight",
|
|
seq: "feature",
|
|
line: 1,
|
|
duration: 5,
|
|
};
|
|
</script>
|
|
<script>
|
|
// hf-code-anim.js — self-contained code-animation engine for HyperFrames registry blocks.
|
|
//
|
|
// Reuses Shiki Magic Move's baked output (window.__TOKENS, baked at author time) and
|
|
// drives the animation as a PAUSED GSAP timeline the HyperFrames engine seeks per frame —
|
|
// NOT CSS transitions (those break under non-sequential frame seeking). FLIP measurement
|
|
// runs once inside document.fonts.ready (glyph metrics must be final). Each block sets
|
|
// window.__BLOCK = { id, effect, seq, line?, duration } and includes this engine; the
|
|
// engine builds one scene that fills the composition and registers window.__timelines[id].
|
|
(function () {
|
|
"use strict";
|
|
var T = window.__TOKENS || {};
|
|
|
|
function lineDiv() {
|
|
var d = document.createElement("div");
|
|
d.className = "line";
|
|
return d;
|
|
}
|
|
|
|
// Render a code state's keyed tokens into codeEl as lines of inline-block .tok spans.
|
|
function renderState(codeEl, state) {
|
|
codeEl.textContent = "";
|
|
var keyToEl = new Map();
|
|
var line = lineDiv();
|
|
codeEl.appendChild(line);
|
|
for (var i = 0; i < state.tokens.length; i++) {
|
|
var t = state.tokens[i];
|
|
if (t.content === "\n") {
|
|
line = lineDiv();
|
|
codeEl.appendChild(line);
|
|
continue;
|
|
}
|
|
var span = document.createElement("span");
|
|
span.className = "tok";
|
|
span.textContent = t.content;
|
|
span.style.color = t.color;
|
|
if (t.fontStyle & 1) span.style.fontStyle = "italic";
|
|
if (t.fontStyle & 2) span.style.fontWeight = "700";
|
|
line.appendChild(span);
|
|
keyToEl.set(t.key, span);
|
|
}
|
|
var last = codeEl.lastChild;
|
|
if (last && last.className === "line" && last.children.length === 0)
|
|
codeEl.removeChild(last);
|
|
return keyToEl;
|
|
}
|
|
|
|
function rectsOf(keyToEl) {
|
|
var m = new Map();
|
|
keyToEl.forEach(function (el, k) {
|
|
var r = el.getBoundingClientRect();
|
|
m.set(k, { left: r.left, top: r.top });
|
|
});
|
|
return m;
|
|
}
|
|
|
|
function makeScene(surface, withGutter) {
|
|
var scene = document.createElement("div");
|
|
scene.className = "scene";
|
|
if (withGutter) {
|
|
var g = document.createElement("div");
|
|
g.className = "gutter";
|
|
scene.appendChild(g);
|
|
}
|
|
var code = document.createElement("div");
|
|
code.className = "code";
|
|
scene.appendChild(code);
|
|
surface.appendChild(scene);
|
|
return { scene: scene, code: code, gutter: scene.querySelector(".gutter") };
|
|
}
|
|
|
|
function fillGutter(g, n) {
|
|
var html = "";
|
|
for (var i = 1; i <= n; i++) html += '<span class="ln">' + i + "</span>";
|
|
g.innerHTML = html;
|
|
}
|
|
|
|
function splitLines(state) {
|
|
var lines = [{ text: "", tokens: [] }];
|
|
for (var i = 0; i < state.tokens.length; i++) {
|
|
var t = state.tokens[i];
|
|
if (t.content === "\n") {
|
|
lines.push({ text: "", tokens: [] });
|
|
continue;
|
|
}
|
|
var cur = lines[lines.length - 1];
|
|
cur.tokens.push(t);
|
|
cur.text += t.content;
|
|
}
|
|
if (lines.length && lines[lines.length - 1].tokens.length === 0) lines.pop();
|
|
return lines;
|
|
}
|
|
|
|
function lineDiff(a, b) {
|
|
var n = a.length;
|
|
var m = b.length;
|
|
var dp = [];
|
|
for (var i = 0; i <= n; i++) dp.push(new Array(m + 1).fill(0));
|
|
for (var i = n - 1; i >= 0; i--) {
|
|
for (var j = m - 1; j >= 0; j--) {
|
|
dp[i][j] =
|
|
a[i].text === b[j].text
|
|
? dp[i + 1][j + 1] + 1
|
|
: Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
}
|
|
}
|
|
var ops = [];
|
|
var i = 0;
|
|
var j = 0;
|
|
while (i < n && j < m) {
|
|
if (a[i].text === b[j].text) {
|
|
ops.push({ type: "same", line: b[j] });
|
|
i++;
|
|
j++;
|
|
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
ops.push({ type: "del", line: a[i] });
|
|
i++;
|
|
} else {
|
|
ops.push({ type: "add", line: b[j] });
|
|
j++;
|
|
}
|
|
}
|
|
while (i < n) ops.push({ type: "del", line: a[i++] });
|
|
while (j < m) ops.push({ type: "add", line: b[j++] });
|
|
return ops;
|
|
}
|
|
|
|
// ---- single-scene effect builders (fade in, play, hold to end) -------------
|
|
var FADE = 0.45;
|
|
|
|
function highlight(tl, surface, state, start, targetIdx) {
|
|
var SWEEP = 0.9;
|
|
var parts = makeScene(surface, true);
|
|
var code = parts.code;
|
|
renderState(code, state);
|
|
var lines = code.querySelectorAll(".line");
|
|
fillGutter(parts.gutter, lines.length);
|
|
var base = code.getBoundingClientRect();
|
|
var idx = Math.max(0, Math.min(lines.length - 1, targetIdx));
|
|
var lr = lines[idx].getBoundingClientRect();
|
|
var box = document.createElement("div");
|
|
box.className = "hl-box";
|
|
box.style.left = lr.left - base.left - 8 + "px";
|
|
box.style.top = lr.top - base.top - 1 + "px";
|
|
box.style.height = lr.height + 2 + "px";
|
|
code.insertBefore(box, code.firstChild);
|
|
tl.set(parts.scene, { opacity: 0 }, 0);
|
|
tl.to(parts.scene, { opacity: 1, duration: FADE, ease: "power1.out" }, start);
|
|
var at = start + FADE + 0.2;
|
|
tl.fromTo(
|
|
box,
|
|
{ width: 0, opacity: 0 },
|
|
{ width: lr.width + 18, opacity: 1, duration: SWEEP, ease: "power2.inOut" },
|
|
at,
|
|
);
|
|
for (var i = 0; i < lines.length; i++) {
|
|
if (i === idx) continue;
|
|
tl.fromTo(
|
|
lines[i],
|
|
{ opacity: 1 },
|
|
{ opacity: 0.45, duration: SWEEP, ease: "power2.inOut" },
|
|
at,
|
|
);
|
|
}
|
|
}
|
|
|
|
function buildEffect(tl, surface, spec) {
|
|
var start = 0.45;
|
|
var seq = T[spec.seq];
|
|
if (spec.effect === "highlight")
|
|
highlight(tl, surface, seq.states[0], start, spec.line || 0);
|
|
}
|
|
|
|
function go() {
|
|
var spec = window.__BLOCK;
|
|
var surface = document.getElementById("surface");
|
|
var root = document.getElementById("root");
|
|
window.__timelines = window.__timelines || {};
|
|
var tl = gsap.timeline({ paused: true });
|
|
// Build inside fonts.ready (glyph metrics must be final), then register.
|
|
// Register ONLY after the timeline is fully built: the runtime's
|
|
// sub-composition readiness gate treats "key present" as "ready" and
|
|
// nests the child once. An empty timeline registered before this build
|
|
// would be nested empty and never re-nested → blank render when used as
|
|
// a sub-composition. See the contract in engine frameCapture.ts.
|
|
document.fonts.ready.then(function () {
|
|
tl.from("#editor", { opacity: 0, scale: 0.985, duration: 0.5, ease: "power2.out" }, 0);
|
|
buildEffect(tl, surface, spec);
|
|
var dur = parseFloat(root.dataset.duration) || tl.duration();
|
|
tl.to({}, { duration: dur }, 0); // pad to full composition length
|
|
window.__timelines["code-highlight"] = tl; // register AFTER setup completes
|
|
if (typeof window.__hfForceTimelineRebind === "function") {
|
|
window.__hfForceTimelineRebind(); // re-nest now that we are populated
|
|
}
|
|
});
|
|
}
|
|
|
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", go);
|
|
else go();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
</Accordion>
|
|
|
|
## Usage
|
|
|
|
After installing, add the block to your host composition:
|
|
|
|
```html
|
|
<div data-composition-id="code-highlight" data-composition-src="compositions/code-highlight.html" data-start="0" data-duration="5" data-track-index="1" data-width="1920" data-height="1080"></div>
|
|
```
|
|
|
|
{/* hf:generated-footer */}
|
|
|
|
Tagged `code` `code-animation` `highlight` `developer`.
|
|
|
|
## Related topics
|
|
|
|
- [Browse the complete Catalog](/catalog)
|
|
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
|
- [Build a richer composition](/go-further)
|