mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-02 12:08:50 +00:00
feat(registry): the video-primitive moves, documented and customisable (#3090)
* feat(registry): add the video-primitive moves, and rebuild the catalog around them
Adds the motion primitives: 277 new components and the blocks that go with
them, plus the ui-primitives, themes and generators they are produced by. The
registry index goes from 176 items to 454, and the search catalog is rebuilt so
the set that is ranked is the set that can be installed.
Additive on purpose. An earlier pass of this port used rsync --delete, which
removed 101 files that exist on main because the incoming set is not a superset
of the current one: beat-freeze-cut and camcorder-hud among them. Whether the
re-port replaces those or sits alongside them is a product decision and not one
a sync flag should make, so nothing is removed here. If any of them are meant
to go, that belongs in its own commit where it can be seen.
The generator is ported too. Main's version only scans examples, so running it
without this change silently rewrote the index down to nine items. It also
rewrites example manifests from templates.json and will overwrite hand-edits;
those were reverted here after each run, and the diff is worth reading rather
than trusting.
Not covered. The 445 moves are not individually reviewed in this commit; the
machinery that ranks and installs them landed separately so it could be read on
its own. The internal evaluation corpus is deliberately absent: it is 1,400
files of briefs, gold labels and verdicts, and this repository is public.
* docs(catalog): publish the primitive and component pages
Adds the Mintlify pages for the moves this PR ships: 163 component pages, 13
primitive pages, and the navigation that lists them. Without these the moves
land installable and undocumented, which is the worse half of a catalog.
Three things left out deliberately.
The 78 MB of docs/public. Nothing references it: every page loads its preview
from static.heygen.ai, so those bytes would be weight in a public repo with no
reader. Checked rather than assumed, by grepping the pages for the path.
Pages for the thirteen moves that were specified and never built. They had
documentation but no registry item, so a reader would have followed a page to a
`hyperframes add` that fails. Their nav entries are pruned with them, and every
one of the 309 remaining catalog and primitive nav entries was verified to point
at a page that exists.
Spike and scratch files that sit alongside the real docs on the source branch:
qa-gallery.html, experiment pages, bundled player javascript. They are working
artifacts, not documentation.
Not covered: the pages are generated output and have not been read individually.
The nav is verified to resolve, and the previews load from a CDN this commit
does not control, so a broken image would show up in review rather than here.
* docs: list the primitive and component pages in the site navigation
The pages this PR adds were unreachable: nothing in docs.json pointed at them.
This appends a Motion primitives group to the existing Catalog tab and a
Primitives tab, both built from main's navigation rather than replacing it.
Copying the source branch's docs.json wholesale was the first attempt and was
wrong. That file describes a different site, tabs Documentation / Catalog /
Primitives / Packages / SDK / Reference against main's Guides / Studio /
Catalog / Developers, and it references pages only that branch has, so the
preview server reported six dead links.
Verified by running the preview and resolving every entry: 484 page refs, 0
dead, no warnings. Group-relative refs are why a flat existence check is the
wrong validator here: cursor resolves through catalog/components and mcp
through guides, so checking docs/<ref>.mdx flat pruned 22 entries that were
fine.
* fix(registry): restore what the port took from main's components
Two regressions this branch introduced into items main already ships. Both were
found by the repo's own gates in packages/cli, not by reading the diff, and
neither is visible to the no-deletions check: no file was deleted, the contents
of files were changed.
The four liquid-glass blocks stopped installing their library. main lists
lib/liquid-glass.iife.js as a second file on each; the port wrote the older
manifest over main's and dropped that entry. The file is still in git and still
on disk, it simply stopped being something `hyperframes add` writes, so the
installed composition's <script src="lib/liquid-glass.iife.js"> would have
resolved to nothing. Every one of the 294 registry-item.json files this branch
touches was then audited against main: these four lost a file entry, and no
item lost a top-level key.
Fourteen caption components gained an empty <video>. The port added
`<video id="wp-video" ...></video>` — no src, no <source> — to each component
and its demo. It renders nothing and the registry linter rejects it as
media_missing_src. Removed rather than given a placeholder, because main's
version of each of these composes over whatever the host composition provides,
so the element only ever added a broken node; a made-up src would ship a
reference to footage that does not exist.
The removal is deliberately surgical. Four of the fourteen also carry a
substantial rewrite from the port, and only the media element and the rule that
styled it are touched, so a blunt revert cannot take the rewrite with it.
Verified: 2540 CLI tests pass, `bun run lint` exits 0. Before this, three tests
failed.
* docs(catalog): play the real composition, and show what can be changed
Four changes to generate-catalog-pages.ts, so all 445 pages stay consistent
rather than 445 files being edited by hand.
The preview plays the composition instead of pointing at a video. Every new
page pointed at static.heygen.ai/<name>.mp4 and every one of those answered
403, so the reader got a black box where the whole point of the page is to
show them the thing. The objects were never uploaded and rendering 445 of them
would have to happen again on every change. The player is already the thing
being documented, so the page embeds it: the item's directory is copied under
docs/public and an iframe loads it through a small wrapper. 444 of 445 pages
play; the remaining one is a texture item that uses its own preview panel.
The iframe is not decoration. Compositions set styles on `body`, so dropping
the element straight into the MDX would put a composition's global CSS in the
same document as the documentation around it.
Three things this got wrong first, all found by opening the page rather than
reading the output:
- The wrapper loaded itself. `../<dir>/<name>.html` from inside preview/<dir>/
resolves back into preview/<dir>/. The player embedded the player and the
frame went black with a second set of controls shrinking into the corner.
- Copying only demo.html was not enough. Most demos are a mount shell whose
child carries data-composition-src="./<name>.html", so the sibling has to
come with it. Every URL answered 200 and the frame was still empty.
- `autoplay` and `loop` are not player attributes. Writing them did nothing
and every preview sat paused on frame 0 — which is blank for any
composition that animates in. The wrapper drives play() and loops on
`ended` instead.
The Variables table. generateParams reads `params`; every item ported from the
video-primitives work declares `variables`, a richer schema with a type, a
default and a range. 112 items carry one and not a single page showed it, so
the most useful thing on the page was the one thing missing.
Nav groups. `if (entry.type === "component") return "Effects"` was the
catch-all, so Effects held 267 of 445 pages: an alphabetical wall. Rules keyed
on tags that already exist in the manifests split it; the largest group is now
73.
An install command with a visible copy button. A plain code fence renders one
on hover only, and it was absent from the accessibility tree entirely. This is
the one line every reader comes to take. navigator.clipboard is unavailable on
insecure origins, which is exactly the local preview these pages are written
against, so the fallback path is load-bearing and is what was exercised in
testing.
Verified: 888 preview URLs fetched, 0 failures. Regenerating three times in a
row produces no change, after a first attempt where "Variables" was added to
GENERATED_HEADINGS with a capital V — the set is compared lowercased, so each
run carried the previous section forward and appended a new one.
Not covered: the 445 pages were not read individually. Coverage here is that
every preview resolves and that a page from each of the block and component
paths was opened and watched.
* docs(catalog): put the code on the page
A reviewer with no stake in the work compared these pages against shadcn/ui's
component pages and motion.dev, and returned one gap: the pages carry almost no
code, so they are pointers to a file the reader does not have yet. Its sharpest
example was the Variables table — names, defaults and accepted values, headed
"set the ones you want to change on the element", on a page that never shows an
element or the syntax for setting anything on one.
Two additions, in the generator so all 445 pages get them.
A snippet under the Variables table: the real mount element with
data-variable-values filled in from the item's own defaults, so it is
copy-and-run correct before it is edited. That is the syntax the demos actually
use, not an illustration written for the page.
The item's source, in a collapsed Accordion. These files run 99 to 463 lines,
so inlining them raw would bury everything else; collapsed, the code is on the
page and one click away. Accordion is already what these docs use for this.
A second reviewer, fresh, confirmed the change landed: it called the table and
snippet actionable rather than filler and said the collapsed source earns its
place.
Also here: the preview retries play() until the clock moves. `ready` can flip
before the runtime the player injects for a mounted sub-composition has finished
wiring up, and a play() landing in that window silently does nothing.
Both reviewers additionally reported every preview frozen at 0:00 and called it
fatal. It is not. The player's clock runs on requestAnimationFrame
(direct-timeline-clock.ts), browsers suspend rAF in a hidden tab, and the
reviewing tab was hidden: document.visibilityState read "hidden" while the
player reported ready and not paused, and a one-second rAF loop never completed
a single tick. Seeking the same composition by hand renders it correctly at any
offset. So the retry stops after ~15s instead of spinning forever, and the
comment says why an automated check of a background tab will always read 0.
Not covered: the reviewers' other standing finding, that only some items carry
variables at all, so the pages do not have one shape. 125 of the 206 items
tagged as a primitive declare none, and giving them variables means authoring
them into each composition, not editing metadata.
Verified: lint exits 0, the no-deletions gate passes, nav resolves 598 refs with
0 dead, and regenerating three times running changes nothing.
* feat(registry): give 55 primitives variables that actually do something
The catalog pages listed variables for 112 of 454 items and nothing for the
rest, so most pages could show a reader what a piece looks like but not what
they could change about it. This adds them to 55 more, taking the count to 167.
These are not metadata. A variable is only real if the composition reads it, so
each one is declared on the root, validated in the composition's own script, and
wired to something visible: travel distance, blur radius, direction, density,
accent family, tone, label text. Declaring a knob the code ignores would put a
table in the published docs that lies about the piece, which is worse than
having no table.
Every one falls back to its declared default when the incoming value is missing
or unrecognised, so a bad override degrades to the shipped look rather than to a
broken frame. With no overrides at all, each item renders exactly as it did
before: that was checked per item against `git show HEAD:` in a real browser,
comparing computed styles rather than eyeballing.
Four things this ran into that are worth writing down.
An apostrophe anywhere in a description terminates the single-quoted
data-composition-variables attribute and breaks the HTML parse. Every
declaration in the registry now parses; that is checked, not assumed.
Where a timeline drives GSAP's own y/scale/filter, GSAP writes inline styles
that beat any CSS custom property, so those knobs cannot be won from the
composition. Most of these items keep their motion in a user-owned "Timeline
integration" comment rather than in code, so no timing variables were declared
for them at all. A direction knob on a wipe can still be wired honestly, by
remapping clip-path inset sides through multipliers whose defaults reproduce the
original exactly.
Colour tokens that only reach a :focus-visible outline, or an element sitting at
opacity 0 at rest, render identically in a video. Those were skipped rather than
shipped as knobs that appear to do nothing.
CSS shorthand defaults need care: `border: var(--x, 0 solid transparent)` moves
computed border-color off currentColor even at zero width. Defaults were chosen
to reproduce the original computed style, not merely to look equivalent.
Not covered: 72 primitives still have no variables, and the UI-primitive demos
that scripts/sync-ui-primitives.ts mirrors are now stale for the converted
items. Nothing runs that script in CI today.
Verified: every declaration parses and deep-equals its manifest array, every
declared id is read by the composition, no demo.html changed, and lint exits 0.
* feat(registry): variables for 16 more primitives, and stop the snippet clipping
Takes the count from 167 to 183 of 454. Same contract as the last batch: each
variable is declared on the root, validated in the composition's own script,
and wired to something visible, because a knob the code ignores would put a
table in the published docs that lies about the piece.
The snippet under each Variables table was clipping. Its data-variable-values
payload is one long line and the code block cut it off mid-value, with no wrap
and no scrollbar, so the one line on the page that exists to be copied could not
be read. The fence now carries `wrap`. Worth noting how that survived: the
generated markdown was correct and every mechanical check passed. It only failed
in a browser, which is where it was eventually seen.
Two techniques this round that are worth keeping.
Where an accent has a themed token family, the knob sets a new custom property
consumed by that one surface, with a fallback to the existing token, rather than
overriding the shared accent. Default therefore sets nothing, so an externally
themed accent is not clobbered, and the non-default options still follow the
theme in both light and dark.
Where GSAP owns the property outright and no CSS multiply can win — number-wheel
animates `y` inline — the knob is wired at build time instead: extra revolutions
lengthen the digit strip and move the target, so travel changes while the resting
frame stays identical. That is a real answer rather than a skipped knob.
Motion knobs that multiply a timeline-driven custom property collapse to identity
at rest, so every one of them was verified with that property pinned to a
mid-flight value rather than at t=0, where all options look the same by
construction.
Not covered: 56 primitives still have no variables.
Verified: every declaration parses and deep-equals its manifest array, every
declared id is read by the composition, no demo.html changed, lint exits 0, and
the wrap fix was confirmed on the rendered page rather than in the markdown.
* feat(registry): variables for 21 more primitives
Takes the count from 183 to 204 of 454. Same contract: declared on the root,
validated in the composition script, wired to something visible, defaults
reproducing the pre-edit render exactly.
Three kinds of knob were turned down this round rather than faked, and the
reasons are worth keeping.
A knob that contradicts its own motion. The sheet panel could be moved to the
left, but the recipe drives GSAP x from the right, so the panel would slide in
from the wrong side while the control claimed otherwise.
A knob that needs two defaults. A separator length means width horizontally and
height vertically, so one token would be wrong half the time.
An option that is not an option. Two components were given a green accent that
probed byte-identical to their default, because the theme accent already is that
token. A row in the docs table that does nothing is worse than a missing row, so
it was replaced with one that differs.
Accent knobs set a new property with a fallback to the shared token rather than
overriding it, verified by rendering with an external accent in place and
confirming the default still yields to it. Motion knobs multiply a
timeline-driven property so they never fight the inline styles GSAP writes;
because those collapse to identity at rest, each was checked twice, once at rest
against HEAD and once with the driven property pinned mid-flight.
Verified: every declaration parses and deep-equals its manifest array, every
declared id is read, defaults match HEAD on computed styles and on a pixel hash
of the rendered element, no demo.html changed, lint exits 0, and the
no-deletions gate passes.
* feat(registry): variables for 4 more primitives, and make manifests agree with their compositions
Takes the count to 207 of 454.
Four items carried a different description for their exit variable in
registry-item.json than in their own data-composition-variables. The catalog page
renders the manifest, so the published table described the knob one way while the
composition header described it another. The composition wins: it is the file
that implements the variable and the declaration is what the runtime reads.
The skill docs no longer describe a hosted tier, since the CLI now ships the two
local tiers only, and skills-manifest.json is regenerated to match.
* feat(registry): variables for 4 more primitives
Takes the count to 211 of 454.
Two knobs are worth calling out because they touch things the timeline also
touches. skeleton-block slide multiplies the driven row offset, so it is
identity at rest and only bends the middle of the move. slider value sets the
resting fill together with the readout text, aria-valuenow and aria-valuetext,
so all three agree; a composition that tweens the fill takes over from there and
owns the readout, which the comment header states plainly rather than hiding. A
multiplier was rejected there because a 0 to 1 tween would push the fill past
the end of the track.
Knobs on elements that sit at opacity 0 at rest were kept only where the shipped
recipe reveals them, and verified with the reveal forced on as well as at rest.
Skipped: an aria-label string knob that never renders, and an accent token
declared in one item CSS that nothing consumes.
* refactor(registry): drop the UI primitives, this is a video catalog
Removes 66 items tagged ui-primitive: accordions, buttons, inputs, dialogs, a
calendar. They are a shadcn-style interface component set that happens to be
expressible as HTML. None of them animate anything, so in a catalog whose job is
to offer moves for video they widen the surface without making it more useful,
and each one is a page a reader has to skip past to reach something that moves.
Every one is new on this branch. None exists on main, so nothing main ships is
being taken away; that was confirmed against origin/main before deleting rather
than assumed, and the no-deletions gate still passes.
Removed with them: registry/ui-primitives, the Operator Black token and contract
files only these items consumed, and the tooling that maintained them
(sync-ui-primitives.ts and scripts/lib/ui-primitives). No other registry item
declares a dependency on any of the 66, so nothing else loses a piece. The now
empty UI Primitives navigation rule goes too.
Generated output is pruned with the sources. The page generator writes files but
never removes ones whose source has gone, so a stale page would have survived and
404d its own preview. Verified: 0 orphan pages, 0 orphan previews, and the
navigation resolves 532 references with none dead.
This does discard variables authored for 54 of them earlier on this branch. That
work is in the history if these ever come back, and it is the right trade: they
should not have been in a video catalog to begin with.
The catalog is now 388 items. Lint exits 0 and 2522 CLI tests pass.
* feat(registry): every motion and transition primitive is now customisable
The last 16 primitives get variables, so none is left without them. 173 of 388
items now declare variables; the rest are blocks and showcases, which are whole
scenes rather than parameterised moves.
Same contract throughout: declared on the root, validated in the composition
script, wired to something visible, and falling back to the declared default on
missing or unrecognised input. With no overrides every item renders exactly as
it did before, verified per item against the pre-change render in a real browser
at rest and at pinned mid-flight states, comparing computed styles and rects and
in most cases a screenshot hash.
This round refused several knobs rather than shipping ones that only look real.
A tilt-card depth knob was written, measured, and thrown away: the card sets
overflow hidden, which forces transform-style flat, so the authored translateZ
is already inert and every option probed identical. It ships a glow knob
instead, which drives inset and visibly changes at rest and under the drift.
slot-machine-roll has no free travel knob because the roll is exactly one row
height and any multiplier lands the reel off-register; size scales row height
and roll distance together, which is the only honest version. soft-blur-in
offers up and down but not left and right, because the shipped tween resets y
and not x, so a horizontal offset would never animate away.
Two pre-existing bugs surfaced while checking honestly, both left alone as out
of scope but worth recording. zoom-through-transition and tracking-in each tween
a custom property that is never set, so CSS reads it as zero and the move starts
from zero rather than from its authored value. The depth and tracking knobs are
scoped around that and their headers say so, rather than pretending the tween is
what it appears to be.
Verified: 388 items, 0 primitives without variables, 0 items where the manifest
and the composition disagree, 0 declared-but-unread variables, nav resolves 532
references with none dead, no demo.html changed, lint exits 0, and the
no-deletions gate passes.
* feat(registry): raise the catalog quality bar, and add eight primitives
Cuts 37 components, adds 8, and writes down the standard both decisions were
made against.
The 37 removals are all new on this branch and absent from main, so nothing
shipped is withdrawn. Each was audited with two pieces of evidence: source
identity after name normalisation, and a composition-level contact sheet
showing the members animate identically.
The largest group was 13 files byte-identical apart from an h3 and one
sentence. Nothing marqueed, panned, zoomed, deployed or dragged. An honesty
tiebreak decided survivors: frosted-glass-wipe has no backdrop-filter,
spring-scale-in has no spring, masked-slide-reveal has no mask,
short-slide-right travels up, and three-particle-ribbon differs from
three-orbiting-cards by one number while having neither particles nor a
ribbon.
Two independent audits agreed 10 out of 10 on a shared calibration sample,
in both directions, including three items a first pass wrongly condemned.
The rubric is the durable part. Fatal criteria are separated from fixable
ones, because no-timeline alone hits 97 items including some of the best;
promoting it would have cut 97 and left a worse catalog. It also records the
harness rules that make a verdict reproducible: render from the composition
rather than the demo, since demos carry content the installed item does not,
and mount sub-compositions rather than inlining them, since inlining renders
black frames indistinguishable from a dead item.
The eight additions target measured gaps. Camera language ranked first
because PSNR across 30 reference demos showed the most impressive
environments barely move: they are sets, not shots.
camera-shake carries nine lens-accurate profiles, amplitude scaled by focal
length so a wide lens shakes differently from a telephoto. rack-focus splats
each light through the aperture shape, so a defocused point becomes an image
of the iris, with flux conserved so highlights survive defocus.
camera-dolly-zoom solves focal length from distance, holding subject size to
0.000 percent drift while the background grows 53 percent. Plus
oscilloscope-trace with history-free phosphor persistence, bar-chart-race,
split-flap-board, spiral-galaxy and vfx-anamorphic-flare.
Each is verified by rendered frames and a seek-equals-playback check rather
than by check passing, which is not a visual gate.
* fix(registry): let the split-flap board finish flipping on screen
The board declared 8s but every flap had settled by 3.5s, so more than half the composition was a still frame and the catalog preview opened on it.
* fix(registry): keep the thread-message-stack payload parseable
A line wrap had put literal newlines inside the JSON string literals of the blocks data-hf-primitive-data payload, so JSON.parse threw in the browser and the composition never ran. The preview script hid it: it rewrote the payload in the temporary copy it captured from, so the catalog picture looked right while every installed copy stayed broken. That repair pass is gone and the payload is fixed where it ships.
Its two tests could not have caught this. Both were written against vitest in a directory the repo runs with node:test, so neither was in test:scripts and neither had ever run. They are converted and registered, along with a new one that JSON.parses every payload in the registry, and that one was checked against the re-wrapped shape before being kept.
* fix(registry): close the apostrophe that truncated a variables declaration
chromatic-aberration-wipe described its accent as "the incoming scene's gradient" inside a single-quoted data-composition-variables attribute, so the attribute ended mid-JSON and the tag never closed. The formatter refused to parse the file, which is how it surfaced, but the runtime would have read a truncated declaration.
Also formats the 159 registry and docs files the branch had left unformatted, regenerates the skills manifest, and drops docs/primitives: those 13 pages import /snippets/PrimitivePlayer.jsx and read docs/public/primitives/, neither of which is on this branch, so mint failed the build on them. Nothing links to them and they ship whole on feat-video-primitives.
CI ran test:scripts before building core, so the preview test added here failed on a missing dist rather than on anything it checks. It now runs after the builds.
* fix(registry): make the review findings real fixes
Ten items declared variables on their composition root but had no variables key in the manifest, so their generated pages shipped no explorer at all. Their manifests now mirror the root. Two more disagreed only in description text, and the root was the truthful side: both compositions paint an inset ring, not the slabs or colour pair the manifest described.
The caption <video> removal left 24 CSS rules addressing elements that no longer exist. Removed, excluding the four ids that were already orphaned on main.
thread-message-stack could not stay fixed: oxfmt reflows a divs contents and lands a newline inside a JSON string literal, so the payload broke again on the next format. A script tag is not an option because the runtime strips every script out of the mounted clone. The reader normalizes HTML whitespace instead, which is what makes it survive any reflow, and the guard test now asserts that contract rather than the byte layout.
downloadFile had lost its 30s timeout, DownloadOptions, and the mid-pipeline error plumbing in a rewrite that was only meant to fix redirects. Five callers were left with no stall guard. Restored, redirect handling kept.
warnUnknownEnumValues re-did the parse readDeclaredDefaults had already done. Both now share one readDeclarations, and the rest splits into compositionLabel, declaredOptions and unknownEnumValue. 1909 core tests unchanged.
Deletes build-qa-gallery, theme-gate and generate-primitive-pages: nothing invokes them, two read a coverage map four directories above the repo root, and the pages the third generates are no longer on this branch. Wires check-no-main-deletions, which is the opposite case, real and tested and never run.
* fix(registry): stop shipping a stale copy of the catalog-search work
This branch carried re-authored copies of the CLI search commits rather than the ones on their own PR, so merging it would have rolled back six later fixes: the vector cache that refuses a half download, the 0o700/0o600 modes, the rebuilt-from-registry index generator, the coverage gate and its CI job, and the scripts typecheck. Those files now come from that branch.
registry.json still listed 64 items whose directories the UI-primitive removal deleted, so hyperframes add would resolve a name and then fail on missing files. Regenerated from disk: 358 searchable items, 358 vectors, gate green.
Also drops an internal provenance block from thread-message-stack, along with the type and the two JSON schemas that existed only to describe it. It published an artifact id, a version id and a heygenverse:// URI, none of which mean anything to someone installing a block, and a public registry is the wrong place for them.
Typechecking scripts/ for the first time surfaced 45 errors in this branch. Fixed rather than suppressed: the geometry test reads positions through one accessor that names a missing index instead of letting NaN reach a tolerance compare, and the null-returning shape helper is asserted at its call sites, except in the test whose subject is the null.
* refactor(scripts): split the page builder into its numbered sections
generateItemMdx had grown to cyclomatic 26 across 196 lines while its own comments already named the seams. previewSection, usageSection and footerSection now own one each, taking it to 13. Regenerating all 358 pages afterwards produces a byte-identical tree, which is the check that matters for a generator.
* fix(cli): repair what the cross-branch file take broke
Taking files wholesale from the catalog-search branch reverted the downloadFile timeout restored one commit earlier, so five callers were back to no stall guard at HEAD. Restored on both branches this time, since that branch never had it either.
It also took that branch test:scripts line without the vitest it depends on, so the script exited 127 and the CI Test job would have failed on a missing binary rather than a test. vitest is a root devDependency now, and the run is scoped to scripts/catalog/ with the slash: without it the prefix also matched catalog-preview-temp.test.ts, a node:test file with no vitest suite in it. Both branches had that one.
Four registry items and their docs copies carried absolute paths from a working directory. A public registry is the wrong place for them and history is permanent, so the sentences now name the source without the path.
Skill docs came from before the code they describe: the catalog command reports unindexed and applies installability after ranking, and both SKILL.md files now say so.
Also drops a double type assertion and ten dead ?? NaN coalesces from the geometry test, the second of which reintroduced exactly the NaN-into-a-tolerance-compare that the checked accessor exists to prevent.
* fix(ci): resolve core from source and take only item directories
The scripts typecheck failed on generate-registry-items importing @hyperframes/core by package name. It resolves on a machine with a warm node_modules, which is why it passed locally, and not in CI. Every other script in the directory already imports core from source and says why in a comment.
The preview job derived its item list with a sed that needs a trailing slash, so registry/components/CATALOG.md never matched, survived as a full path, and was handed to the renderer as an item name. The grep now requires a directory component. Simulated against this PR: 219 items, none of them a path.
* refactor(registry): load gsap from the cdn like every other item
store-badge-lockup vendored gsap 3.14.2 as a 4,200 line minified file and installed it into the users project, while 540 other items load that exact version from jsDelivr. Repointed, the copy deleted and the manifest entry with it, so hyperframes add store-badge-lockup no longer writes a second copy of gsap into someone elses compositions directory. Re-rendered and re-generated: the preview still draws.
* feat(registry): swap in the detailed device models
Replaces the iPhone and MacBook models in the three device blocks. The old assets were untitled meshes with no keyboard on the laptop; these name every part and model the keycaps, speaker grilles, antenna bands and camera plateau.
Not a drop-in. The compositions found the screen by side effects, the material that happened to carry an emissiveMap for the phone and a mesh literally called matte for the laptop, and neither exists now. They select front-glass and display instead.
Both panels ship UVs authored for a tiling material, the laptop runs u -6.3 to 6.3, so one screen image clamped and smeared across the panel. Planar UVs are derived from each panel bounds at load.
The old phone display sat at the model minimum Z and the timeline spins assume a screen facing -Z. These face +Z, so the model is aligned by reading which of its own parts is front rather than re-timing the animation.
Removes the hand-drawn Apple logo from two blocks: the replacement ships apple-logo meshes, and the drawn one used coordinates read off the old lid, so it floated beside the device.
The preview copy only took top level files, so models/, lib/ and assets/ never reached docs/public and 38 items rendered there without their assets. That is the source of the non-blocking 404s in the preview job. It recurses now, which also brings vendored bundles across, so the generated tree is out of the lint scope.
The html-in-canvas notice is a Danger callout: without the flag the preview is a black rectangle, which is a prerequisite rather than a caveat.
* chore(registry): rebase onto the merged catalog search
This branch carried its own copy of the catalog-search work so that merging it in either order could not regress the other. That copy is now the older one: main has the consent fix, the contributor path for someone without the embedding model, the restored download test and the corrected gate message. Every file main owns is taken from main, and the three duplicated CLI commits are dropped rather than replayed.
Regenerated afterwards, because the registry it describes has changed: registry.json, the vector index, the catalog pages and the skills manifest.
* fix(scripts): stop the rebase reverting the preview pipeline
Resolving the rebase in favour of this branch took three files whose newer versions had already merged, so the branch quietly reverted them.
generate-catalog-previews.ts lost encodeForWeb, which exists because publishing masters directly put 25 Mbps files on the docs CDN and one 20-second preview was 60 MB. It also lost the ffmpeg transcode, so a jpeg capture was being written straight to a .png path while the comment above still said it transcoded, and it lost openOpaqueCapture, re-creating the second copy of a capture setup that was extracted precisely to stop there being two. This PR renders previews for over 200 items, so all three shipped at scale.
scripts/tsconfig.json regained exclusions that hole the gate, and generate-template-previews.ts went back to importing the producer by package name, which is the CI failure that import was changed to fix.
All three are taken from main. Also drops an alignScreenToMinusZ copied into the laptop block, which has no front and back to compare and never called it, and makes the preview copy lstat so a symlinked directory cannot send it outside the repo.
* fix(registry): clear the five items this PR added that the linter rejects
The registry linter is not wired into CI, so five items this PR adds were shipping with real render defects nobody would have seen fail.
caption-camera-follow and grade-split-reveal styled their root by its own class. Sub-composition CSS is scoped to [data-composition-id=...] <selector>, so a selector whose leftmost part is the root class becomes a descendant selector and stops matching the root: the scene renders unstyled at render time while looking correct in every static check and in preview. Both now key off the attribute the scoper already adds.
logo-brand-close tweened letterSpacing, which the browser snaps to integer device pixels, so the ease-out tail stutters under seek-by-frame capture. It is a scaleX now.
terminal-simulator named SFMono-Regular, which the renderer cannot resolve, so the text silently fell back.
oversized-cursor was a false positive: the rule scans raw source for head tags and a literal one written inside a JS comment paired with the real closing tag. Confirmed against a render, nothing leaks into frame, so the comment says head element rather than the tag.
Also stops generate-registry-items.ts dropping catalogArtifact.revision. build-local-vectors.ts stamps it so the CLI and the coverage gate can tell whether the published vectors still describe this registry; regenerating the item list erased it, and the gate then failed until someone rebuilt the index.
41 items still fail the linter, every one of them pre-existing on main.
This commit is contained in:
@@ -70,9 +70,13 @@ jobs:
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
# Find which blocks/components changed in this PR
|
||||
# Find which blocks/components changed in this PR. The trailing slash
|
||||
# in the grep matters: an item is a directory, and a file sitting
|
||||
# directly under registry/components (CATALOG.md) does not match the
|
||||
# sed, so without it the whole path survives and is passed to the
|
||||
# renderer as if it were an item name.
|
||||
CHANGED_ITEMS=$(git diff --name-only --diff-filter=ACMR "$BASE_SHA"...HEAD -- registry/blocks/ registry/components/ \
|
||||
| grep -E '^registry/(blocks|components)/' \
|
||||
| grep -E '^registry/(blocks|components)/[^/]+/' \
|
||||
| sed 's|^registry/[^/]*/\([^/]*\)/.*|\1|' \
|
||||
| sort -u)
|
||||
|
||||
|
||||
@@ -293,10 +293,13 @@ jobs:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
- run: bun run test:scripts
|
||||
- run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
|
||||
- run: bun run --cwd packages/core build
|
||||
- run: bun run --cwd packages/core build:hyperframes-runtime
|
||||
# After the builds: the preview scripts drive the producer, which imports
|
||||
# @hyperframes/core from dist, so running these first fails on a missing
|
||||
# module rather than on anything the test is actually checking.
|
||||
- run: bun run test:scripts
|
||||
- run: bun run --filter '!@hyperframes/producer' test
|
||||
|
||||
producer-source-tests:
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"node_modules/",
|
||||
"playground/",
|
||||
"packages/producer/tests/**/src/vendor/**",
|
||||
"docs/public/catalog/",
|
||||
"registry/blocks/**/lib/*.iife.js",
|
||||
"chrome/"
|
||||
],
|
||||
@@ -34,6 +35,9 @@
|
||||
},
|
||||
{
|
||||
"files": ["docs/snippets/**/*.jsx", "docs/snippets/**/*.tsx"],
|
||||
"globals": {
|
||||
"CodeBlock": "readonly"
|
||||
},
|
||||
"rules": {
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"packages/aws-lambda": {
|
||||
"name": "@hyperframes/aws-lambda",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.103",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.700.0",
|
||||
"@aws-sdk/client-sfn": "^3.700.0",
|
||||
@@ -57,7 +57,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@hyperframes/cli",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.103",
|
||||
"bin": {
|
||||
"hyperframes": "./bin/hyperframes.mjs",
|
||||
},
|
||||
@@ -107,7 +107,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@hyperframes/core",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.103",
|
||||
"dependencies": {
|
||||
"@chenglou/pretext": "^0.0.5",
|
||||
"@hyperframes/lint": "workspace:*",
|
||||
@@ -132,7 +132,7 @@
|
||||
},
|
||||
"packages/engine": {
|
||||
"name": "@hyperframes/engine",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.103",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^2.0.5",
|
||||
"@hyperframes/core": "workspace:^",
|
||||
@@ -151,7 +151,7 @@
|
||||
},
|
||||
"packages/gcp-cloud-run": {
|
||||
"name": "@hyperframes/gcp-cloud-run",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.103",
|
||||
"dependencies": {
|
||||
"@google-cloud/storage": "^7.14.0",
|
||||
"@google-cloud/workflows": "^4.2.0",
|
||||
@@ -172,7 +172,7 @@
|
||||
},
|
||||
"packages/lint": {
|
||||
"name": "@hyperframes/lint",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.103",
|
||||
"dependencies": {
|
||||
"@hyperframes/parsers": "workspace:*",
|
||||
"htmlparser2": "^10.1.0",
|
||||
@@ -190,7 +190,7 @@
|
||||
},
|
||||
"packages/parsers": {
|
||||
"name": "@hyperframes/parsers",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.103",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.0",
|
||||
"acorn": "^8.17.0",
|
||||
@@ -210,7 +210,7 @@
|
||||
},
|
||||
"packages/player": {
|
||||
"name": "@hyperframes/player",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.103",
|
||||
"dependencies": {
|
||||
"@hyperframes/core": "workspace:*",
|
||||
},
|
||||
@@ -225,7 +225,7 @@
|
||||
},
|
||||
"packages/producer": {
|
||||
"name": "@hyperframes/producer",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.103",
|
||||
"dependencies": {
|
||||
"@fontsource/archivo-black": "^5.2.8",
|
||||
"@fontsource/eb-garamond": "^5.2.7",
|
||||
@@ -270,7 +270,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@hyperframes/sdk",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.103",
|
||||
"dependencies": {
|
||||
"@hyperframes/core": "workspace:*",
|
||||
"@hyperframes/parsers": "workspace:*",
|
||||
@@ -300,7 +300,7 @@
|
||||
},
|
||||
"packages/shader-transitions": {
|
||||
"name": "@hyperframes/shader-transitions",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.103",
|
||||
"dependencies": {
|
||||
"html2canvas": "^1.4.1",
|
||||
},
|
||||
@@ -312,7 +312,7 @@
|
||||
},
|
||||
"packages/studio": {
|
||||
"name": "@hyperframes/studio",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.103",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.20.1",
|
||||
"@codemirror/commands": "^6.10.3",
|
||||
@@ -362,7 +362,7 @@
|
||||
},
|
||||
"packages/studio-server": {
|
||||
"name": "@hyperframes/studio-server",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.103",
|
||||
"dependencies": {
|
||||
"@hyperframes/core": "workspace:*",
|
||||
"@hyperframes/parsers": "workspace:*",
|
||||
|
||||
@@ -3,34 +3,979 @@ title: "App Showcase"
|
||||
description: "Fitness app product showcase with three floating smartphone screens"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/app-showcase.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/app-showcase.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/app-showcase.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="App Showcase preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add app-showcase
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add app-showcase" />
|
||||
|
||||
That writes one file: `compositions/app-showcase.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 5.5 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`app-showcase.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>app-showcase</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background-color: #0a0a0f;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
data-composition-id="app-showcase"
|
||||
data-composition-src="compositions/app-showcase.html"
|
||||
data-start="0"
|
||||
data-duration="5.5"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="5.5"
|
||||
>
|
||||
<!-- Background -->
|
||||
<div class="s3-bg" id="s3Bg"></div>
|
||||
<div class="s3-bg-glow" id="s3BgGlow"></div>
|
||||
|
||||
<!-- Stage wrapper for ambient scale -->
|
||||
<div class="s3-stage" id="s3Stage">
|
||||
<!-- ═══════════════════════════════════════
|
||||
LEFT PHONE — Lifestyle / Motivational
|
||||
═══════════════════════════════════════ -->
|
||||
<div class="s3-phone s3-phone-left" id="s3PhoneLeft">
|
||||
<div class="s3-bezel">
|
||||
<div class="s3-notch"></div>
|
||||
<div class="s3-screen s3-screen-left">
|
||||
<!-- Abstract exercise shapes -->
|
||||
<div class="left-shapes">
|
||||
<svg viewBox="0 0 304 400" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
<!-- Abstract running figure — geometric -->
|
||||
<circle cx="152" cy="80" r="28" fill="#271f15" opacity="0.15" />
|
||||
<line
|
||||
x1="152"
|
||||
y1="108"
|
||||
x2="152"
|
||||
y2="200"
|
||||
stroke="#271f15"
|
||||
stroke-width="6"
|
||||
stroke-linecap="round"
|
||||
opacity="0.15"
|
||||
/>
|
||||
<line
|
||||
x1="152"
|
||||
y1="145"
|
||||
x2="115"
|
||||
y2="175"
|
||||
stroke="#271f15"
|
||||
stroke-width="6"
|
||||
stroke-linecap="round"
|
||||
opacity="0.15"
|
||||
/>
|
||||
<line
|
||||
x1="152"
|
||||
y1="145"
|
||||
x2="195"
|
||||
y2="165"
|
||||
stroke="#271f15"
|
||||
stroke-width="6"
|
||||
stroke-linecap="round"
|
||||
opacity="0.15"
|
||||
/>
|
||||
<line
|
||||
x1="152"
|
||||
y1="200"
|
||||
x2="120"
|
||||
y2="260"
|
||||
stroke="#271f15"
|
||||
stroke-width="6"
|
||||
stroke-linecap="round"
|
||||
opacity="0.15"
|
||||
/>
|
||||
<line
|
||||
x1="152"
|
||||
y1="200"
|
||||
x2="190"
|
||||
y2="255"
|
||||
stroke="#271f15"
|
||||
stroke-width="6"
|
||||
stroke-linecap="round"
|
||||
opacity="0.15"
|
||||
/>
|
||||
<!-- Decorative arcs -->
|
||||
<path
|
||||
d="M60 300 Q152 240 244 300"
|
||||
stroke="#e4fa72"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
opacity="0.5"
|
||||
/>
|
||||
<path
|
||||
d="M40 330 Q152 260 264 330"
|
||||
stroke="#e4fa72"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
opacity="0.3"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="left-bottom">
|
||||
<div class="left-tagline" id="s3Tagline">Unleash Full Potential</div>
|
||||
<div class="left-cta">START NOW</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════
|
||||
CENTER PHONE — Dashboard (Hero)
|
||||
═══════════════════════════════════════ -->
|
||||
<div class="s3-phone s3-phone-center" id="s3PhoneCenter">
|
||||
<div class="s3-bezel">
|
||||
<div class="s3-notch"></div>
|
||||
<div class="s3-screen s3-screen-center">
|
||||
<!-- Header -->
|
||||
<div class="center-header">
|
||||
<div class="center-avatar">JM</div>
|
||||
<div class="center-user-info">
|
||||
<div class="center-name">James Medrano</div>
|
||||
<div class="center-subtitle">Premium Member</div>
|
||||
</div>
|
||||
<div class="center-bell">
|
||||
<svg viewBox="0 0 20 20" width="20" height="20" fill="none">
|
||||
<path
|
||||
d="M10 2a5 5 0 00-5 5v3l-1.5 2.5h13L15 10V7a5 5 0 00-5-5z"
|
||||
stroke="#7c857c"
|
||||
stroke-width="1.4"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 16.5a2 2 0 004 0"
|
||||
stroke="#7c857c"
|
||||
stroke-width="1.4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<circle cx="14" cy="5" r="3" fill="#e4fa72" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Weekly Goal -->
|
||||
<div class="center-goal">
|
||||
<div class="goal-label">Weekly Goal</div>
|
||||
<div class="goal-ring-wrap">
|
||||
<svg class="goal-ring" viewBox="0 0 140 140">
|
||||
<circle
|
||||
class="ring-track"
|
||||
cx="70"
|
||||
cy="70"
|
||||
r="52"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="10"
|
||||
/>
|
||||
<circle
|
||||
class="ring-fill"
|
||||
id="s3Ring"
|
||||
cx="70"
|
||||
cy="70"
|
||||
r="52"
|
||||
fill="none"
|
||||
stroke="#e4fa72"
|
||||
stroke-width="10"
|
||||
stroke-linecap="round"
|
||||
transform="rotate(-90 70 70)"
|
||||
/>
|
||||
</svg>
|
||||
<div class="goal-count">
|
||||
<span class="goal-num">2</span>
|
||||
<span class="goal-sep">/</span>
|
||||
<span class="goal-den">3</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goal-days">
|
||||
<span class="day done">M</span>
|
||||
<span class="day done">T</span>
|
||||
<span class="day">W</span>
|
||||
<span class="day">T</span>
|
||||
<span class="day">F</span>
|
||||
<span class="day">S</span>
|
||||
<span class="day">S</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Your Progress -->
|
||||
<div class="center-progress">
|
||||
<div class="progress-header">
|
||||
<span class="progress-title">Your Progress</span>
|
||||
<span class="progress-link">See all</span>
|
||||
</div>
|
||||
<div class="progress-bars">
|
||||
<div class="bar-row">
|
||||
<div class="bar-icon">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="none">
|
||||
<path
|
||||
d="M8 2l1.5 3H13l-2.5 2 1 3L8 8.5 4.5 10l1-3L3 5h3.5z"
|
||||
fill="#e4fa72"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="bar-info">
|
||||
<span class="bar-name">Running</span>
|
||||
<span class="bar-val">5.2 km</span>
|
||||
</div>
|
||||
<div class="bar-track"><div class="bar-value bar-v1" id="s3Bar1"></div></div>
|
||||
</div>
|
||||
<div class="bar-row">
|
||||
<div class="bar-icon">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="none">
|
||||
<circle cx="8" cy="8" r="5" stroke="#e4fa72" stroke-width="2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="bar-info">
|
||||
<span class="bar-name">Cycling</span>
|
||||
<span class="bar-val">12.8 km</span>
|
||||
</div>
|
||||
<div class="bar-track"><div class="bar-value bar-v2" id="s3Bar2"></div></div>
|
||||
</div>
|
||||
<div class="bar-row">
|
||||
<div class="bar-icon">
|
||||
<svg viewBox="0 0 16 16" width="14" height="14" fill="none">
|
||||
<rect
|
||||
x="3"
|
||||
y="6"
|
||||
width="10"
|
||||
height="6"
|
||||
rx="1"
|
||||
stroke="#e4fa72"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path d="M6 6V4a2 2 0 014 0v2" stroke="#e4fa72" stroke-width="1.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="bar-info">
|
||||
<span class="bar-name">Strength</span>
|
||||
<span class="bar-val">45 min</span>
|
||||
</div>
|
||||
<div class="bar-track"><div class="bar-value bar-v3" id="s3Bar3"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════
|
||||
RIGHT PHONE — Calories Data
|
||||
═══════════════════════════════════════ -->
|
||||
<div class="s3-phone s3-phone-right" id="s3PhoneRight">
|
||||
<div class="s3-bezel">
|
||||
<div class="s3-notch"></div>
|
||||
<div class="s3-screen s3-screen-right">
|
||||
<div class="right-header">
|
||||
<div class="right-title">Burned Calories</div>
|
||||
<div class="right-value">740 <span class="right-unit">kcal</span></div>
|
||||
</div>
|
||||
|
||||
<div class="right-graph-wrap">
|
||||
<svg class="right-graph" viewBox="0 0 260 130" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Grid lines -->
|
||||
<line
|
||||
x1="10"
|
||||
y1="25"
|
||||
x2="250"
|
||||
y2="25"
|
||||
stroke="#d9e0bc"
|
||||
stroke-width="0.5"
|
||||
opacity="0.5"
|
||||
/>
|
||||
<line
|
||||
x1="10"
|
||||
y1="55"
|
||||
x2="250"
|
||||
y2="55"
|
||||
stroke="#d9e0bc"
|
||||
stroke-width="0.5"
|
||||
opacity="0.5"
|
||||
/>
|
||||
<line
|
||||
x1="10"
|
||||
y1="85"
|
||||
x2="250"
|
||||
y2="85"
|
||||
stroke="#d9e0bc"
|
||||
stroke-width="0.5"
|
||||
opacity="0.5"
|
||||
/>
|
||||
<line
|
||||
x1="10"
|
||||
y1="115"
|
||||
x2="250"
|
||||
y2="115"
|
||||
stroke="#d9e0bc"
|
||||
stroke-width="0.5"
|
||||
opacity="0.5"
|
||||
/>
|
||||
<!-- Area fill -->
|
||||
<path
|
||||
class="graph-area"
|
||||
id="s3GraphArea"
|
||||
d="M10,105 L50,80 90,90 130,45 170,60 210,25 250,35 250,115 10,115 Z"
|
||||
fill="#e4fa72"
|
||||
opacity="0.15"
|
||||
/>
|
||||
<!-- Line -->
|
||||
<polyline
|
||||
class="graph-line"
|
||||
id="s3GraphLine"
|
||||
points="10,105 50,80 90,90 130,45 170,60 210,25 250,35"
|
||||
fill="none"
|
||||
stroke="#e4fa72"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<!-- Dots -->
|
||||
<circle class="graph-dot" cx="10" cy="105" r="3.5" fill="#e4fa72" />
|
||||
<circle class="graph-dot" cx="50" cy="80" r="3.5" fill="#e4fa72" />
|
||||
<circle class="graph-dot" cx="90" cy="90" r="3.5" fill="#e4fa72" />
|
||||
<circle class="graph-dot" cx="130" cy="45" r="3.5" fill="#e4fa72" />
|
||||
<circle class="graph-dot" cx="170" cy="60" r="3.5" fill="#e4fa72" />
|
||||
<circle class="graph-dot" cx="210" cy="25" r="3.5" fill="#e4fa72" />
|
||||
<circle class="graph-dot" cx="250" cy="35" r="3.5" fill="#e4fa72" />
|
||||
</svg>
|
||||
<div class="right-labels">
|
||||
<span>Mon</span><span>Tue</span><span>Wed</span><span>Thu</span><span>Fri</span
|
||||
><span>Sat</span><span>Sun</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-stats">
|
||||
<div class="stat-pill">
|
||||
<span class="stat-dot" style="background: #e4fa72"></span>
|
||||
<span class="stat-label">Active</span>
|
||||
<span class="stat-num">540</span>
|
||||
</div>
|
||||
<div class="stat-pill">
|
||||
<span class="stat-dot" style="background: #7c857c"></span>
|
||||
<span class="stat-label">Rest</span>
|
||||
<span class="stat-num">200</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap");
|
||||
|
||||
[data-composition-id="app-showcase"] {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
font-family: "DM Sans", sans-serif;
|
||||
color: #271f15;
|
||||
}
|
||||
|
||||
/* ── Background ── */
|
||||
[data-composition-id="app-showcase"] .s3-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(160deg, #f1f2ec 0%, #e8ecda 40%, #f1f2ec 100%);
|
||||
}
|
||||
[data-composition-id="app-showcase"] .s3-bg-glow {
|
||||
position: absolute;
|
||||
width: 900px;
|
||||
height: 900px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: radial-gradient(circle, rgba(228, 250, 114, 0.18) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* ── Stage ── */
|
||||
[data-composition-id="app-showcase"] .s3-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
/* ── Phones ── */
|
||||
[data-composition-id="app-showcase"] .s3-phone {
|
||||
position: absolute;
|
||||
width: 320px;
|
||||
height: 650px;
|
||||
top: 215px;
|
||||
left: 800px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .s3-phone-center {
|
||||
z-index: 30;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .s3-phone-left {
|
||||
z-index: 20;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .s3-phone-right {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
/* ── Bezel ── */
|
||||
[data-composition-id="app-showcase"] .s3-bezel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 42px;
|
||||
background: #271f15;
|
||||
padding: 8px;
|
||||
box-shadow:
|
||||
0 24px 64px rgba(39, 31, 21, 0.35),
|
||||
0 4px 16px rgba(39, 31, 21, 0.15);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .s3-notch {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100px;
|
||||
height: 26px;
|
||||
background: #271f15;
|
||||
border-radius: 0 0 18px 18px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* ── Screens ── */
|
||||
[data-composition-id="app-showcase"] .s3-screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 36px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Left screen */
|
||||
[data-composition-id="app-showcase"] .s3-screen-left {
|
||||
background: linear-gradient(160deg, #d9e0bc 0%, #c8d4a0 50%, #b8c78a 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .left-shapes {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .left-shapes svg {
|
||||
width: 200px;
|
||||
height: 280px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .left-bottom {
|
||||
padding: 0 28px 40px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .left-tagline {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #271f15;
|
||||
line-height: 1.15;
|
||||
margin-bottom: 16px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .left-cta {
|
||||
display: inline-block;
|
||||
background: #271f15;
|
||||
color: #e4fa72;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
padding: 10px 28px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
/* Center screen */
|
||||
[data-composition-id="app-showcase"] .s3-screen-center {
|
||||
background: #1a1a1a;
|
||||
padding: 42px 22px 22px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
[data-composition-id="app-showcase"] .center-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .center-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #e4fa72, #d9e0bc);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #271f15;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .center-user-info {
|
||||
flex: 1;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .center-name {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #f1f2ec;
|
||||
line-height: 1.2;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .center-subtitle {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #7c857c;
|
||||
margin-top: 1px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .center-bell {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Weekly Goal */
|
||||
[data-composition-id="app-showcase"] .center-goal {
|
||||
background: #242424;
|
||||
border-radius: 20px;
|
||||
padding: 18px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .goal-label {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #7c857c;
|
||||
align-self: flex-start;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .goal-ring-wrap {
|
||||
position: relative;
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .goal-ring {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .ring-track {
|
||||
opacity: 0.3;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .goal-count {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .goal-num {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: #e4fa72;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .goal-sep {
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
color: #7c857c;
|
||||
margin: 0 2px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .goal-den {
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
color: #7c857c;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .goal-days {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .day {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #7c857c;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .day.done {
|
||||
background: #e4fa72;
|
||||
color: #271f15;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Progress section */
|
||||
[data-composition-id="app-showcase"] .center-progress {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .progress-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #f1f2ec;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .progress-link {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #e4fa72;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .progress-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .bar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .bar-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: #2a2a2a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .bar-info {
|
||||
width: 70px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .bar-name {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #f1f2ec;
|
||||
line-height: 1.2;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .bar-val {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
color: #7c857c;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .bar-track {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: #2a2a2a;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .bar-value {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
background: #e4fa72;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
/* Right screen */
|
||||
[data-composition-id="app-showcase"] .s3-screen-right {
|
||||
background: #f1f2ec;
|
||||
padding: 42px 20px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .right-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .right-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #7c857c;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .right-value {
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
color: #271f15;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .right-unit {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
color: #7c857c;
|
||||
}
|
||||
|
||||
[data-composition-id="app-showcase"] .right-graph-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .right-graph {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .right-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
color: #7c857c;
|
||||
}
|
||||
|
||||
[data-composition-id="app-showcase"] .right-stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .stat-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #e8ecda;
|
||||
border-radius: 20px;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .stat-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .stat-label {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #7c857c;
|
||||
}
|
||||
[data-composition-id="app-showcase"] .stat-num {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #271f15;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var S = '[data-composition-id="app-showcase"] ';
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
// ── SVG setup ──
|
||||
var graphLine = document.querySelector(S + "#s3GraphLine");
|
||||
var graphLen = graphLine.getTotalLength();
|
||||
gsap.set(graphLine, { strokeDasharray: graphLen, strokeDashoffset: graphLen });
|
||||
|
||||
var graphArea = document.querySelector(S + "#s3GraphArea");
|
||||
gsap.set(graphArea, { opacity: 0 });
|
||||
|
||||
var graphDots = document.querySelectorAll(S + ".graph-dot");
|
||||
gsap.set(graphDots, { opacity: 0, scale: 0 });
|
||||
|
||||
var CIRC = 2 * 3.14159265 * 52;
|
||||
gsap.set(S + "#s3Ring", { strokeDasharray: CIRC, strokeDashoffset: CIRC });
|
||||
|
||||
// ── Initial states ──
|
||||
gsap.set(S + ".s3-bg", { opacity: 0 });
|
||||
gsap.set(S + ".s3-bg-glow", { opacity: 0 });
|
||||
// Center phone: starts scaled down + hidden
|
||||
gsap.set(S + "#s3PhoneCenter", { opacity: 0, scale: 0.85 });
|
||||
// Side phones: start stacked behind center (same position), hidden
|
||||
gsap.set(S + "#s3PhoneLeft", { opacity: 0, x: 0, rotation: 0 });
|
||||
gsap.set(S + "#s3PhoneRight", { opacity: 0, x: 0, rotation: 0 });
|
||||
gsap.set(S + ".bar-value", { width: "0%" });
|
||||
gsap.set(S + "#s3Tagline", { opacity: 0, y: 20 });
|
||||
gsap.set(S + ".left-cta", { opacity: 0, y: 12 });
|
||||
|
||||
// ════════════════════════════════════
|
||||
// BUILD PHASE (0–2s)
|
||||
// ════════════════════════════════════
|
||||
|
||||
// Background
|
||||
tl.to(S + ".s3-bg", { opacity: 1, duration: 0.8, ease: "power1.out" }, 0);
|
||||
tl.to(S + ".s3-bg-glow", { opacity: 1, duration: 1.2, ease: "power1.out" }, 0.2);
|
||||
|
||||
// Center phone — scales up first
|
||||
tl.to(
|
||||
S + "#s3PhoneCenter",
|
||||
{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: 0.7,
|
||||
ease: "back.out(1.4)",
|
||||
},
|
||||
0.2,
|
||||
);
|
||||
|
||||
// Side phones: opacity matches center (0.7s, back.out), fan starts at 0.1
|
||||
tl.to(S + "#s3PhoneLeft", { opacity: 1, duration: 0.7, ease: "back.out(1.4)" }, 0.3);
|
||||
tl.to(
|
||||
S + "#s3PhoneLeft",
|
||||
{
|
||||
x: -340,
|
||||
rotation: -12,
|
||||
duration: 1.2,
|
||||
ease: "expo.out",
|
||||
},
|
||||
0.3,
|
||||
);
|
||||
|
||||
tl.to(S + "#s3PhoneRight", { opacity: 1, duration: 0.7, ease: "back.out(1.4)" }, 0.4);
|
||||
tl.to(
|
||||
S + "#s3PhoneRight",
|
||||
{
|
||||
x: 340,
|
||||
rotation: 12,
|
||||
duration: 1.2,
|
||||
ease: "expo.out",
|
||||
},
|
||||
0.4,
|
||||
);
|
||||
|
||||
// ════════════════════════════════════
|
||||
// BREATHE PHASE (2–4s)
|
||||
// ════════════════════════════════════
|
||||
|
||||
// Progress ring fills to 2/3
|
||||
tl.to(
|
||||
S + "#s3Ring",
|
||||
{
|
||||
strokeDashoffset: CIRC * (1 - 2 / 3),
|
||||
duration: 1.2,
|
||||
ease: "circ.out",
|
||||
},
|
||||
1.8,
|
||||
);
|
||||
|
||||
// Activity bars stagger in
|
||||
tl.to(S + "#s3Bar1", { width: "75%", duration: 0.8, ease: "power3.out" }, 2.1);
|
||||
tl.to(S + "#s3Bar2", { width: "50%", duration: 0.8, ease: "power3.out" }, 2.3);
|
||||
tl.to(S + "#s3Bar3", { width: "90%", duration: 0.8, ease: "power3.out" }, 2.5);
|
||||
|
||||
// Graph line draws on
|
||||
tl.to(
|
||||
graphLine,
|
||||
{
|
||||
strokeDashoffset: 0,
|
||||
duration: 1.4,
|
||||
ease: "sine.inOut",
|
||||
},
|
||||
2.0,
|
||||
);
|
||||
|
||||
// Graph area fades in
|
||||
tl.to(graphArea, { opacity: 0.15, duration: 0.8, ease: "power1.out" }, 2.6);
|
||||
|
||||
// Graph dots pop in
|
||||
tl.to(
|
||||
graphDots,
|
||||
{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.3,
|
||||
ease: "back.out(2)",
|
||||
stagger: { each: 0.06, ease: "power2.in" },
|
||||
},
|
||||
3.0,
|
||||
);
|
||||
|
||||
// Tagline and CTA on left phone
|
||||
tl.to(
|
||||
S + "#s3Tagline",
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 0.6,
|
||||
ease: "power2.out",
|
||||
},
|
||||
2.2,
|
||||
);
|
||||
tl.to(
|
||||
S + ".left-cta",
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 0.5,
|
||||
ease: "power2.out",
|
||||
},
|
||||
2.5,
|
||||
);
|
||||
|
||||
// Ambient slow scale push
|
||||
tl.to(
|
||||
S + "#s3Stage",
|
||||
{
|
||||
scale: 1.02,
|
||||
duration: 3.5,
|
||||
ease: "sine.inOut",
|
||||
},
|
||||
2.0,
|
||||
);
|
||||
|
||||
// ════════════════════════════════════
|
||||
// RESOLVE PHASE (4–5.5s)
|
||||
// ════════════════════════════════════
|
||||
|
||||
// Subtle glow pulse on lime accent elements
|
||||
tl.to(S + ".ring-fill", { opacity: 0.65, duration: 0.6, ease: "sine.inOut" }, 4.0);
|
||||
tl.to(S + ".ring-fill", { opacity: 1, duration: 0.6, ease: "sine.inOut" }, 4.6);
|
||||
|
||||
tl.to(S + ".bar-value", { opacity: 0.7, duration: 0.6, ease: "sine.inOut" }, 4.1);
|
||||
tl.to(S + ".bar-value", { opacity: 1, duration: 0.6, ease: "sine.inOut" }, 4.7);
|
||||
|
||||
tl.to(graphLine, { opacity: 0.65, duration: 0.6, ease: "sine.inOut" }, 4.05);
|
||||
tl.to(graphLine, { opacity: 1, duration: 0.6, ease: "sine.inOut" }, 4.65);
|
||||
|
||||
window.__timelines["app-showcase"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="app-showcase" data-composition-src="compositions/app-showcase.html" data-start="0" data-duration="5.5" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,352 @@ title: "Apple Money Count"
|
||||
description: "Apple-style finance counter that counts from $0 to $10,000, flashes green, and bursts money icons with sound."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/apple-money-count.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/apple-money-count.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/apple-money-count.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Apple Money Count preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add apple-money-count
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add apple-money-count" />
|
||||
|
||||
That writes `compositions/apple-money-count.html`, plus 1 supporting file under `assets/`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 5 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`apple-money-count.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<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: var(--bg, #fdfefe);
|
||||
}
|
||||
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--bg, #fdfefe);
|
||||
color: var(--fg, #111315);
|
||||
font-family: var(
|
||||
--font-display,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Helvetica Neue",
|
||||
Arial,
|
||||
sans-serif
|
||||
);
|
||||
}
|
||||
|
||||
.green-flash {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
background: var(--accent, #30d158);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.stage-content {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-3, 120px) var(--space-3, 160px);
|
||||
}
|
||||
|
||||
.amount-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1080px;
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
.amount {
|
||||
position: relative;
|
||||
color: var(--fg, #111315);
|
||||
font-size: 190px;
|
||||
font-weight: 900;
|
||||
line-height: 0.9;
|
||||
letter-spacing: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
text-shadow:
|
||||
0 3px 0 var(--bg, rgba(255, 255, 255, 0.58)),
|
||||
0 18px 36px var(--fg, rgba(17, 19, 21, 0.14)),
|
||||
0 42px 92px var(--fg, rgba(17, 19, 21, 0.1));
|
||||
will-change: transform, color, opacity, text-shadow;
|
||||
}
|
||||
|
||||
.money-burst {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 8;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.money-icon {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
opacity: 0;
|
||||
filter: drop-shadow(0 20px 24px var(--brand, rgba(7, 84, 31, 0.2)));
|
||||
will-change: transform, opacity, filter;
|
||||
}
|
||||
|
||||
.money-icon.bill {
|
||||
width: 96px;
|
||||
height: 52px;
|
||||
border-radius: var(--radius, 10px);
|
||||
background: var(--accent, #30d158);
|
||||
box-shadow:
|
||||
inset 0 0 0 3px var(--brand, rgba(7, 84, 31, 0.2)),
|
||||
inset 0 -10px 18px var(--brand, rgba(7, 84, 31, 0.1)),
|
||||
0 16px 26px var(--brand, rgba(7, 84, 31, 0.2)),
|
||||
0 34px 64px var(--brand, rgba(7, 84, 31, 0.16));
|
||||
}
|
||||
|
||||
.money-icon.bill::before {
|
||||
position: absolute;
|
||||
inset: var(--space-1, 9px) var(--space-2, 16px);
|
||||
border: 2px solid var(--border, rgba(7, 84, 31, 0.28));
|
||||
border-radius: 999px;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.money-icon.coin {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(
|
||||
circle at 34% 30%,
|
||||
var(--accent-2, #fff7a6) 0%,
|
||||
var(--accent, #ffd54f) 40%,
|
||||
var(--brand, #d9a514) 100%
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 0 0 4px var(--brand, rgba(111, 76, 0, 0.12)),
|
||||
inset 0 -8px 14px var(--brand, rgba(111, 76, 0, 0.1)),
|
||||
0 16px 30px var(--brand, rgba(111, 76, 0, 0.18)),
|
||||
0 34px 58px var(--brand, rgba(111, 76, 0, 0.12));
|
||||
}
|
||||
|
||||
.money-icon .mark {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--fg, #07541f);
|
||||
font-size: 30px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.money-icon.coin .mark {
|
||||
color: var(--fg, #6f4c00);
|
||||
font-size: 28px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="apple-money-count"
|
||||
data-composition-src="compositions/apple-money-count.html"
|
||||
data-root="true"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<div id="greenFlash" class="green-flash"></div>
|
||||
<div class="stage-content">
|
||||
<div class="amount-wrap">
|
||||
<div id="amount" class="amount">$0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="moneyBurst" class="money-burst" data-layout-allow-overflow></div>
|
||||
<audio
|
||||
id="sfxTrack"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-track-index="9"
|
||||
src="assets/sfx-production.wav"
|
||||
data-volume="0.92"
|
||||
></audio>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const amount = document.querySelector("#amount");
|
||||
const moneyBurst = document.querySelector("#moneyBurst");
|
||||
const countState = { value: 0 };
|
||||
|
||||
function formatMoney(value) {
|
||||
const bounded = Math.max(0, Math.min(10000, value));
|
||||
const rounded = Math.round(bounded);
|
||||
return "$" + rounded.toLocaleString("en-US");
|
||||
}
|
||||
|
||||
function renderAmount(value) {
|
||||
amount.textContent = formatMoney(value);
|
||||
}
|
||||
|
||||
const moneyIcons = [];
|
||||
const moneySpecs = [];
|
||||
|
||||
for (let i = 0; i < 62; i += 1) {
|
||||
const icon = document.createElement("div");
|
||||
const isCoin = i % 3 === 0;
|
||||
icon.className = "money-icon " + (isCoin ? "coin" : "bill");
|
||||
icon.innerHTML = '<span class="mark">$</span>';
|
||||
moneyBurst.appendChild(icon);
|
||||
moneyIcons.push(icon);
|
||||
|
||||
const angle = i * 2.399963229728653;
|
||||
const ring = i % 5;
|
||||
const radiusX = 260 + ring * 145 + (i % 7) * 12;
|
||||
const radiusY = 160 + ring * 78 + (i % 6) * 12;
|
||||
const offsetX = (i % 4) * 24 - 36;
|
||||
const offsetY = (i % 5) * 18 - 36;
|
||||
const x = Math.max(-865, Math.min(865, Math.cos(angle) * radiusX + offsetX));
|
||||
const y = Math.max(-465, Math.min(465, Math.sin(angle * 1.13) * radiusY + offsetY));
|
||||
|
||||
moneySpecs.push({
|
||||
x,
|
||||
y,
|
||||
delay: (i % 8) * 0.025,
|
||||
duration: 0.74 + (i % 5) * 0.045,
|
||||
fadeDelay: (i % 5) * 0.05,
|
||||
rotation: ((i * 43) % 160) - 80,
|
||||
scale: isCoin ? 0.72 + (i % 4) * 0.08 : 0.68 + (i % 5) * 0.07,
|
||||
});
|
||||
}
|
||||
|
||||
renderAmount(0);
|
||||
|
||||
const tl = gsap.timeline({
|
||||
paused: true,
|
||||
defaults: { ease: "power3.out" },
|
||||
});
|
||||
|
||||
gsap.set(moneyIcons, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
xPercent: -50,
|
||||
yPercent: -50,
|
||||
opacity: 0,
|
||||
scale: 0.18,
|
||||
transformOrigin: "50% 50%",
|
||||
});
|
||||
|
||||
tl.from(
|
||||
"#amount",
|
||||
{
|
||||
y: 26,
|
||||
opacity: 0,
|
||||
scale: 0.985,
|
||||
duration: 0.45,
|
||||
ease: "power2.out",
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
countState,
|
||||
{
|
||||
value: 10000,
|
||||
duration: 3.16,
|
||||
ease: "none",
|
||||
onUpdate: () => renderAmount(countState.value),
|
||||
onComplete: () => renderAmount(10000),
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
"#amount",
|
||||
{
|
||||
color: "var(--accent, #30d158)",
|
||||
textShadow:
|
||||
"0 3px 0 var(--bg, rgba(255,255,255,0.52)), 0 18px 40px var(--accent, rgba(48,209,88,0.3)), 0 46px 96px var(--brand, rgba(7,84,31,0.2))",
|
||||
duration: 0.18,
|
||||
},
|
||||
3.16,
|
||||
);
|
||||
tl.to("#amount", { scale: 1.06, duration: 0.16, ease: "back.out(2.2)" }, 3.16);
|
||||
tl.to("#amount", { scale: 1, duration: 0.23, ease: "power2.out" }, 3.33);
|
||||
|
||||
tl.to("#greenFlash", { opacity: 0.34, duration: 0.08, ease: "none" }, 3.16);
|
||||
tl.to("#greenFlash", { opacity: 0.22, duration: 0.76, ease: "none" }, 3.24);
|
||||
tl.to("#greenFlash", { opacity: 0, duration: 0.16, ease: "power2.out" }, 4);
|
||||
|
||||
moneyIcons.forEach((icon, i) => {
|
||||
const spec = moneySpecs[i];
|
||||
tl.to(
|
||||
icon,
|
||||
{
|
||||
x: spec.x,
|
||||
y: spec.y,
|
||||
rotation: spec.rotation,
|
||||
scale: spec.scale,
|
||||
opacity: 1,
|
||||
duration: spec.duration,
|
||||
ease: "power4.out",
|
||||
},
|
||||
3.28 + spec.delay,
|
||||
);
|
||||
tl.to(
|
||||
icon,
|
||||
{
|
||||
y: spec.y + (spec.y < 0 ? -58 : 58),
|
||||
scale: spec.scale * 0.54,
|
||||
opacity: 0,
|
||||
duration: 0.38,
|
||||
ease: "power2.in",
|
||||
},
|
||||
4.18 + spec.fadeDelay,
|
||||
);
|
||||
});
|
||||
|
||||
tl.to("#amount", { opacity: 0, scale: 0.985, duration: 0.28, ease: "power2.in" }, 4.36);
|
||||
|
||||
window.__timelines["apple-money-count"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="apple-money-count" data-composition-src="compositions/apple-money-count.html" data-start="0" data-duration="5" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -0,0 +1,660 @@
|
||||
---
|
||||
title: "Bar Chart Race"
|
||||
description: "Ranked bars grow and overtake each other as a time series advances, with the axis rescaling as values climb and the accent handing over to whoever leads"
|
||||
---
|
||||
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
|
||||
|
||||
<VariablesExplorer
|
||||
previewSrc="/public/catalog/preview/blocks/bar-chart-race.html"
|
||||
compositionId="bar-chart-race"
|
||||
compositionSrc="compositions/bar-chart-race.html"
|
||||
variables={[{"id":"title","type":"string","label":"Title","default":"Streaming Subscribers by Service"},{"id":"subtitle","type":"string","label":"Subtitle","default":"Ranked by reported subscribers"},{"id":"periods","type":"string","label":"Period labels (comma separated)","default":"2019, 2020, 2021, 2022, 2023, 2024"},{"id":"series","type":"string","label":"Series (one per line: Name: v1, v2, ...)","default":"Northwind: 42, 58, 71, 96, 118, 131\nCobalt: 30, 46, 68, 92, 126, 168\nFerry: 55, 62, 66, 70, 74, 79\nMarlow: 18, 33, 52, 61, 88, 104\nAster: 25, 28, 44, 58, 63, 72\nPell: 12, 20, 39, 47, 55, 90\nQuill: 8, 11, 15, 24, 40, 66\nDunmore: 35, 37, 38, 40, 42, 44"},{"id":"barCount","type":"number","label":"Bars shown","default":6,"min":3,"max":12,"step":1},{"id":"periodDuration","type":"number","label":"Seconds per period","default":2,"min":0.4,"max":6,"step":0.1},{"id":"valuePrefix","type":"string","label":"Value prefix","default":"$"},{"id":"valueSuffix","type":"string","label":"Value suffix","default":"M"},{"id":"valueDecimals","type":"number","label":"Value decimals","default":0,"min":0,"max":3,"step":1},{"id":"accent","type":"color","label":"Leader accent","default":"#c8452d"}]}
|
||||
>
|
||||
|
||||
```html bar-chart-race.html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Bar Chart Race</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background-color: #f5f3ef;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="bcr-root"
|
||||
data-composition-id="bar-chart-race"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-composition-variables='[
|
||||
{"id":"title","type":"string","label":"Title","default":"Streaming Subscribers by Service"},
|
||||
{"id":"subtitle","type":"string","label":"Subtitle","default":"Ranked by reported subscribers"},
|
||||
{"id":"periods","type":"string","label":"Period labels (comma separated)","default":"2019, 2020, 2021, 2022, 2023, 2024"},
|
||||
{"id":"series","type":"string","label":"Series (one per line: Name: v1, v2, ...)","default":"Northwind: 42, 58, 71, 96, 118, 131\nCobalt: 30, 46, 68, 92, 126, 168\nFerry: 55, 62, 66, 70, 74, 79\nMarlow: 18, 33, 52, 61, 88, 104\nAster: 25, 28, 44, 58, 63, 72\nPell: 12, 20, 39, 47, 55, 90\nQuill: 8, 11, 15, 24, 40, 66\nDunmore: 35, 37, 38, 40, 42, 44"},
|
||||
{"id":"barCount","type":"number","label":"Bars shown","default":6,"min":3,"max":12,"step":1},
|
||||
{"id":"periodDuration","type":"number","label":"Seconds per period","default":2,"min":0.4,"max":6,"step":0.1},
|
||||
{"id":"valuePrefix","type":"string","label":"Value prefix","default":"$"},
|
||||
{"id":"valueSuffix","type":"string","label":"Value suffix","default":"M"},
|
||||
{"id":"valueDecimals","type":"number","label":"Value decimals","default":0,"min":0,"max":3,"step":1},
|
||||
{"id":"accent","type":"color","label":"Leader accent","default":"#c8452d"}
|
||||
]'
|
||||
>
|
||||
<div id="bcr-stage" class="clip" data-start="0" data-duration="12" data-track-index="0">
|
||||
<div id="bcr-bg"></div>
|
||||
|
||||
<div id="bcr-head">
|
||||
<div id="bcr-head-left">
|
||||
<h1 id="bcr-title">Streaming Subscribers by Service</h1>
|
||||
<p id="bcr-subtitle">Ranked by reported subscribers</p>
|
||||
</div>
|
||||
<div id="bcr-head-right">
|
||||
<span id="bcr-period-caption">Period</span>
|
||||
<span id="bcr-period">2019</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rows live here, one per series, positioned by rank. -->
|
||||
<div id="bcr-plot"></div>
|
||||
|
||||
<!-- Axis is painted above the rows: the tick labels sliding left is the
|
||||
visible evidence that the scale is rescaling as values climb. -->
|
||||
<div id="bcr-axis"></div>
|
||||
|
||||
<p id="bcr-source">Placeholder data</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#bcr-root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
font-family: Inter, sans-serif;
|
||||
color: #1f1d1b;
|
||||
}
|
||||
|
||||
#bcr-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
/* Scene fill goes on a full-bleed child, never the composition root. */
|
||||
#bcr-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: #f5f3ef;
|
||||
}
|
||||
|
||||
#bcr-head {
|
||||
position: absolute;
|
||||
top: 58px;
|
||||
left: 96px;
|
||||
width: 1728px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#bcr-title {
|
||||
font-size: 46px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.015em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
#bcr-subtitle {
|
||||
margin-top: 10px;
|
||||
font-size: 19px;
|
||||
font-weight: 400;
|
||||
color: #6b6560;
|
||||
}
|
||||
|
||||
#bcr-head-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
#bcr-period-caption {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: #6b6560;
|
||||
}
|
||||
|
||||
#bcr-period {
|
||||
margin-top: 4px;
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
#bcr-plot {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 236px;
|
||||
width: 1920px;
|
||||
height: 760px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bcr-row {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1920px;
|
||||
}
|
||||
|
||||
/* Rows are transparent so both bars of an overtake stay visible sliding
|
||||
past each other; only the two TEXT blocks carry an opaque backing, so
|
||||
the front row's label covers the one it crosses instead of tangling
|
||||
with it. */
|
||||
.bcr-name {
|
||||
position: absolute;
|
||||
left: 96px;
|
||||
width: 272px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
text-align: right;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
line-height: 1.7;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background-color: #f5f3ef;
|
||||
}
|
||||
|
||||
.bcr-bar {
|
||||
position: absolute;
|
||||
left: 400px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 3px;
|
||||
background-color: #1f1d1b;
|
||||
}
|
||||
|
||||
.bcr-value {
|
||||
position: absolute;
|
||||
left: 404px;
|
||||
top: 50%;
|
||||
padding: 0 12px;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
line-height: 1.7;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
background-color: #f5f3ef;
|
||||
}
|
||||
|
||||
#bcr-axis {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 5000;
|
||||
}
|
||||
|
||||
.bcr-tick-line {
|
||||
position: absolute;
|
||||
top: 232px;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 768px;
|
||||
background-color: rgba(31, 29, 27, 0.11);
|
||||
}
|
||||
|
||||
.bcr-tick-line.bcr-tick-zero {
|
||||
background-color: rgba(31, 29, 27, 0.5);
|
||||
}
|
||||
|
||||
.bcr-tick-label {
|
||||
position: absolute;
|
||||
top: 198px;
|
||||
left: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #6b6560;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#bcr-source {
|
||||
position: absolute;
|
||||
left: 96px;
|
||||
top: 1014px;
|
||||
font-size: 16px;
|
||||
color: #6b6560;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// ---- variables -------------------------------------------------
|
||||
// The declaration attribute is the single source of truth: when the
|
||||
// runtime is absent (raw file open) we parse the same attribute.
|
||||
function readVars() {
|
||||
// Read the declaration off THIS block's own root, never
|
||||
// document.documentElement: mounted as a sub-composition the block's
|
||||
// <html> is discarded and documentElement is the HOST's, so a
|
||||
// documentElement read returns the wrong element (or null) and the
|
||||
// block renders with no series at all. Runtime / host / --variables
|
||||
// values are layered on top as overrides, so the block does not
|
||||
// depend on the runtime being injected before this script runs.
|
||||
var out = {};
|
||||
var root = document.getElementById("bcr-root");
|
||||
try {
|
||||
JSON.parse(root.getAttribute("data-composition-variables")).forEach(function (d) {
|
||||
out[d.id] = d.default;
|
||||
});
|
||||
} catch (err) {
|
||||
// Declaration unreadable; the per-variable literal fallbacks below
|
||||
// still produce a renderable composition.
|
||||
}
|
||||
var hf = window.__hyperframes;
|
||||
var overrides =
|
||||
hf && typeof hf.getVariables === "function"
|
||||
? hf.getVariables()
|
||||
: window.__hfVariables;
|
||||
if (overrides && typeof overrides === "object") {
|
||||
for (var key in overrides) {
|
||||
if (overrides[key] !== undefined) out[key] = overrides[key];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function num(value, fallback, lo, hi) {
|
||||
var n = typeof value === "number" ? value : Number.parseFloat(value);
|
||||
if (!Number.isFinite(n)) n = fallback;
|
||||
return Math.min(hi, Math.max(lo, n));
|
||||
}
|
||||
|
||||
function str(value, fallback) {
|
||||
return typeof value === "string" && value.length > 0 ? value : fallback;
|
||||
}
|
||||
|
||||
var V = readVars();
|
||||
var ACCENT = str(V.accent, "#c8452d");
|
||||
var BAR_COLOR = "#1f1d1b";
|
||||
var BAR_COUNT = Math.round(num(V.barCount, 6, 1, 24));
|
||||
var PERIOD_DURATION = num(V.periodDuration, 2, 0.1, 30);
|
||||
var DECIMALS = Math.round(num(V.valueDecimals, 0, 0, 6));
|
||||
var PREFIX = typeof V.valuePrefix === "string" ? V.valuePrefix : "";
|
||||
var SUFFIX = typeof V.valueSuffix === "string" ? V.valueSuffix : "";
|
||||
|
||||
// ---- data format ------------------------------------------------
|
||||
// Wide table, one series per line: "Name: v1, v2, v3".
|
||||
// `;` is accepted as a line separator so the whole table survives a
|
||||
// single-line text input. Short rows hold their last value.
|
||||
function parsePeriods(text) {
|
||||
return String(text)
|
||||
.split(",")
|
||||
.map(function (s) {
|
||||
return s.trim();
|
||||
})
|
||||
.filter(function (s) {
|
||||
return s.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function parseSeries(text, periodCount) {
|
||||
var out = [];
|
||||
var lines = String(text).split(/[\n;]+/);
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
var split = line.indexOf(":");
|
||||
if (split < 0) continue;
|
||||
var label = line.slice(0, split).trim();
|
||||
if (!label) continue;
|
||||
var values = line
|
||||
.slice(split + 1)
|
||||
.split(",")
|
||||
.map(function (s) {
|
||||
return Number.parseFloat(s.trim());
|
||||
})
|
||||
.filter(function (n) {
|
||||
return Number.isFinite(n);
|
||||
});
|
||||
if (values.length === 0) continue;
|
||||
while (values.length < periodCount) values.push(values[values.length - 1]);
|
||||
out.push({ label: label, values: values.slice(0, periodCount) });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
var PERIODS = parsePeriods(str(V.periods, "2019, 2020, 2021, 2022, 2023, 2024"));
|
||||
if (PERIODS.length === 0) PERIODS = ["1"];
|
||||
var SERIES = parseSeries(str(V.series, ""), PERIODS.length);
|
||||
var T = PERIODS.length;
|
||||
var N = SERIES.length;
|
||||
|
||||
// ---- closed-form state ------------------------------------------
|
||||
// k = 10 interpolated keyframes per period (Bostock's number). Rank is
|
||||
// computed at each keyframe from the interpolated value and baked once;
|
||||
// a bar's on-screen row is then solved FROM rank, so two bars can never
|
||||
// cross without swapping. Everything below is a pure function of t.
|
||||
var K = 10;
|
||||
var KF_DURATION = PERIOD_DURATION / K;
|
||||
var KF_COUNT = T > 1 ? (T - 1) * K + 1 : 1;
|
||||
var RACE_SECONDS = T > 1 ? (T - 1) * PERIOD_DURATION : 0;
|
||||
|
||||
function clamp(v, lo, hi) {
|
||||
return v < lo ? lo : v > hi ? hi : v;
|
||||
}
|
||||
|
||||
function valueAt(t, series) {
|
||||
if (T < 2) return series.values[0];
|
||||
var u = t / PERIOD_DURATION;
|
||||
var i = clamp(Math.floor(u), 0, T - 2);
|
||||
var f = clamp(u - i, 0, 1);
|
||||
return series.values[i] + (series.values[i + 1] - series.values[i]) * f;
|
||||
}
|
||||
|
||||
// ranks[m][j] = integer rank of series j at keyframe m (0 = leader).
|
||||
var RANKS = [];
|
||||
for (var m = 0; m < KF_COUNT; m++) {
|
||||
var tm = m * KF_DURATION;
|
||||
var order = SERIES.map(function (s, j) {
|
||||
return { j: j, v: valueAt(tm, s) };
|
||||
});
|
||||
// Descending by value; index breaks ties so the sort is total and
|
||||
// therefore identical on every evaluation.
|
||||
order.sort(function (a, b) {
|
||||
return b.v - a.v || a.j - b.j;
|
||||
});
|
||||
var row = new Array(N);
|
||||
for (var r = 0; r < order.length; r++) row[order[r].j] = r;
|
||||
RANKS.push(row);
|
||||
}
|
||||
|
||||
function smoothstep(x) {
|
||||
return x * x * (3 - 2 * x);
|
||||
}
|
||||
|
||||
// Continuous row position solved from the baked ranks. A rank change
|
||||
// between two adjacent keyframes IS the swap, and it plays out over one
|
||||
// keyframe interval (PERIOD_DURATION / 10) instead of snapping.
|
||||
function rankPosAt(t, j) {
|
||||
if (KF_COUNT < 2) return RANKS[0][j];
|
||||
var m = t / KF_DURATION;
|
||||
var m0 = clamp(Math.floor(m), 0, KF_COUNT - 2);
|
||||
var e = smoothstep(clamp(m - m0, 0, 1));
|
||||
var a = RANKS[m0][j];
|
||||
var b = RANKS[m0 + 1][j];
|
||||
return a + (b - a) * e;
|
||||
}
|
||||
|
||||
// ---- formatting --------------------------------------------------
|
||||
function formatValue(v) {
|
||||
return (
|
||||
PREFIX +
|
||||
v.toLocaleString("en-US", {
|
||||
minimumFractionDigits: DECIMALS,
|
||||
maximumFractionDigits: DECIMALS,
|
||||
}) +
|
||||
SUFFIX
|
||||
);
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
var h = String(hex).trim().replace("#", "");
|
||||
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
||||
var n = Number.parseInt(h, 16);
|
||||
if (!Number.isFinite(n)) return [31, 29, 27];
|
||||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
||||
}
|
||||
|
||||
// Accent is the only colour in the piece and it carries exactly one
|
||||
// meaning: this bar currently leads. It is binary, not blended — a
|
||||
// half-accent bar would assert "half leading", which is not a fact.
|
||||
// It hands over at the instant the two values cross, which is also the
|
||||
// instant the two bars are the same length, so the handover lands on
|
||||
// the overtake instead of drifting near it. Flat fill, never a
|
||||
// gradient: a gradient inside a bar makes the longest bar the palest
|
||||
// exactly where the eye is comparing lengths.
|
||||
function hexToCss(hex) {
|
||||
var rgb = hexToRgb(hex);
|
||||
return "rgb(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ")";
|
||||
}
|
||||
|
||||
var BAR_FILL = hexToCss(BAR_COLOR);
|
||||
var ACCENT_FILL = hexToCss(ACCENT);
|
||||
|
||||
function niceStep(x) {
|
||||
var e = Math.pow(10, Math.floor(Math.log10(x)));
|
||||
var f = x / e;
|
||||
return e * (f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10);
|
||||
}
|
||||
|
||||
// ---- geometry ----------------------------------------------------
|
||||
var TRACK_X = 400;
|
||||
var TRACK_W = 1180;
|
||||
var PLOT_H = 760;
|
||||
var PITCH = PLOT_H / BAR_COUNT;
|
||||
var BAR_H = Math.max(12, PITCH * 0.62);
|
||||
var TICK_POOL = 12;
|
||||
|
||||
// ---- DOM ---------------------------------------------------------
|
||||
var plot = document.getElementById("bcr-plot");
|
||||
var axis = document.getElementById("bcr-axis");
|
||||
var periodEl = document.getElementById("bcr-period");
|
||||
document.getElementById("bcr-title").textContent = str(
|
||||
V.title,
|
||||
"Streaming Subscribers by Service",
|
||||
);
|
||||
document.getElementById("bcr-subtitle").textContent = str(
|
||||
V.subtitle,
|
||||
"Ranked by reported subscribers",
|
||||
);
|
||||
|
||||
var rows = SERIES.map(function (s) {
|
||||
var row = document.createElement("div");
|
||||
row.className = "bcr-row";
|
||||
row.style.height = PITCH + "px";
|
||||
|
||||
// Declared, not silenced: two rows mid-overtake genuinely occupy the
|
||||
// same band for ~200ms. Readability is handled by the opaque chip on
|
||||
// each label, so the front one is always legible.
|
||||
var name = document.createElement("div");
|
||||
name.className = "bcr-name";
|
||||
name.setAttribute("data-layout-allow-overlap", "");
|
||||
name.textContent = s.label;
|
||||
|
||||
var bar = document.createElement("div");
|
||||
bar.className = "bcr-bar";
|
||||
bar.style.height = BAR_H + "px";
|
||||
|
||||
var value = document.createElement("div");
|
||||
value.className = "bcr-value";
|
||||
value.setAttribute("data-layout-allow-overlap", "");
|
||||
|
||||
row.appendChild(name);
|
||||
row.appendChild(bar);
|
||||
row.appendChild(value);
|
||||
plot.appendChild(row);
|
||||
return { row: row, bar: bar, value: value };
|
||||
});
|
||||
|
||||
var ticks = [];
|
||||
for (var k = 0; k < TICK_POOL; k++) {
|
||||
var line = document.createElement("div");
|
||||
line.className = "bcr-tick-line" + (k === 0 ? " bcr-tick-zero" : "");
|
||||
var label = document.createElement("div");
|
||||
label.className = "bcr-tick-label";
|
||||
axis.appendChild(line);
|
||||
axis.appendChild(label);
|
||||
ticks.push({ line: line, label: label });
|
||||
}
|
||||
|
||||
// ---- render ------------------------------------------------------
|
||||
function render(t) {
|
||||
if (N === 0) return;
|
||||
|
||||
var values = SERIES.map(function (s) {
|
||||
return valueAt(t, s);
|
||||
});
|
||||
// Paint order follows the CURRENT value, not the row position: the
|
||||
// bar that has just edged ahead comes forward, so at the instant two
|
||||
// bars are superimposed the frame shows the one that is overtaking
|
||||
// (its bar and its label together) rather than the one it passed.
|
||||
var byValue = values
|
||||
.map(function (v, i) {
|
||||
return i;
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return values[b] - values[a] || a - b;
|
||||
});
|
||||
var zRank = new Array(values.length);
|
||||
for (var q = 0; q < byValue.length; q++) zRank[byValue[q]] = q;
|
||||
var leader = byValue[0];
|
||||
var maxValue = values[leader];
|
||||
// Headroom keeps the leader off the frame edge; the domain is a
|
||||
// continuous function of t, so the axis glides instead of stepping.
|
||||
var scaleMax = Math.max(maxValue * 1.06, 1e-6);
|
||||
var step = niceStep(scaleMax / 4);
|
||||
|
||||
for (var i = 0; i < TICK_POOL; i++) {
|
||||
var tv = i * step;
|
||||
var visible = tv <= scaleMax;
|
||||
var tx = TRACK_X + (tv / scaleMax) * TRACK_W;
|
||||
ticks[i].line.style.transform = "translateX(" + tx.toFixed(2) + "px)";
|
||||
ticks[i].line.style.opacity = visible ? "1" : "0";
|
||||
ticks[i].label.style.transform =
|
||||
"translateX(" + tx.toFixed(2) + "px) translateX(-50%)";
|
||||
ticks[i].label.style.opacity = visible ? "1" : "0";
|
||||
ticks[i].label.textContent = visible ? formatValue(tv) : "";
|
||||
}
|
||||
|
||||
for (var j = 0; j < rows.length; j++) {
|
||||
var rankPos = rankPosAt(t, j);
|
||||
var barW = (values[j] / scaleMax) * TRACK_W;
|
||||
var el = rows[j];
|
||||
|
||||
el.row.style.transform = "translateY(" + (rankPos * PITCH).toFixed(2) + "px)";
|
||||
// Fades out only as it slides past the last visible slot.
|
||||
el.row.style.opacity = clamp(BAR_COUNT - rankPos, 0, 1).toFixed(3);
|
||||
el.row.style.zIndex = String(1000 - zRank[j]);
|
||||
|
||||
el.bar.style.width = barW.toFixed(2) + "px";
|
||||
el.bar.style.backgroundColor = j === leader ? ACCENT_FILL : BAR_FILL;
|
||||
|
||||
el.value.style.transform = "translate(" + barW.toFixed(2) + "px, -50%)";
|
||||
el.value.textContent = formatValue(values[j]);
|
||||
}
|
||||
|
||||
var pIndex = clamp(Math.floor(t / PERIOD_DURATION), 0, T - 1);
|
||||
periodEl.textContent = PERIODS[pIndex];
|
||||
}
|
||||
|
||||
// ---- timeline ----------------------------------------------------
|
||||
// A property SETTER drives the per-frame work: tl.eventCallback("onUpdate")
|
||||
// does not fire on tl.seek(), a tweened accessor does.
|
||||
var driver = { _t: 0 };
|
||||
Object.defineProperty(driver, "t", {
|
||||
get: function () {
|
||||
return this._t;
|
||||
},
|
||||
set: function (value) {
|
||||
this._t = value;
|
||||
render(value);
|
||||
},
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
if (RACE_SECONDS > 0) {
|
||||
tl.to(driver, { t: RACE_SECONDS, duration: RACE_SECONDS, ease: "none" }, 0);
|
||||
}
|
||||
render(0);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["bar-chart-race"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</VariablesExplorer>
|
||||
|
||||
## Install
|
||||
|
||||
<InstallCommand command="npx hyperframes add bar-chart-race" />
|
||||
|
||||
That writes one file: `compositions/bar-chart-race.html`.
|
||||
|
||||
## Add it to your video
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="bar-chart-race"
|
||||
data-composition-src="compositions/bar-chart-race.html"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Change how it looks
|
||||
|
||||
Set these CSS variables on the block:
|
||||
|
||||
- `title` — Title. Defaults to `Streaming Subscribers by Service`.
|
||||
- `subtitle` — Subtitle. Defaults to `Ranked by reported subscribers`.
|
||||
- `periods` — Period labels (comma separated). Defaults to `2019, 2020, 2021, 2022, 2023, 2024`.
|
||||
- `series` — Series (one per line: Name: v1, v2, ...). Defaults to `Northwind: 42, 58, 71, 96, 118, 131
|
||||
Cobalt: 30, 46, 68, 92, 126, 168
|
||||
Ferry: 55, 62, 66, 70, 74, 79
|
||||
Marlow: 18, 33, 52, 61, 88, 104
|
||||
Aster: 25, 28, 44, 58, 63, 72
|
||||
Pell: 12, 20, 39, 47, 55, 90
|
||||
Quill: 8, 11, 15, 24, 40, 66
|
||||
Dunmore: 35, 37, 38, 40, 42, 44`.
|
||||
- `barCount` — Bars shown. Defaults to `6`.
|
||||
- `periodDuration` — Seconds per period. Defaults to `2`.
|
||||
- `valuePrefix` — Value prefix. Defaults to `$`.
|
||||
- `valueSuffix` — Value suffix. Defaults to `M`.
|
||||
- `valueDecimals` — Value decimals. Defaults to `0`.
|
||||
- `accent` — Leader accent. Defaults to `#c8452d`.
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `data` `chart` `statistics` `ranking`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
||||
- [Build a richer composition](/go-further)
|
||||
@@ -3,13 +3,13 @@ title: "Beat Freeze Cut"
|
||||
description: "A beat-driven speed ramp, freeze-frame hit, and hard-cut sequence for music-led promos and montages."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/beat-freeze-cut.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/beat-freeze-cut.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/beat-freeze-cut.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Beat Freeze Cut preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add beat-freeze-cut
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add beat-freeze-cut" />
|
||||
|
||||
That writes one file: `compositions/beat-freeze-cut.html`.
|
||||
|
||||
@@ -39,6 +39,894 @@ Set these CSS variables on the block:
|
||||
- `--bg-color` — Background. Defaults to `#0B0D10`.
|
||||
- `--accent-color` — Accent. Defaults to `#00E5C7`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`beat-freeze-cut.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Beat Freeze Cut</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600;700;800;900&display=block"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #0b0d10;
|
||||
}
|
||||
|
||||
/*
|
||||
EDIT / STUDIO PARAMS
|
||||
────────────────────
|
||||
Studio color params (CSS custom properties on the root):
|
||||
--bg-color default #0B0D10
|
||||
--accent-color default #00E5C7
|
||||
|
||||
Numeric / string params (beat times, labels, intensity) are NOT in the
|
||||
formal registry params schema today (only color keys are used by
|
||||
existing blocks). Edit the CONFIG object in the script below instead.
|
||||
*/
|
||||
|
||||
#bfc-root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
--bg-color: #0b0d10;
|
||||
--accent-color: #00e5c7;
|
||||
--bfc-ink: #f4f6f8;
|
||||
--bfc-muted: #a0a8b0;
|
||||
--bfc-panel: #14181f;
|
||||
--bfc-panel-2: #1a2029;
|
||||
--bfc-line: rgba(255, 255, 255, 0.08);
|
||||
font-family:
|
||||
"Inter",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
color: var(--bfc-ink);
|
||||
}
|
||||
|
||||
#bfc-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 60% at 50% 40%, rgba(0, 229, 199, 0.07) 0%, transparent 55%),
|
||||
radial-gradient(
|
||||
ellipse 50% 40% at 80% 80%,
|
||||
rgba(255, 255, 255, 0.03) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
var(--bg-color);
|
||||
}
|
||||
|
||||
#bfc-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.22;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
|
||||
background-size: 80px 80px;
|
||||
mask-image: radial-gradient(ellipse 70% 60% at 50% 45%, #000 20%, transparent 75%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#bfc-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform-origin: 50% 48%;
|
||||
will-change: transform, filter;
|
||||
}
|
||||
|
||||
#bfc-shot-a,
|
||||
#bfc-shot-b {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#bfc-shot-b {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ── Shot A: single product card hero ── */
|
||||
#bfc-a-crop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
|
||||
#bfc-a-card {
|
||||
position: relative;
|
||||
width: 680px;
|
||||
height: 720px;
|
||||
border-radius: 36px;
|
||||
background:
|
||||
linear-gradient(160deg, rgba(255, 255, 255, 0.06) 0%, transparent 40%),
|
||||
linear-gradient(180deg, var(--bfc-panel-2) 0%, var(--bfc-panel) 100%);
|
||||
border: 1px solid var(--bfc-line);
|
||||
box-shadow:
|
||||
0 40px 120px rgba(0, 0, 0, 0.55),
|
||||
0 0 0 1px rgba(0, 229, 199, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
overflow: hidden;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
|
||||
#bfc-a-card-glow {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 28%;
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
margin-left: -210px;
|
||||
margin-top: -210px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(0, 229, 199, 0.28) 0%, transparent 68%);
|
||||
opacity: 0.85;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#bfc-a-wave {
|
||||
position: absolute;
|
||||
left: 64px;
|
||||
right: 64px;
|
||||
top: 120px;
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
#bfc-a-wave svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#bfc-a-wave-path {
|
||||
fill: none;
|
||||
stroke: var(--accent-color);
|
||||
stroke-width: 4;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
#bfc-a-wave-fill {
|
||||
fill: var(--accent-color);
|
||||
opacity: 0.12;
|
||||
}
|
||||
|
||||
#bfc-a-bars {
|
||||
position: absolute;
|
||||
left: 80px;
|
||||
right: 80px;
|
||||
bottom: 160px;
|
||||
height: 96px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bfc-bar {
|
||||
flex: 1;
|
||||
border-radius: 6px 6px 2px 2px;
|
||||
background: linear-gradient(180deg, var(--accent-color) 0%, rgba(0, 229, 199, 0.25) 100%);
|
||||
transform-origin: 50% 100%;
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
#bfc-a-meta {
|
||||
position: absolute;
|
||||
left: 64px;
|
||||
right: 64px;
|
||||
bottom: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#bfc-a-kicker {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--bfc-muted);
|
||||
}
|
||||
|
||||
#bfc-a-pill {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #04120f;
|
||||
background: var(--accent-color);
|
||||
padding: 8px 16px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
/* ── Shot B: split layout, higher info density ── */
|
||||
#bfc-b-layout {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1.05fr 0.95fr;
|
||||
gap: 48px;
|
||||
padding: 100px 110px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#bfc-b-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
transform-origin: 0% 50%;
|
||||
}
|
||||
|
||||
#bfc-b-eyebrow {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
#bfc-b-title {
|
||||
font-size: 128px;
|
||||
font-weight: 900;
|
||||
line-height: 0.92;
|
||||
letter-spacing: -0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--bfc-ink);
|
||||
}
|
||||
|
||||
#bfc-b-sub {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: var(--bfc-muted);
|
||||
max-width: 520px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
#bfc-b-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.bfc-b-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 28px 32px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(135deg, var(--bfc-panel-2), var(--bfc-panel));
|
||||
border: 1px solid var(--bfc-line);
|
||||
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.bfc-b-card-label {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--bfc-muted);
|
||||
}
|
||||
|
||||
.bfc-b-card-value {
|
||||
font-size: 42px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--bfc-ink);
|
||||
}
|
||||
|
||||
.bfc-b-card-value.accent {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
#bfc-b-accent-bar {
|
||||
width: 120px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent-color);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ── Intro / transient typography ── */
|
||||
#bfc-intro {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 72px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 20;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#bfc-intro-label {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.28em;
|
||||
text-transform: uppercase;
|
||||
color: var(--bfc-muted);
|
||||
padding: 10px 18px;
|
||||
border: 1px solid var(--bfc-line);
|
||||
border-radius: 999px;
|
||||
background: rgba(11, 13, 16, 0.55);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
#bfc-hit-type {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 30;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#bfc-hit-type-text {
|
||||
font-size: 160px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--bfc-ink);
|
||||
text-shadow:
|
||||
0 0 40px rgba(0, 229, 199, 0.35),
|
||||
0 8px 40px rgba(0, 0, 0, 0.55);
|
||||
-webkit-text-stroke: 2px rgba(0, 0, 0, 0.35);
|
||||
paint-order: stroke fill;
|
||||
}
|
||||
|
||||
/* ── Freeze frame treatment ── */
|
||||
#bfc-freeze {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 25;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#bfc-freeze-outline {
|
||||
position: absolute;
|
||||
inset: 48px;
|
||||
border: 3px solid var(--accent-color);
|
||||
border-radius: 8px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 229, 199, 0.25),
|
||||
inset 0 0 0 1px rgba(0, 229, 199, 0.15),
|
||||
0 0 48px rgba(0, 229, 199, 0.2);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#bfc-freeze-flash {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #ffffff;
|
||||
opacity: 0;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
#bfc-freeze-contour {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(90deg, transparent 0%, rgba(0, 229, 199, 0.08) 48%, transparent 52%),
|
||||
radial-gradient(ellipse 40% 55% at 50% 48%, transparent 40%, rgba(0, 229, 199, 0.18) 100%);
|
||||
opacity: 0;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
#bfc-freeze-badge {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
right: 96px;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: #04120f;
|
||||
background: var(--accent-color);
|
||||
padding: 10px 18px;
|
||||
border-radius: 6px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ── Hard-cut connector (directional stretch / blur) ── */
|
||||
#bfc-cut {
|
||||
position: absolute;
|
||||
inset: -8% -20%;
|
||||
pointer-events: none;
|
||||
z-index: 40;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(0, 229, 199, 0.12) 35%,
|
||||
rgba(255, 255, 255, 0.55) 50%,
|
||||
rgba(0, 229, 199, 0.12) 65%,
|
||||
transparent 100%
|
||||
);
|
||||
filter: blur(18px);
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
|
||||
#bfc-vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 15;
|
||||
background: radial-gradient(
|
||||
ellipse 75% 70% at 50% 50%,
|
||||
transparent 45%,
|
||||
rgba(0, 0, 0, 0.55) 100%
|
||||
);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
#bfc-driver {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="bfc-root"
|
||||
data-composition-id="beat-freeze-cut"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="bfc-bg" aria-hidden="true"></div>
|
||||
<div id="bfc-grid" aria-hidden="true"></div>
|
||||
|
||||
<div id="bfc-stage">
|
||||
<!-- Shot A -->
|
||||
<div id="bfc-shot-a">
|
||||
<div id="bfc-a-crop" data-layout-allow-overflow>
|
||||
<div id="bfc-a-card" data-layout-allow-overflow>
|
||||
<div id="bfc-a-card-glow" aria-hidden="true"></div>
|
||||
<div id="bfc-a-wave" aria-hidden="true">
|
||||
<svg viewBox="0 0 592 220" preserveAspectRatio="none">
|
||||
<path
|
||||
id="bfc-a-wave-fill"
|
||||
d="M0,160 C40,150 60,90 100,100 C140,110 160,40 200,55 C240,70 260,150 300,140 C340,130 360,30 400,45 C440,60 460,130 500,120 C540,110 560,70 592,80 L592,220 L0,220 Z"
|
||||
/>
|
||||
<path
|
||||
id="bfc-a-wave-path"
|
||||
d="M0,160 C40,150 60,90 100,100 C140,110 160,40 200,55 C240,70 260,150 300,140 C340,130 360,30 400,45 C440,60 460,130 500,120 C540,110 560,70 592,80"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="bfc-a-bars" aria-hidden="true">
|
||||
<div class="bfc-bar" id="bfc-bar-0" style="height: 34%"></div>
|
||||
<div class="bfc-bar" id="bfc-bar-1" style="height: 58%"></div>
|
||||
<div class="bfc-bar" id="bfc-bar-2" style="height: 42%"></div>
|
||||
<div class="bfc-bar" id="bfc-bar-3" style="height: 78%"></div>
|
||||
<div class="bfc-bar" id="bfc-bar-4" style="height: 52%"></div>
|
||||
<div class="bfc-bar" id="bfc-bar-5" style="height: 88%"></div>
|
||||
<div class="bfc-bar" id="bfc-bar-6" style="height: 46%"></div>
|
||||
<div class="bfc-bar" id="bfc-bar-7" style="height: 64%"></div>
|
||||
<div class="bfc-bar" id="bfc-bar-8" style="height: 36%"></div>
|
||||
<div class="bfc-bar" id="bfc-bar-9" style="height: 72%"></div>
|
||||
<div class="bfc-bar" id="bfc-bar-10" style="height: 48%"></div>
|
||||
<div class="bfc-bar" id="bfc-bar-11" style="height: 92%"></div>
|
||||
</div>
|
||||
<div id="bfc-a-meta">
|
||||
<span id="bfc-a-kicker">MUSIC PROMO</span>
|
||||
<span id="bfc-a-pill">LIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shot B -->
|
||||
<div id="bfc-shot-b">
|
||||
<div id="bfc-b-layout">
|
||||
<div id="bfc-b-left">
|
||||
<div id="bfc-b-eyebrow">Next shot</div>
|
||||
<div id="bfc-b-title">HARD<br />CUT</div>
|
||||
<div id="bfc-b-accent-bar" aria-hidden="true"></div>
|
||||
<div id="bfc-b-sub">
|
||||
Beat-locked freeze, then cut. Built for music-led promos and montages.
|
||||
</div>
|
||||
</div>
|
||||
<div id="bfc-b-right">
|
||||
<div class="bfc-b-card" id="bfc-b-card-0">
|
||||
<span class="bfc-b-card-label">Ramp</span>
|
||||
<span class="bfc-b-card-value accent">1.35→2.2</span>
|
||||
</div>
|
||||
<div class="bfc-b-card" id="bfc-b-card-1">
|
||||
<span class="bfc-b-card-label">Freeze</span>
|
||||
<span class="bfc-b-card-value">0.8s</span>
|
||||
</div>
|
||||
<div class="bfc-b-card" id="bfc-b-card-2">
|
||||
<span class="bfc-b-card-label">Cut</span>
|
||||
<span class="bfc-b-card-value accent">3.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="bfc-vignette" aria-hidden="true"></div>
|
||||
|
||||
<div id="bfc-intro" aria-hidden="true">
|
||||
<span id="bfc-intro-label">ON THE BEAT</span>
|
||||
</div>
|
||||
|
||||
<div id="bfc-hit-type" aria-hidden="true">
|
||||
<span id="bfc-hit-type-text">DROP</span>
|
||||
</div>
|
||||
|
||||
<div id="bfc-freeze" aria-hidden="true">
|
||||
<div id="bfc-freeze-flash"></div>
|
||||
<div id="bfc-freeze-contour"></div>
|
||||
<div id="bfc-freeze-outline"></div>
|
||||
<div id="bfc-freeze-badge">FREEZE</div>
|
||||
</div>
|
||||
|
||||
<div id="bfc-cut" aria-hidden="true"></div>
|
||||
|
||||
<div id="bfc-driver" class="clip" data-start="0" data-duration="6" data-track-index="0"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
// ── EDIT ME (not Studio params — see HTML comment) ──────────────
|
||||
var CONFIG = {
|
||||
primaryLabel: "DROP",
|
||||
secondaryLabel: "ON THE BEAT",
|
||||
beat1: 0.7,
|
||||
beat2: 2.2,
|
||||
freezeTime: 2.2,
|
||||
intensity: 1.0,
|
||||
};
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
var DURATION = 6;
|
||||
var intensity = Math.max(0.6, Math.min(1.4, Number(CONFIG.intensity) || 1));
|
||||
var beat1 = Number(CONFIG.beat1) || 0.7;
|
||||
var beat2 = Number(CONFIG.beat2) || 2.2;
|
||||
var freezeAt = Number(CONFIG.freezeTime) || beat2;
|
||||
var cutAt = 3.0;
|
||||
var beat3 = 4.7;
|
||||
var holdAt = 5.2;
|
||||
|
||||
var root = document.getElementById("bfc-root");
|
||||
var stage = document.getElementById("bfc-stage");
|
||||
var shotA = document.getElementById("bfc-shot-a");
|
||||
var shotB = document.getElementById("bfc-shot-b");
|
||||
var aCrop = document.getElementById("bfc-a-crop");
|
||||
var aCard = document.getElementById("bfc-a-card");
|
||||
var intro = document.getElementById("bfc-intro");
|
||||
var introLabel = document.getElementById("bfc-intro-label");
|
||||
var hitType = document.getElementById("bfc-hit-type");
|
||||
var hitText = document.getElementById("bfc-hit-type-text");
|
||||
var freeze = document.getElementById("bfc-freeze");
|
||||
var freezeOutline = document.getElementById("bfc-freeze-outline");
|
||||
var freezeFlash = document.getElementById("bfc-freeze-flash");
|
||||
var freezeContour = document.getElementById("bfc-freeze-contour");
|
||||
var freezeBadge = document.getElementById("bfc-freeze-badge");
|
||||
var cutFx = document.getElementById("bfc-cut");
|
||||
var bLeft = document.getElementById("bfc-b-left");
|
||||
var bCards = [
|
||||
document.getElementById("bfc-b-card-0"),
|
||||
document.getElementById("bfc-b-card-1"),
|
||||
document.getElementById("bfc-b-card-2"),
|
||||
];
|
||||
var bars = [];
|
||||
for (var bi = 0; bi < 12; bi++) {
|
||||
bars.push(document.getElementById("bfc-bar-" + bi));
|
||||
}
|
||||
|
||||
// Apply editable labels
|
||||
if (hitText) hitText.textContent = CONFIG.primaryLabel || "DROP";
|
||||
if (introLabel) introLabel.textContent = CONFIG.secondaryLabel || "ON THE BEAT";
|
||||
|
||||
// Accent color from CSS var → freeze/glow that use hardcoded rgba fallbacks
|
||||
// stay coherent because stroke/fill already use var(--accent-color).
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Initial states (no CSS transform conflicts on animated props)
|
||||
gsap.set(stage, { scale: 1, x: 0, y: 0, filter: "blur(0px) brightness(1)" });
|
||||
gsap.set(aCrop, { scale: 1, x: 0, y: 0 });
|
||||
// Card visible at t=0 so catalog snapshots are never a black frame
|
||||
gsap.set(aCard, { scale: 0.9, y: 20, opacity: 1 });
|
||||
gsap.set(shotA, { opacity: 1, visibility: "visible" });
|
||||
gsap.set(shotB, { opacity: 0, visibility: "hidden" });
|
||||
gsap.set(intro, { opacity: 0, visibility: "hidden", y: -12 });
|
||||
gsap.set(hitType, { opacity: 0, visibility: "hidden", scale: 1.15 });
|
||||
gsap.set(freeze, { opacity: 0, visibility: "hidden" });
|
||||
gsap.set([freezeOutline, freezeFlash, freezeContour, freezeBadge], { opacity: 0 });
|
||||
gsap.set(cutFx, { opacity: 0, visibility: "hidden", scaleX: 0.4, x: -200 });
|
||||
gsap.set(bLeft, { opacity: 0, x: -48 });
|
||||
gsap.set(bCards, { opacity: 0, x: 40 });
|
||||
gsap.set(bars, { scaleY: 0.55 });
|
||||
|
||||
// Full duration anchor (seek-safe length)
|
||||
tl.to({}, { duration: DURATION, ease: "none" }, 0);
|
||||
|
||||
// ── 0.00–0.70: intro push-in + secondary type ──
|
||||
tl.to(aCard, { scale: 1, y: 0, duration: 0.55, ease: "power3.out" }, 0);
|
||||
tl.to(stage, { scale: 1 + 0.04 * intensity, duration: 0.7, ease: "power1.out" }, 0);
|
||||
tl.to(bars, { scaleY: 1, duration: 0.5, ease: "power2.out", stagger: 0.02 }, 0.12);
|
||||
tl.set(intro, { visibility: "visible" }, 0.12);
|
||||
tl.to(intro, { opacity: 1, y: 0, duration: 0.28, ease: "power2.out" }, 0.12);
|
||||
tl.to(intro, { opacity: 0, y: -8, duration: 0.18, ease: "power2.in" }, 0.52);
|
||||
tl.set(intro, { visibility: "hidden", opacity: 0 }, 0.7);
|
||||
|
||||
// ── 0.70–1.35: beat 1 — hard reframe ──
|
||||
tl.to(
|
||||
aCrop,
|
||||
{
|
||||
scale: 1 + 0.18 * intensity,
|
||||
x: -80 * intensity,
|
||||
y: 24 * intensity,
|
||||
duration: 0.22,
|
||||
ease: "power4.out",
|
||||
},
|
||||
beat1,
|
||||
);
|
||||
tl.to(
|
||||
stage,
|
||||
{
|
||||
scale: 1.08 * intensity > 1.2 ? 1.2 : 1 + 0.08 * intensity,
|
||||
duration: 0.28,
|
||||
ease: "expo.out",
|
||||
},
|
||||
beat1,
|
||||
);
|
||||
tl.to(
|
||||
bars,
|
||||
{
|
||||
scaleY: function (i) {
|
||||
// Deterministic bar pattern (no Math.random)
|
||||
var pattern = [0.55, 1.15, 0.7, 1.35, 0.9, 1.4, 0.65, 1.1, 0.5, 1.25, 0.8, 1.45];
|
||||
return pattern[i % pattern.length];
|
||||
},
|
||||
duration: 0.18,
|
||||
ease: "power3.out",
|
||||
stagger: 0.01,
|
||||
},
|
||||
beat1,
|
||||
);
|
||||
|
||||
// ── 1.35–2.20: speed ramp into freeze ──
|
||||
var rampStart = 1.35;
|
||||
var rampDur = Math.max(0.2, freezeAt - rampStart);
|
||||
tl.to(
|
||||
aCrop,
|
||||
{
|
||||
scale: 1.28 * (0.9 + 0.1 * intensity),
|
||||
x: 28 * intensity,
|
||||
y: -12 * intensity,
|
||||
duration: rampDur,
|
||||
ease: "power3.in",
|
||||
},
|
||||
rampStart,
|
||||
);
|
||||
tl.to(
|
||||
stage,
|
||||
{
|
||||
scale: 1.12,
|
||||
filter: "blur(0px) brightness(1.08)",
|
||||
duration: rampDur,
|
||||
ease: "power2.in",
|
||||
},
|
||||
rampStart,
|
||||
);
|
||||
tl.to(
|
||||
bars,
|
||||
{
|
||||
scaleY: 1.55,
|
||||
duration: rampDur * 0.85,
|
||||
ease: "power2.in",
|
||||
stagger: { each: 0.012, from: "center" },
|
||||
},
|
||||
rampStart,
|
||||
);
|
||||
|
||||
// ── 2.20–3.00: freeze hit (snap-on for seek/snapshot stability) ──
|
||||
tl.set(freeze, { visibility: "visible", opacity: 1 }, freezeAt);
|
||||
tl.set(freezeOutline, { opacity: 1 }, freezeAt);
|
||||
tl.set(freezeContour, { opacity: 0.85 }, freezeAt);
|
||||
tl.set(freezeBadge, { opacity: 1 }, freezeAt);
|
||||
tl.set(hitType, { visibility: "visible", opacity: 1, scale: 1 }, freezeAt);
|
||||
// Hold crop/stage where ramp ended
|
||||
tl.to(aCrop, { duration: 0.01, ease: "none" }, freezeAt);
|
||||
tl.to(
|
||||
stage,
|
||||
{
|
||||
scale: 1.12,
|
||||
filter: "blur(0px) brightness(1.15)",
|
||||
duration: 0.05,
|
||||
ease: "power4.out",
|
||||
},
|
||||
freezeAt,
|
||||
);
|
||||
// Single exposure flash (not a strobe sequence)
|
||||
tl.fromTo(
|
||||
freezeFlash,
|
||||
{ opacity: 0.55 * intensity },
|
||||
{ opacity: 0, duration: 0.28, ease: "power2.out" },
|
||||
freezeAt,
|
||||
);
|
||||
tl.to(
|
||||
hitType,
|
||||
{ opacity: 0, scale: 0.96, duration: 0.2, ease: "power2.in" },
|
||||
freezeAt + 0.55,
|
||||
);
|
||||
tl.set(hitType, { visibility: "hidden", opacity: 0 }, freezeAt + 0.78);
|
||||
// Soft settle of freeze dressing before cut
|
||||
tl.to(
|
||||
[freezeOutline, freezeContour, freezeBadge],
|
||||
{ opacity: 0.75, duration: 0.35, ease: "sine.out" },
|
||||
freezeAt + 0.35,
|
||||
);
|
||||
|
||||
// ── 3.00–3.35: hard cut connector ──
|
||||
tl.set(cutFx, { visibility: "visible" }, cutAt);
|
||||
tl.fromTo(
|
||||
cutFx,
|
||||
{ opacity: 0, scaleX: 0.35, x: -280 },
|
||||
{
|
||||
opacity: 0.95,
|
||||
scaleX: 1.6,
|
||||
x: 120,
|
||||
duration: 0.12,
|
||||
ease: "power4.in",
|
||||
},
|
||||
cutAt,
|
||||
);
|
||||
tl.to(
|
||||
stage,
|
||||
{
|
||||
filter: "blur(14px) brightness(1.35)",
|
||||
scaleX: 1.35,
|
||||
scaleY: 0.92,
|
||||
duration: 0.12,
|
||||
ease: "power3.in",
|
||||
},
|
||||
cutAt,
|
||||
);
|
||||
// Switch shots mid-cut — shot B content lands immediately (no black gap)
|
||||
var switchAt = cutAt + 0.14;
|
||||
tl.set(shotA, { opacity: 0, visibility: "hidden" }, switchAt);
|
||||
tl.set(shotB, { opacity: 1, visibility: "visible" }, switchAt);
|
||||
tl.set(hitType, { opacity: 0, visibility: "hidden" }, switchAt);
|
||||
tl.set(
|
||||
[freeze, freezeOutline, freezeFlash, freezeContour, freezeBadge],
|
||||
{ opacity: 0, visibility: "hidden" },
|
||||
switchAt,
|
||||
);
|
||||
tl.set(freeze, { visibility: "hidden" }, switchAt);
|
||||
// Shot B content visible on switch (offset settles after)
|
||||
tl.set(bLeft, { opacity: 1, x: -24 }, switchAt);
|
||||
tl.set(bCards, { opacity: 1, x: 24 }, switchAt);
|
||||
tl.to(
|
||||
stage,
|
||||
{
|
||||
filter: "blur(0px) brightness(1)",
|
||||
scale: 1,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
duration: 0.18,
|
||||
ease: "power3.out",
|
||||
},
|
||||
switchAt,
|
||||
);
|
||||
tl.to(cutFx, { opacity: 0, x: 360, duration: 0.16, ease: "power2.out" }, cutAt + 0.16);
|
||||
tl.set(cutFx, { visibility: "hidden", opacity: 0 }, cutAt + 0.34);
|
||||
|
||||
// ── after switch: shot B settle (no scale overlap with recover) ──
|
||||
tl.to(bLeft, { x: 0, duration: 0.4, ease: "power3.out" }, switchAt);
|
||||
tl.to(
|
||||
bCards,
|
||||
{
|
||||
x: 0,
|
||||
duration: 0.38,
|
||||
ease: "power3.out",
|
||||
stagger: 0.06,
|
||||
},
|
||||
switchAt + 0.04,
|
||||
);
|
||||
tl.to(stage, { scale: 1.02, duration: 1.17, ease: "sine.out" }, switchAt + 0.18);
|
||||
|
||||
// ── 4.70–5.20: final beat punch-in ──
|
||||
tl.to(
|
||||
stage,
|
||||
{
|
||||
scale: 1.08 * (0.9 + 0.1 * intensity),
|
||||
duration: 0.14,
|
||||
ease: "power4.out",
|
||||
},
|
||||
beat3,
|
||||
);
|
||||
tl.to(
|
||||
bCards[1],
|
||||
{
|
||||
scale: 1.04,
|
||||
duration: 0.12,
|
||||
ease: "power3.out",
|
||||
},
|
||||
beat3,
|
||||
);
|
||||
tl.to(
|
||||
bCards[1],
|
||||
{
|
||||
scale: 1,
|
||||
duration: 0.22,
|
||||
ease: "power2.out",
|
||||
},
|
||||
beat3 + 0.14,
|
||||
);
|
||||
// Micro freeze feel: brief hold of scale
|
||||
tl.to(
|
||||
stage,
|
||||
{ scale: 1.08 * (0.9 + 0.1 * intensity), duration: 0.2, ease: "none" },
|
||||
beat3 + 0.14,
|
||||
);
|
||||
|
||||
// ── 5.20–6.00: hold readable end card (≥0.8s) ──
|
||||
tl.to(stage, { scale: 1.03, duration: 0.35, ease: "power2.out" }, holdAt);
|
||||
// Ensure transient layers stay dead through end
|
||||
tl.set(intro, { opacity: 0, visibility: "hidden" }, holdAt);
|
||||
tl.set(hitType, { opacity: 0, visibility: "hidden" }, holdAt);
|
||||
tl.set(freeze, { opacity: 0, visibility: "hidden" }, holdAt);
|
||||
tl.set(cutFx, { opacity: 0, visibility: "hidden" }, holdAt);
|
||||
|
||||
window.__timelines["beat-freeze-cut"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `transition` `music` `beat-driven` `freeze-frame`.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,13 @@ title: "Camcorder HUD"
|
||||
description: "Responsive REC, battery, editable date placeholder, and timeline-driven counter overlay for creator-camera and camcorder treatments"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/camcorder-hud.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/camcorder-hud.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/camcorder-hud.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Camcorder HUD preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add camcorder-hud
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add camcorder-hud" />
|
||||
|
||||
That writes one file: `compositions/camcorder-hud.html`.
|
||||
|
||||
@@ -32,6 +32,178 @@ It runs for 4 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`camcorder-hud.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#ch-hud {
|
||||
--hf-camcorder-color: #fff;
|
||||
--hf-camcorder-accent: #f12c2c;
|
||||
--hf-camcorder-inset: 5vmin;
|
||||
--hf-camcorder-font-size: 3.7vmin;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
color: var(--hf-camcorder-color);
|
||||
font-family: monospace;
|
||||
font-size: var(--hf-camcorder-font-size);
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
pointer-events: none;
|
||||
-webkit-text-stroke: 0.12vmin rgb(0 0 0 / 92%);
|
||||
text-shadow: 0 0.2vmin 0.6vmin rgb(0 0 0 / 90%);
|
||||
}
|
||||
|
||||
#ch-hud .ch-corner {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#ch-hud .ch-record {
|
||||
top: var(--hf-camcorder-inset);
|
||||
left: var(--hf-camcorder-inset);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.8vmin;
|
||||
}
|
||||
|
||||
#ch-hud .ch-record-dot {
|
||||
width: 2.2vmin;
|
||||
height: 2.2vmin;
|
||||
border-radius: 50%;
|
||||
background: var(--hf-camcorder-accent);
|
||||
box-shadow: 0 0 0.9vmin color-mix(in srgb, var(--hf-camcorder-accent) 55%, transparent);
|
||||
}
|
||||
|
||||
#ch-hud .ch-battery {
|
||||
top: var(--hf-camcorder-inset);
|
||||
right: var(--hf-camcorder-inset);
|
||||
width: 7vmin;
|
||||
height: 3.5vmin;
|
||||
border: 0.35vmin solid currentcolor;
|
||||
}
|
||||
|
||||
#ch-hud .ch-battery::before {
|
||||
position: absolute;
|
||||
top: 0.7vmin;
|
||||
left: 0.7vmin;
|
||||
width: 4.7vmin;
|
||||
height: 1.4vmin;
|
||||
background: currentcolor;
|
||||
content: "";
|
||||
}
|
||||
|
||||
#ch-hud .ch-battery::after {
|
||||
position: absolute;
|
||||
top: 0.7vmin;
|
||||
right: -1vmin;
|
||||
width: 0.7vmin;
|
||||
height: 1.4vmin;
|
||||
background: currentcolor;
|
||||
content: "";
|
||||
}
|
||||
|
||||
#ch-hud .ch-date {
|
||||
bottom: var(--hf-camcorder-inset);
|
||||
left: var(--hf-camcorder-inset);
|
||||
}
|
||||
|
||||
#ch-hud .ch-counter {
|
||||
right: var(--hf-camcorder-inset);
|
||||
bottom: var(--hf-camcorder-inset);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#ch-hud .ch-mode {
|
||||
display: block;
|
||||
margin-bottom: 0.7vmin;
|
||||
font-size: 2.6vmin;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="camcorder-hud"
|
||||
data-composition-id="camcorder-hud"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="ch-hud" data-hf-counter-start="0" aria-hidden="true">
|
||||
<div class="ch-corner ch-record"><span class="ch-record-dot"></span><span>REC</span></div>
|
||||
<div class="ch-corner ch-battery"></div>
|
||||
<div class="ch-corner ch-date">JAN 01 2000<br />12:00 AM</div>
|
||||
<div class="ch-corner ch-counter">
|
||||
<span class="ch-mode">SP · 16:9</span><span class="ch-counter-value">00:00:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var hud = document.getElementById("ch-hud");
|
||||
var counter = hud && hud.querySelector(".ch-counter-value");
|
||||
var recordDot = hud && hud.querySelector(".ch-record-dot");
|
||||
var host = hud && hud.closest("[data-composition-id]");
|
||||
if (!hud || !counter || !recordDot || !host) {
|
||||
throw new Error("Camcorder HUD could not find its composition host");
|
||||
}
|
||||
|
||||
var compositionId = host.getAttribute("data-composition-id");
|
||||
var duration = Number(host.getAttribute("data-duration")) || 4;
|
||||
var counterStart = Number(hud.getAttribute("data-hf-counter-start")) || 0;
|
||||
var state = { seconds: 0 };
|
||||
|
||||
function formatCounter(value) {
|
||||
var seconds = Math.max(0, Math.floor(value));
|
||||
var hours = Math.floor(seconds / 3600);
|
||||
var minutes = Math.floor((seconds % 3600) / 60);
|
||||
var remainder = seconds % 60;
|
||||
return [hours, minutes, remainder]
|
||||
.map(function (part) {
|
||||
return String(part).padStart(2, "0");
|
||||
})
|
||||
.join(":");
|
||||
}
|
||||
|
||||
function updateHud() {
|
||||
counter.textContent = formatCounter(counterStart + state.seconds);
|
||||
recordDot.style.opacity = Math.floor(state.seconds / 0.55) % 2 === 0 ? "1" : "0.25";
|
||||
}
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
tl.to(state, { seconds: duration, duration: duration, ease: "none", onUpdate: updateHud }, 0);
|
||||
updateHud();
|
||||
window.__timelines[compositionId] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `camcorder` `camera` `recording` `hud` `overlay` `creator` `media-treatment-overlay`.
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
---
|
||||
title: "Dolly Zoom (Vertigo Shot)"
|
||||
description: "Camera tracks toward or away from the subject while the field of view changes the opposite way, so the subject holds its frame size while the background collapses behind it or rushes in. Focal length is solved from dolly distance every frame against d * tan(FOV/2) = const, not keyframed beside it. Requires depth: the subject sits on the z = 0 plane and the content behind it must be layered or genuinely 3D (translateZ at two or more distinct depths) -- a flat element gains nothing from this move."
|
||||
---
|
||||
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
|
||||
|
||||
<VariablesExplorer
|
||||
previewSrc="/public/catalog/preview/blocks/camera-dolly-zoom.html"
|
||||
compositionId="camera-dolly-zoom"
|
||||
compositionSrc="compositions/camera-dolly-zoom.html"
|
||||
variables={[{"id":"direction","type":"enum","label":"Dolly direction","default":"out","options":[{"value":"out","label":"Dolly out + zoom in (background rushes in)"},{"value":"in","label":"Dolly in + zoom out (background falls away)"}]},{"id":"strength","type":"number","label":"Move strength (end / start distance ratio)","default":2,"min":1.1,"max":4,"step":0.1},{"id":"subjectDistance","type":"number","label":"Camera-to-subject distance at the start of the move","default":1400,"min":600,"max":3000,"step":50,"unit":"px"},{"id":"easing","type":"enum","label":"Dolly easing","default":"power2.inOut","options":[{"value":"none","label":"Linear"},{"value":"sine.inOut","label":"Sine in-out"},{"value":"power1.inOut","label":"Power1 in-out"},{"value":"power2.inOut","label":"Power2 in-out"},{"value":"power3.inOut","label":"Power3 in-out"}]}]}
|
||||
>
|
||||
|
||||
```html camera-dolly-zoom.html
|
||||
<!doctype html>
|
||||
<html
|
||||
lang="en"
|
||||
data-composition-variables='[
|
||||
{
|
||||
"id": "direction",
|
||||
"type": "enum",
|
||||
"label": "Dolly direction",
|
||||
"default": "out",
|
||||
"options": [
|
||||
{ "value": "out", "label": "Dolly out + zoom in (background rushes in)" },
|
||||
{ "value": "in", "label": "Dolly in + zoom out (background falls away)" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "strength",
|
||||
"type": "number",
|
||||
"label": "Move strength (end / start distance ratio)",
|
||||
"default": 2,
|
||||
"min": 1.1,
|
||||
"max": 4,
|
||||
"step": 0.1
|
||||
},
|
||||
{
|
||||
"id": "subjectDistance",
|
||||
"type": "number",
|
||||
"label": "Camera-to-subject distance at the start of the move",
|
||||
"default": 1400,
|
||||
"min": 600,
|
||||
"max": 3000,
|
||||
"step": 50,
|
||||
"unit": "px"
|
||||
},
|
||||
{
|
||||
"id": "easing",
|
||||
"type": "enum",
|
||||
"label": "Dolly easing",
|
||||
"default": "power2.inOut",
|
||||
"options": [
|
||||
{ "value": "none", "label": "Linear" },
|
||||
{ "value": "sine.inOut", "label": "Sine in-out" },
|
||||
{ "value": "power1.inOut", "label": "Power1 in-out" },
|
||||
{ "value": "power2.inOut", "label": "Power2 in-out" },
|
||||
{ "value": "power3.inOut", "label": "Power3 in-out" }
|
||||
]
|
||||
}
|
||||
]'
|
||||
>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!--
|
||||
===================================================================
|
||||
DOLLY ZOOM (Vertigo / Hitchcock shot)
|
||||
===================================================================
|
||||
|
||||
The camera tracks toward or away from the subject while the field of
|
||||
view changes the opposite way, so the SUBJECT HOLDS ITS FRAME SIZE
|
||||
while the background collapses behind it or rushes in.
|
||||
|
||||
THE CONSTRAINT (this is the whole effect, and it is exact, not taste):
|
||||
|
||||
d * tan(FOV / 2) = constant
|
||||
|
||||
where d is the camera-to-subject distance. Get it wrong and the shot
|
||||
reads as a clumsy zoom. So focal length is SOLVED from distance every
|
||||
frame; the two are never keyframed side by side.
|
||||
|
||||
In CSS 3D, `perspective: P px` IS the focal length in pixels, and it
|
||||
also places the camera exactly P px in front of the z = 0 plane, so
|
||||
tan(FOV / 2) = (viewportWidth / 2) / P. Substituting collapses the
|
||||
invariant to a single ratio:
|
||||
|
||||
P(t) / d(t) = SUBJECT_SCALE (constant)
|
||||
|
||||
Everything the rig animates falls out of that one line (see the script).
|
||||
|
||||
-------------------------------------------------------------------
|
||||
WHAT THIS PRIMITIVE REQUIRES OF ITS CONTENT
|
||||
-------------------------------------------------------------------
|
||||
|
||||
DEPTH. A flat element gains nothing from a dolly zoom -- with one
|
||||
layer there is no background to collapse and the move is invisible.
|
||||
Content dropped into #dz-rig MUST be layered or genuinely 3D:
|
||||
|
||||
* The SUBJECT sits on the z = 0 plane -- `translateZ(0)`. That is
|
||||
the plane the solve holds; anything you want frame-locked goes
|
||||
there and nowhere else.
|
||||
* BACKGROUND layers get `translateZ(-N px)`, N > 0, at least two or
|
||||
three distinct depths (this demo ships five arches plus a back
|
||||
wall). More separation = stronger effect.
|
||||
* Everything inside #dz-rig must keep `transform-style: preserve-3d`
|
||||
on its ancestors and must NOT sit behind `overflow: hidden`, or
|
||||
the browser flattens the scene and the depth disappears.
|
||||
* Keep every layer at a depth where `d + N > 0` for the whole move,
|
||||
i.e. no layer in front of the camera's closest approach.
|
||||
|
||||
-------------------------------------------------------------------
|
||||
DETERMINISM
|
||||
-------------------------------------------------------------------
|
||||
|
||||
State at frame N is computed from N alone. d(t) is a lerp of the
|
||||
eased tween progress, P is solved from d, and both are written by a
|
||||
property setter on the tweened driver object. Nothing accumulates,
|
||||
nothing reads a clock, and no frame depends on the frame before it.
|
||||
(The setter matters: `tl.eventCallback("onUpdate", ...)` does NOT
|
||||
fire on `tl.seek()`, so anything driven that way freezes on frame 0.)
|
||||
-->
|
||||
<div
|
||||
id="dz-root"
|
||||
data-composition-id="camera-dolly-zoom"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<style>
|
||||
#dz-root {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #0b1020;
|
||||
font-family: Inter, sans-serif;
|
||||
}
|
||||
|
||||
/* The camera. `perspective` is the focal length and is solved from
|
||||
the dolly distance every frame -- see the script. */
|
||||
#dz-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
perspective-origin: 50% 50%;
|
||||
}
|
||||
|
||||
/* The rig root. Carries the camera's z offset so the subject plane
|
||||
always sits exactly `d` in front of the camera. */
|
||||
#dz-rig {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
/* --- authored depth layers ------------------------------------ */
|
||||
|
||||
.dz-arch {
|
||||
position: absolute;
|
||||
left: -140px;
|
||||
top: -80px;
|
||||
width: 2200px;
|
||||
height: 1240px;
|
||||
border: 16px solid #465280;
|
||||
border-radius: 8px;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
#dz-backwall {
|
||||
position: absolute;
|
||||
left: -3040px;
|
||||
top: -1710px;
|
||||
width: 8000px;
|
||||
height: 4500px;
|
||||
background: #10162c;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
/* Bright doorway on the back wall -- the layer whose on-screen size
|
||||
changes while the subject's does not. */
|
||||
#dz-portal {
|
||||
position: absolute;
|
||||
left: 3300px;
|
||||
top: 1850px;
|
||||
width: 1400px;
|
||||
height: 1500px;
|
||||
background: #cfe4ff;
|
||||
}
|
||||
|
||||
/* --- the subject: z = 0, the plane the solve holds ------------- */
|
||||
|
||||
#dz-subject {
|
||||
position: absolute;
|
||||
left: 840px;
|
||||
top: 300px;
|
||||
width: 240px;
|
||||
height: 620px;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
.dz-fig-head {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto;
|
||||
border-radius: 50%;
|
||||
background: #f2b134;
|
||||
}
|
||||
.dz-fig-body {
|
||||
width: 240px;
|
||||
height: 470px;
|
||||
margin-top: 30px;
|
||||
border-radius: 120px 120px 10px 10px;
|
||||
background: #f2b134;
|
||||
}
|
||||
|
||||
/* --- flat overlay (outside the 3D rig) ------------------------- */
|
||||
|
||||
.dz-label {
|
||||
position: absolute;
|
||||
left: 64px;
|
||||
bottom: 56px;
|
||||
padding: 14px 22px;
|
||||
border-radius: 8px;
|
||||
background: #0b1020;
|
||||
color: #e9eeff;
|
||||
font-size: 26px;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="dz-stage" class="clip" data-start="0" data-duration="4" data-track-index="0">
|
||||
<div id="dz-rig">
|
||||
<div id="dz-backwall" data-layout-allow-overflow style="transform: translateZ(-3200px)">
|
||||
<div id="dz-portal"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="dz-arch"
|
||||
data-layout-allow-overflow
|
||||
style="transform: translateZ(-2360px); border-color: #384166"
|
||||
></div>
|
||||
<div
|
||||
class="dz-arch"
|
||||
data-layout-allow-overflow
|
||||
style="transform: translateZ(-1720px); border-color: #465280"
|
||||
></div>
|
||||
<div
|
||||
class="dz-arch"
|
||||
data-layout-allow-overflow
|
||||
style="transform: translateZ(-1120px); border-color: #57669c"
|
||||
></div>
|
||||
<div
|
||||
class="dz-arch"
|
||||
data-layout-allow-overflow
|
||||
style="transform: translateZ(-560px); border-color: #6b7cb8"
|
||||
></div>
|
||||
<div
|
||||
class="dz-arch"
|
||||
data-layout-allow-overflow
|
||||
style="transform: translateZ(0px); border-color: #8194d4"
|
||||
></div>
|
||||
|
||||
<div id="dz-subject" style="transform: translateZ(0px)">
|
||||
<div class="dz-fig-head"></div>
|
||||
<div class="dz-fig-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dz-label">Dolly zoom · d × tan(fov/2) = const</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
(function () {
|
||||
var DEFAULTS = {
|
||||
direction: "out",
|
||||
strength: 2,
|
||||
subjectDistance: 1400,
|
||||
easing: "power2.inOut",
|
||||
};
|
||||
var declared =
|
||||
window.__hyperframes && window.__hyperframes.getVariables
|
||||
? window.__hyperframes.getVariables()
|
||||
: {};
|
||||
var vars = Object.assign({}, DEFAULTS, declared);
|
||||
|
||||
function clamp(value, lo, hi, fallback) {
|
||||
var n = Number(value);
|
||||
if (!isFinite(n)) return fallback;
|
||||
return Math.min(hi, Math.max(lo, n));
|
||||
}
|
||||
function oneOf(value, allowed, fallback) {
|
||||
return allowed.indexOf(String(value)) >= 0 ? String(value) : fallback;
|
||||
}
|
||||
|
||||
var direction = oneOf(vars.direction, ["in", "out"], DEFAULTS.direction);
|
||||
var strength = clamp(vars.strength, 1.1, 4, DEFAULTS.strength);
|
||||
var ease = oneOf(
|
||||
vars.easing,
|
||||
["none", "sine.inOut", "power1.inOut", "power2.inOut", "power3.inOut"],
|
||||
DEFAULTS.easing,
|
||||
);
|
||||
|
||||
// d0 = camera-to-subject distance at the start of the move (px).
|
||||
// d1 = distance at the end. "out" pulls the camera back and zooms in;
|
||||
// "in" pushes the camera forward and zooms out.
|
||||
var d0 = clamp(vars.subjectDistance, 600, 3000, DEFAULTS.subjectDistance);
|
||||
var d1 = direction === "in" ? d0 / strength : d0 * strength;
|
||||
|
||||
// ---- the solve --------------------------------------------------
|
||||
// Subject size is fixed iff d * tan(FOV/2) = const. With CSS 3D,
|
||||
// tan(FOV/2) = (width/2) / perspective, so that invariant is exactly
|
||||
// perspective / distance = SUBJECT_SCALE. Focal length is therefore
|
||||
// derived from the dolly, never animated alongside it.
|
||||
var SUBJECT_SCALE = 1; // subject renders 1:1 => its plane is z = 0
|
||||
|
||||
var stage = document.getElementById("dz-stage");
|
||||
var rig = document.getElementById("dz-rig");
|
||||
|
||||
function applyCamera(u) {
|
||||
var d = d0 + (d1 - d0) * u; // dolly -- pure function of u
|
||||
var P = SUBJECT_SCALE * d; // focal length solved from d
|
||||
var camZ = P - d; // put the subject plane at camera distance d
|
||||
stage.style.perspective = P + "px";
|
||||
rig.style.transform = "translateZ(" + camZ + "px)";
|
||||
}
|
||||
|
||||
// GSAP suppresses timeline-level onUpdate during seek(), so the camera
|
||||
// is driven by a property setter on the tweened object instead: that
|
||||
// fires on every render, seeks included.
|
||||
var camera = {
|
||||
_u: 0,
|
||||
get u() {
|
||||
return this._u;
|
||||
},
|
||||
set u(value) {
|
||||
this._u = value;
|
||||
applyCamera(value);
|
||||
},
|
||||
};
|
||||
|
||||
applyCamera(0);
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
tl.to(camera, { u: 1, duration: 4, ease: ease }, 0);
|
||||
window.__timelines["camera-dolly-zoom"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</VariablesExplorer>
|
||||
|
||||
## Install
|
||||
|
||||
<InstallCommand command="npx hyperframes add camera-dolly-zoom" />
|
||||
|
||||
That writes one file: `compositions/camera-dolly-zoom.html`.
|
||||
|
||||
## Add it to your video
|
||||
|
||||
It runs for 4 seconds at 1920×1080. Paste this into your composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="camera-dolly-zoom"
|
||||
data-composition-src="compositions/camera-dolly-zoom.html"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `camera` `depth` `3d` `cinematic`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
||||
- [Build a richer composition](/go-further)
|
||||
@@ -3,34 +3,398 @@ title: "Chromatic Radial Split"
|
||||
description: "Shader transition with chromatic aberration radial split"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/chromatic-radial-split.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/chromatic-radial-split.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/chromatic-radial-split.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Chromatic Radial Split preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add chromatic-radial-split
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add chromatic-radial-split" />
|
||||
|
||||
That writes one file: `compositions/chromatic-radial-split.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`chromatic-radial-split.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="chromatic-radial-split"
|
||||
data-composition-src="compositions/chromatic-radial-split.html"
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<style>
|
||||
[data-composition-id="main"] {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #22223b;
|
||||
}
|
||||
|
||||
/* Blueprint grid */
|
||||
.bp-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.bp-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.bp-right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 720px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 60px 60px 60px 40px;
|
||||
}
|
||||
.bp-divider {
|
||||
position: absolute;
|
||||
left: 1200px;
|
||||
top: 60px;
|
||||
bottom: 60px;
|
||||
width: 1px;
|
||||
background: #7678ed;
|
||||
opacity: 0.15;
|
||||
}
|
||||
.bp-number {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4em;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 60px;
|
||||
color: #7678ed;
|
||||
}
|
||||
|
||||
/* Right panel typography */
|
||||
.bp-name {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 38px;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 20px;
|
||||
color: #7678ed;
|
||||
}
|
||||
.bp-plabel {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
color: #7678ed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.bp-prompt {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
padding: 16px 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
color: #7678ed;
|
||||
background: rgba(118, 120, 237, 0.06);
|
||||
border: 1px solid rgba(118, 120, 237, 0.12);
|
||||
}
|
||||
.bp-desc {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
color: #9a8c98;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* GL scenes (hidden, just for texture capture) */
|
||||
.scene {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.scene-label {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 140px;
|
||||
letter-spacing: 0.06em;
|
||||
opacity: 0.12;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="bp-grid"></div>
|
||||
<div class="bp-divider"></div>
|
||||
<div class="bp-number">08 / 14</div>
|
||||
|
||||
<!-- GL demo scenes (captured as textures, then hidden) -->
|
||||
<div id="s1" class="scene" style="background: #22223b">
|
||||
<div class="scene-label" style="color: #7678ed">SCENE A</div>
|
||||
</div>
|
||||
<div id="s2" class="scene" style="background: #7678ed; opacity: 0">
|
||||
<div class="scene-label" style="color: #22223b">SCENE B</div>
|
||||
</div>
|
||||
|
||||
<!-- GL canvas (left panel only) -->
|
||||
<canvas
|
||||
id="gl-canvas"
|
||||
width="1200"
|
||||
height="1080"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
"
|
||||
>
|
||||
</canvas>
|
||||
|
||||
<!-- Right panel metadata -->
|
||||
<div class="bp-right">
|
||||
<div class="bp-name">Chromatic Radial Split</div>
|
||||
<div class="bp-plabel">Prompt</div>
|
||||
<div class="bp-prompt">"use chromatic radial split shader transition"</div>
|
||||
<div class="bp-desc">
|
||||
RGB channels separate and converge radially. From-scene splits outward, to-scene converges
|
||||
inward.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var glCanvas = document.getElementById("gl-canvas");
|
||||
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
|
||||
if (!gl) {
|
||||
console.warn("WebGL not available");
|
||||
window.__timelines["main"] = gsap.timeline({ paused: true });
|
||||
} else {
|
||||
gl.viewport(0, 0, 1200, 1080);
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
||||
var sceneTextures = {};
|
||||
|
||||
function captureScene(sceneId) {
|
||||
var scene = document.getElementById(sceneId);
|
||||
var origOpacity = scene.style.opacity;
|
||||
var origZ = scene.style.zIndex;
|
||||
scene.style.opacity = "1";
|
||||
scene.style.zIndex = "999";
|
||||
scene.offsetHeight;
|
||||
var c = document.createElement("canvas");
|
||||
c.width = 1200;
|
||||
c.height = 1080;
|
||||
var ctx = c.getContext("2d");
|
||||
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
|
||||
ctx.fillRect(0, 0, 1200, 1080);
|
||||
var sr = scene.getBoundingClientRect();
|
||||
var els = scene.querySelectorAll("*");
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
var el = els[i],
|
||||
cs = window.getComputedStyle(el);
|
||||
if (cs.display === "none" || cs.visibility === "hidden") continue;
|
||||
var r = el.getBoundingClientRect();
|
||||
if (r.width < 1 || r.height < 1) continue;
|
||||
var x = r.left - sr.left,
|
||||
y = r.top - sr.top,
|
||||
w = r.width,
|
||||
h = r.height;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
|
||||
var bg = cs.backgroundColor;
|
||||
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
}
|
||||
var hasChildEls = el.querySelector("div,span,img,video");
|
||||
var text = "";
|
||||
for (var j = 0; j < el.childNodes.length; j++)
|
||||
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
|
||||
text = text.trim();
|
||||
if (text && !hasChildEls) {
|
||||
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
|
||||
ctx.fillStyle = cs.color;
|
||||
if (cs.letterSpacing && cs.letterSpacing !== "normal")
|
||||
ctx.letterSpacing = cs.letterSpacing;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(text, x + w / 2, y + h / 2);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
scene.style.opacity = origOpacity;
|
||||
scene.style.zIndex = origZ;
|
||||
var tex = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
|
||||
sceneTextures[sceneId] = tex;
|
||||
}
|
||||
|
||||
var vertSrc =
|
||||
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
|
||||
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
|
||||
var quadBuf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.bufferData(
|
||||
gl.ARRAY_BUFFER,
|
||||
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
||||
gl.STATIC_DRAW,
|
||||
);
|
||||
function compileShader(src, type) {
|
||||
var s = gl.createShader(type);
|
||||
gl.shaderSource(s, src);
|
||||
gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
|
||||
console.error("Shader:", gl.getShaderInfoLog(s));
|
||||
return s;
|
||||
}
|
||||
function mkProg(fragSrc) {
|
||||
var p = gl.createProgram();
|
||||
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
|
||||
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
|
||||
gl.linkProgram(p);
|
||||
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
|
||||
console.error("Link:", gl.getProgramInfoLog(p));
|
||||
return p;
|
||||
}
|
||||
|
||||
var H =
|
||||
"precision mediump float;" +
|
||||
"varying vec2 v_uv;" +
|
||||
"uniform sampler2D u_from, u_to;" +
|
||||
"uniform float u_progress;" +
|
||||
"uniform vec2 u_resolution;\n";
|
||||
|
||||
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
|
||||
var progTrans = mkProg(
|
||||
H +
|
||||
"void main(){vec2 c=v_uv-.5;float fromShift=u_progress*.06;float fr=texture2D(u_from,clamp(v_uv+c*fromShift,0.,1.)).r;float fg=texture2D(u_from,v_uv).g;float fb=texture2D(u_from,clamp(v_uv-c*fromShift,0.,1.)).b;vec3 fromSplit=vec3(fr,fg,fb);float toShift=(1.-u_progress)*.06;float tr=texture2D(u_to,clamp(v_uv-c*toShift,0.,1.)).r;float tg=texture2D(u_to,v_uv).g;float tb=texture2D(u_to,clamp(v_uv+c*toShift,0.,1.)).b;vec3 toSplit=vec3(tr,tg,tb);gl_FragColor=vec4(mix(fromSplit,toSplit,u_progress),1.);}",
|
||||
);
|
||||
|
||||
function renderShader(prog, texFrom, texTo, progress) {
|
||||
gl.useProgram(prog);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texFrom);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texTo);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
|
||||
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
|
||||
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
|
||||
var pos = gl.getAttribLocation(prog, "a_pos");
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.enableVertexAttribArray(pos);
|
||||
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
function easeInOut(p) {
|
||||
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
|
||||
}
|
||||
|
||||
captureScene("s1");
|
||||
captureScene("s2");
|
||||
glCanvas.style.display = "block";
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
document.querySelectorAll(".scene").forEach(function (s) {
|
||||
s.style.opacity = "0";
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({
|
||||
paused: true,
|
||||
onUpdate: function () {
|
||||
var t = tl.time();
|
||||
if (t >= 1.0 && t < 3.0) {
|
||||
renderShader(
|
||||
progTrans,
|
||||
sceneTextures["s1"],
|
||||
sceneTextures["s2"],
|
||||
easeInOut((t - 1.0) / 2.0),
|
||||
);
|
||||
} else if (t >= 3.0) {
|
||||
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
|
||||
} else {
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
|
||||
window.__timelines["main"] = tl;
|
||||
} // end if (gl)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="chromatic-radial-split" data-composition-src="compositions/chromatic-radial-split.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,398 @@ title: "Cinematic Zoom"
|
||||
description: "Shader transition with dramatic zoom blur"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/cinematic-zoom.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/cinematic-zoom.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/cinematic-zoom.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Cinematic Zoom preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add cinematic-zoom
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add cinematic-zoom" />
|
||||
|
||||
That writes one file: `compositions/cinematic-zoom.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`cinematic-zoom.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="cinematic-zoom"
|
||||
data-composition-src="compositions/cinematic-zoom.html"
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<style>
|
||||
[data-composition-id="main"] {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #3d348b;
|
||||
}
|
||||
|
||||
/* Blueprint grid */
|
||||
.bp-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.bp-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.bp-right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 720px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 60px 60px 60px 40px;
|
||||
}
|
||||
.bp-divider {
|
||||
position: absolute;
|
||||
left: 1200px;
|
||||
top: 60px;
|
||||
bottom: 60px;
|
||||
width: 1px;
|
||||
background: #f7b801;
|
||||
opacity: 0.15;
|
||||
}
|
||||
.bp-number {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4em;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 60px;
|
||||
color: #f7b801;
|
||||
}
|
||||
|
||||
/* Right panel typography */
|
||||
.bp-name {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 38px;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 20px;
|
||||
color: #f7b801;
|
||||
}
|
||||
.bp-plabel {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
color: #f7b801;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.bp-prompt {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
padding: 16px 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
color: #f7b801;
|
||||
background: rgba(247, 184, 1, 0.06);
|
||||
border: 1px solid rgba(247, 184, 1, 0.12);
|
||||
}
|
||||
.bp-desc {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
color: #a0a0c0;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* GL scenes (hidden, just for texture capture) */
|
||||
.scene {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.scene-label {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 140px;
|
||||
letter-spacing: 0.06em;
|
||||
opacity: 0.12;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="bp-grid"></div>
|
||||
<div class="bp-divider"></div>
|
||||
<div class="bp-number">07 / 14</div>
|
||||
|
||||
<!-- GL demo scenes (captured as textures, then hidden) -->
|
||||
<div id="s1" class="scene" style="background: #3d348b">
|
||||
<div class="scene-label" style="color: #f7b801">SCENE A</div>
|
||||
</div>
|
||||
<div id="s2" class="scene" style="background: #f7b801; opacity: 0">
|
||||
<div class="scene-label" style="color: #3d348b">SCENE B</div>
|
||||
</div>
|
||||
|
||||
<!-- GL canvas (left panel only) -->
|
||||
<canvas
|
||||
id="gl-canvas"
|
||||
width="1200"
|
||||
height="1080"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
"
|
||||
>
|
||||
</canvas>
|
||||
|
||||
<!-- Right panel metadata -->
|
||||
<div class="bp-right">
|
||||
<div class="bp-name">Cinematic Zoom</div>
|
||||
<div class="bp-plabel">Prompt</div>
|
||||
<div class="bp-prompt">"use cinematic zoom shader transition"</div>
|
||||
<div class="bp-desc">
|
||||
Both scenes zoom-blur with per-channel radial offset. From zooms outward, to zooms inward
|
||||
from tight.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var glCanvas = document.getElementById("gl-canvas");
|
||||
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
|
||||
if (!gl) {
|
||||
console.warn("WebGL not available");
|
||||
window.__timelines["main"] = gsap.timeline({ paused: true });
|
||||
} else {
|
||||
gl.viewport(0, 0, 1200, 1080);
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
||||
var sceneTextures = {};
|
||||
|
||||
function captureScene(sceneId) {
|
||||
var scene = document.getElementById(sceneId);
|
||||
var origOpacity = scene.style.opacity;
|
||||
var origZ = scene.style.zIndex;
|
||||
scene.style.opacity = "1";
|
||||
scene.style.zIndex = "999";
|
||||
scene.offsetHeight;
|
||||
var c = document.createElement("canvas");
|
||||
c.width = 1200;
|
||||
c.height = 1080;
|
||||
var ctx = c.getContext("2d");
|
||||
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
|
||||
ctx.fillRect(0, 0, 1200, 1080);
|
||||
var sr = scene.getBoundingClientRect();
|
||||
var els = scene.querySelectorAll("*");
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
var el = els[i],
|
||||
cs = window.getComputedStyle(el);
|
||||
if (cs.display === "none" || cs.visibility === "hidden") continue;
|
||||
var r = el.getBoundingClientRect();
|
||||
if (r.width < 1 || r.height < 1) continue;
|
||||
var x = r.left - sr.left,
|
||||
y = r.top - sr.top,
|
||||
w = r.width,
|
||||
h = r.height;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
|
||||
var bg = cs.backgroundColor;
|
||||
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
}
|
||||
var hasChildEls = el.querySelector("div,span,img,video");
|
||||
var text = "";
|
||||
for (var j = 0; j < el.childNodes.length; j++)
|
||||
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
|
||||
text = text.trim();
|
||||
if (text && !hasChildEls) {
|
||||
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
|
||||
ctx.fillStyle = cs.color;
|
||||
if (cs.letterSpacing && cs.letterSpacing !== "normal")
|
||||
ctx.letterSpacing = cs.letterSpacing;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(text, x + w / 2, y + h / 2);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
scene.style.opacity = origOpacity;
|
||||
scene.style.zIndex = origZ;
|
||||
var tex = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
|
||||
sceneTextures[sceneId] = tex;
|
||||
}
|
||||
|
||||
var vertSrc =
|
||||
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
|
||||
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
|
||||
var quadBuf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.bufferData(
|
||||
gl.ARRAY_BUFFER,
|
||||
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
||||
gl.STATIC_DRAW,
|
||||
);
|
||||
function compileShader(src, type) {
|
||||
var s = gl.createShader(type);
|
||||
gl.shaderSource(s, src);
|
||||
gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
|
||||
console.error("Shader:", gl.getShaderInfoLog(s));
|
||||
return s;
|
||||
}
|
||||
function mkProg(fragSrc) {
|
||||
var p = gl.createProgram();
|
||||
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
|
||||
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
|
||||
gl.linkProgram(p);
|
||||
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
|
||||
console.error("Link:", gl.getProgramInfoLog(p));
|
||||
return p;
|
||||
}
|
||||
|
||||
var H =
|
||||
"precision mediump float;" +
|
||||
"varying vec2 v_uv;" +
|
||||
"uniform sampler2D u_from, u_to;" +
|
||||
"uniform float u_progress;" +
|
||||
"uniform vec2 u_resolution;\n";
|
||||
|
||||
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
|
||||
var progTrans = mkProg(
|
||||
H +
|
||||
"void main(){vec2 d=v_uv-vec2(.5);float fromS=u_progress*.08;float toS=(1.-u_progress)*.06;float fr=0.,fg=0.,fb=0.;for(int i=0;i<12;i++){float f=float(i)/12.;fr+=texture2D(u_from,v_uv-d*(fromS*1.06)*f).r;fg+=texture2D(u_from,v_uv-d*fromS*f).g;fb+=texture2D(u_from,v_uv-d*(fromS*.94)*f).b;}vec3 fromBl=vec3(fr,fg,fb)/12.;float tr=0.,tg=0.,tb=0.;for(int i=0;i<12;i++){float f=float(i)/12.;tr+=texture2D(u_to,v_uv+d*(toS*1.06)*f).r;tg+=texture2D(u_to,v_uv+d*toS*f).g;tb+=texture2D(u_to,v_uv+d*(toS*.94)*f).b;}vec3 toBl=vec3(tr,tg,tb)/12.;gl_FragColor=vec4(mix(fromBl,toBl,u_progress),1.);}",
|
||||
);
|
||||
|
||||
function renderShader(prog, texFrom, texTo, progress) {
|
||||
gl.useProgram(prog);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texFrom);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texTo);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
|
||||
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
|
||||
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
|
||||
var pos = gl.getAttribLocation(prog, "a_pos");
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.enableVertexAttribArray(pos);
|
||||
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
function easeInOut(p) {
|
||||
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
|
||||
}
|
||||
|
||||
captureScene("s1");
|
||||
captureScene("s2");
|
||||
glCanvas.style.display = "block";
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
document.querySelectorAll(".scene").forEach(function (s) {
|
||||
s.style.opacity = "0";
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({
|
||||
paused: true,
|
||||
onUpdate: function () {
|
||||
var t = tl.time();
|
||||
if (t >= 1.0 && t < 3.0) {
|
||||
renderShader(
|
||||
progTrans,
|
||||
sceneTextures["s1"],
|
||||
sceneTextures["s2"],
|
||||
easeInOut((t - 1.0) / 2.0),
|
||||
);
|
||||
} else if (t >= 3.0) {
|
||||
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
|
||||
} else {
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
|
||||
window.__timelines["main"] = tl;
|
||||
} // end if (gl)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="cinematic-zoom" data-composition-src="compositions/cinematic-zoom.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,645 @@ title: "Code 3D Extrude"
|
||||
description: "Syntax-highlighted code on a lit, beveled 3D slab that rotates through real space and settles to a readable rest — true WebGL depth and lighting, not a 2D transform."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-3d-extrude.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-3d-extrude.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-3d-extrude.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code 3D Extrude preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-3d-extrude
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-3d-extrude" />
|
||||
|
||||
That writes one file: `compositions/code-3d-extrude.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-3d-extrude.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Code 3D Extrude</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/build/three.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;
|
||||
}
|
||||
#gl {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="code-3d-extrude"
|
||||
data-composition-src="compositions/code-3d-extrude.html"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<canvas id="gl" width="1920" height="1080"></canvas>
|
||||
</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-3d-extrude",
|
||||
effect: "extrude",
|
||||
seq: "feature",
|
||||
duration: 8,
|
||||
seed: 7,
|
||||
};
|
||||
</script>
|
||||
<script>
|
||||
// hf-code-anim-gl.js — WebGL code-animation engine for the "crazy" Code Animations blocks.
|
||||
//
|
||||
// Bridges the baked Shiki tokens (window.__TOKENS) into Three.js: the tokens are drawn once
|
||||
// to an offscreen 2D canvas (a CanvasTexture), which then drives a lit 3D slab, a dissolve
|
||||
// shader, or a GPU particle system. Determinism per the project contract:
|
||||
// - render only via tl.eventCallback("onUpdate", …) + one initial render() — never rAF
|
||||
// - all randomness from a seeded mulberry32 — never Math.random / Date.now
|
||||
// - canvas drawn once after document.fonts.ready (final glyph metrics)
|
||||
// - paused GSAP timeline registered on window.__timelines[id]; HF seeks it per frame
|
||||
//
|
||||
// Each block sets window.__BLOCK = { id, effect, seq, duration, seed } and includes this engine.
|
||||
(function () {
|
||||
"use strict";
|
||||
var T = window.__TOKENS || {};
|
||||
|
||||
// ---- seeded PRNG (no Math.random) -----------------------------------------
|
||||
function mulberry32(a) {
|
||||
return function () {
|
||||
a |= 0;
|
||||
a = (a + 0x6d2b79f5) | 0;
|
||||
var t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
// ---- split a token state into lines of {content,color} --------------------
|
||||
function splitLines(state) {
|
||||
var lines = [[]];
|
||||
for (var i = 0; i < state.tokens.length; i++) {
|
||||
var t = state.tokens[i];
|
||||
if (t.content === "\n") {
|
||||
lines.push([]);
|
||||
continue;
|
||||
}
|
||||
lines[lines.length - 1].push(t);
|
||||
}
|
||||
while (lines.length && lines[lines.length - 1].length === 0) lines.pop();
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ---- THE BRIDGE: draw baked tokens to an offscreen 2D canvas ---------------
|
||||
// Returns { canvas, W, H, lines, lineH, padY, fontPx } so effects can sample it.
|
||||
function drawCodeToCanvas(state, opts) {
|
||||
opts = opts || {};
|
||||
var fontPx = opts.fontPx || 36;
|
||||
var lineH = opts.lineH || 54;
|
||||
var padX = opts.padX || 70;
|
||||
var padY = opts.padY || 60;
|
||||
var weight = opts.weight || 500;
|
||||
var font = weight + " " + fontPx + 'px "JetBrains Mono", monospace';
|
||||
|
||||
var lines = splitLines(state);
|
||||
var measure = document.createElement("canvas").getContext("2d");
|
||||
measure.font = font;
|
||||
var maxW = 0;
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var w = 0;
|
||||
for (var j = 0; j < lines[i].length; j++)
|
||||
w += measure.measureText(lines[i][j].content).width;
|
||||
if (w > maxW) maxW = w;
|
||||
}
|
||||
var W = Math.max(2, Math.ceil(maxW + padX * 2));
|
||||
var H = Math.max(2, Math.ceil(lines.length * lineH + padY * 2));
|
||||
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.width = W;
|
||||
canvas.height = H;
|
||||
var ctx = canvas.getContext("2d");
|
||||
if (opts.transparent) {
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
} else {
|
||||
ctx.fillStyle = opts.bg || "#0b0f17";
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
}
|
||||
ctx.textBaseline = "top";
|
||||
ctx.font = font;
|
||||
for (var li = 0; li < lines.length; li++) {
|
||||
var x = padX;
|
||||
var y = padY + li * lineH;
|
||||
for (var k = 0; k < lines[li].length; k++) {
|
||||
var tok = lines[li][k];
|
||||
ctx.fillStyle = tok.color;
|
||||
ctx.fillText(tok.content, x, y);
|
||||
x += ctx.measureText(tok.content).width;
|
||||
}
|
||||
}
|
||||
return {
|
||||
canvas: canvas,
|
||||
W: W,
|
||||
H: H,
|
||||
lines: lines.length,
|
||||
lineH: lineH,
|
||||
padY: padY,
|
||||
fontPx: fontPx,
|
||||
};
|
||||
}
|
||||
|
||||
function fxExtrude(THREE, scene, camera, renderer, state, st, tl, dur) {
|
||||
scene.background = new THREE.Color(0x05070b);
|
||||
var code = drawCodeToCanvas(state, {
|
||||
fontPx: 38,
|
||||
lineH: 56,
|
||||
padX: 80,
|
||||
padY: 70,
|
||||
weight: 600,
|
||||
});
|
||||
var tex = new THREE.CanvasTexture(code.canvas);
|
||||
tex.minFilter = THREE.LinearFilter;
|
||||
tex.magFilter = THREE.LinearFilter;
|
||||
tex.generateMipmaps = false;
|
||||
|
||||
var aspect = code.W / code.H;
|
||||
var slabH = 6.2;
|
||||
var slabW = slabH * aspect;
|
||||
var depth = 0.5;
|
||||
|
||||
// beveled slab: rounded extruded rectangle
|
||||
var hw = slabW / 2;
|
||||
var hh = slabH / 2;
|
||||
var r = 0.18;
|
||||
var shape = new THREE.Shape();
|
||||
shape.moveTo(-hw + r, -hh);
|
||||
shape.lineTo(hw - r, -hh);
|
||||
shape.quadraticCurveTo(hw, -hh, hw, -hh + r);
|
||||
shape.lineTo(hw, hh - r);
|
||||
shape.quadraticCurveTo(hw, hh, hw - r, hh);
|
||||
shape.lineTo(-hw + r, hh);
|
||||
shape.quadraticCurveTo(-hw, hh, -hw, hh - r);
|
||||
shape.lineTo(-hw, -hh + r);
|
||||
shape.quadraticCurveTo(-hw, -hh, -hw + r, -hh);
|
||||
var geo = new THREE.ExtrudeGeometry(shape, {
|
||||
depth: depth,
|
||||
bevelEnabled: true,
|
||||
bevelThickness: 0.12,
|
||||
bevelSize: 0.1,
|
||||
bevelSegments: 3,
|
||||
steps: 1,
|
||||
});
|
||||
geo.center();
|
||||
// UVs from ExtrudeGeometry don't map the texture nicely to the face; remap front-face UVs from XY bounds.
|
||||
geo.computeBoundingBox();
|
||||
var bb = geo.boundingBox;
|
||||
var sizeX = bb.max.x - bb.min.x;
|
||||
var sizeY = bb.max.y - bb.min.y;
|
||||
var pos = geo.attributes.position;
|
||||
var uv = geo.attributes.uv;
|
||||
for (var vi = 0; vi < pos.count; vi++) {
|
||||
uv.setXY(vi, (pos.getX(vi) - bb.min.x) / sizeX, (pos.getY(vi) - bb.min.y) / sizeY);
|
||||
}
|
||||
uv.needsUpdate = true;
|
||||
|
||||
// Face is an unlit "screen" — shows the exact vivid Shiki colors (no PBR/emissive wash).
|
||||
// The 3D read comes from the lit, beveled EDGES + perspective, like a glowing monitor.
|
||||
var faceMat = new THREE.MeshBasicMaterial({ map: tex });
|
||||
var edgeMat = new THREE.MeshStandardMaterial({
|
||||
color: 0x141d2b,
|
||||
roughness: 0.3,
|
||||
metalness: 0.4,
|
||||
});
|
||||
// ExtrudeGeometry groups: 0 = front+back faces, 1 = sides. Front shows the code.
|
||||
var slab = new THREE.Mesh(geo, [faceMat, edgeMat]);
|
||||
scene.add(slab);
|
||||
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
|
||||
var key = new THREE.DirectionalLight(0xbcd4ff, 1.25);
|
||||
key.position.set(5, 7, 9);
|
||||
scene.add(key);
|
||||
var rim = new THREE.DirectionalLight(0x4fffa0, 0.45);
|
||||
rim.position.set(-7, -3, 5);
|
||||
scene.add(rim);
|
||||
|
||||
camera.position.set(0, 0, 10);
|
||||
|
||||
// fit camera distance so the WHOLE slab fits (width-limited for wide code), ~78% fill
|
||||
var vfov = (camera.fov * Math.PI) / 180;
|
||||
var screenAspect = 1920 / 1080;
|
||||
var fitH = slabH / 2 / Math.tan(vfov / 2) / 0.78;
|
||||
var fitW = slabW / 2 / (Math.tan(vfov / 2) * screenAspect) / 0.78;
|
||||
var fitZ = Math.max(fitH, fitW);
|
||||
|
||||
st.rotY = -1.0;
|
||||
st.rotX = 0.4;
|
||||
st.camZ = fitZ + 6;
|
||||
tl.to(st, { rotY: 0, rotX: 0, camZ: fitZ, duration: dur * 0.6, ease: "power3.out" }, 0);
|
||||
// gentle breathing drift at rest (subtle — stays readable)
|
||||
tl.to(
|
||||
st,
|
||||
{ rotY: 0.06, rotX: -0.02, duration: dur * 0.4, ease: "sine.inOut" },
|
||||
dur * 0.6,
|
||||
);
|
||||
|
||||
return function () {
|
||||
slab.rotation.y = st.rotY;
|
||||
slab.rotation.x = st.rotX;
|
||||
camera.position.z = st.camZ;
|
||||
camera.lookAt(0, 0, 0);
|
||||
// boot loop's render() issues the single renderer.render() — don't double-render here
|
||||
};
|
||||
}
|
||||
|
||||
// ---- dispatch + boot ------------------------------------------------------
|
||||
function buildEffect(THREE, scene, camera, renderer, spec, st, tl) {
|
||||
var state = T[spec.seq].states[0];
|
||||
var dur = spec.duration;
|
||||
if (spec.effect === "extrude")
|
||||
return fxExtrude(THREE, scene, camera, renderer, state, st, tl, dur);
|
||||
return function () {};
|
||||
}
|
||||
|
||||
// Free GPU resources (renderer, geometries, materials, textures) on page teardown.
|
||||
// One-shot renders exit per job so nothing leaks there; this guards long-lived
|
||||
// Studio/player sessions that reload. Fires only on unload — never during capture.
|
||||
function disposeGL(renderer, scene) {
|
||||
scene.traverse(function (o) {
|
||||
if (o.geometry && o.geometry.dispose) o.geometry.dispose();
|
||||
var mats = o.material ? (Array.isArray(o.material) ? o.material : [o.material]) : [];
|
||||
mats.forEach(function (m) {
|
||||
if (m.map && m.map.dispose) m.map.dispose();
|
||||
if (
|
||||
m.uniforms &&
|
||||
m.uniforms.uTex &&
|
||||
m.uniforms.uTex.value &&
|
||||
m.uniforms.uTex.value.dispose
|
||||
) {
|
||||
m.uniforms.uTex.value.dispose();
|
||||
}
|
||||
if (m.dispose) m.dispose();
|
||||
});
|
||||
});
|
||||
if (renderer.dispose) renderer.dispose();
|
||||
}
|
||||
|
||||
function go() {
|
||||
var spec = window.__BLOCK;
|
||||
var root = document.getElementById("root");
|
||||
var glcanvas = document.getElementById("gl");
|
||||
var W = 1920;
|
||||
var H = 1080;
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
window.__timelines[spec.id] = tl; // register synchronously
|
||||
|
||||
var THREE = window.THREE;
|
||||
var renderer = new THREE.WebGLRenderer({
|
||||
canvas: glcanvas,
|
||||
antialias: true,
|
||||
alpha: false,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.setSize(W, H, false);
|
||||
var scene = new THREE.Scene();
|
||||
var camera = new THREE.PerspectiveCamera(45, W / H, 0.1, 200);
|
||||
var st = {};
|
||||
|
||||
document.fonts.ready.then(function () {
|
||||
var renderFn = buildEffect(THREE, scene, camera, renderer, spec, st, tl);
|
||||
function render() {
|
||||
renderFn();
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
tl.eventCallback("onUpdate", render);
|
||||
var dur = parseFloat(root.dataset.duration) || tl.duration();
|
||||
tl.to({}, { duration: dur }, 0); // pad to full composition length
|
||||
render(); // paint frame 0
|
||||
window.addEventListener("beforeunload", function () {
|
||||
disposeGL(renderer, scene);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", go);
|
||||
else go();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-3d-extrude" data-composition-src="compositions/code-3d-extrude.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,848 @@ title: "Code Diff"
|
||||
description: "An edit shown as a colored diff — removed lines collapse in red, added lines expand in green."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-diff.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-diff.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-diff.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Diff preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-diff
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-diff" />
|
||||
|
||||
That writes one file: `compositions/code-diff.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 6 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-diff.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Code Diff</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-diff"
|
||||
data-composition-src="compositions/code-diff.html"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<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">greet.js</span> — Code Diff</span>
|
||||
</div>
|
||||
<div class="code-surface" id="surface"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__TOKENS = {
|
||||
diff: {
|
||||
lang: "js",
|
||||
theme: "github-dark",
|
||||
bg: "#24292e",
|
||||
fg: "#e1e4e8",
|
||||
states: [
|
||||
{
|
||||
code: 'function greet(name) {\n console.log("hi " + name)\n}',
|
||||
tokens: [
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-0",
|
||||
content: "function",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-1",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-2",
|
||||
content: "greet",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-3",
|
||||
content: "(",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-4",
|
||||
content: "name",
|
||||
color: "#FFAB70",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-5",
|
||||
content: ") {",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-6",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-7",
|
||||
content: " ",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-8",
|
||||
content: "console.",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-9",
|
||||
content: "log",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-10",
|
||||
content: "(",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-11",
|
||||
content: '"hi "',
|
||||
color: "#9ECBFF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-12",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-13",
|
||||
content: "+",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-14",
|
||||
content: " ",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-15",
|
||||
content: "name)",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-16",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-17",
|
||||
content: "}",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-18",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'function greet(name, lang) {\n const msg = translate("hi", lang)\n console.log(`${msg} ${name}`)\n}',
|
||||
tokens: [
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-0",
|
||||
content: "function",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-1",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-2",
|
||||
content: "greet",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-3",
|
||||
content: "(",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-4",
|
||||
content: "name",
|
||||
color: "#FFAB70",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-5",
|
||||
content: ",",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-6",
|
||||
content: " ",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-7",
|
||||
content: "lang",
|
||||
color: "#FFAB70",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-5",
|
||||
content: ") {",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-6",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-7",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-11",
|
||||
content: "const",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-12",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-13",
|
||||
content: "msg",
|
||||
color: "#79B8FF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-14",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-15",
|
||||
content: "=",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-16",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-17",
|
||||
content: "translate",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-10",
|
||||
content: "(",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-19",
|
||||
content: '"hi"',
|
||||
color: "#9ECBFF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-20",
|
||||
content: ", lang)",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-21",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-22",
|
||||
content: " ",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-23",
|
||||
content: "console.",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-9",
|
||||
content: "log",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-25",
|
||||
content: "(",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-26",
|
||||
content: "`${",
|
||||
color: "#9ECBFF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-27",
|
||||
content: "msg",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-28",
|
||||
content: "} ${",
|
||||
color: "#9ECBFF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-29",
|
||||
content: "name",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-30",
|
||||
content: "}`",
|
||||
color: "#9ECBFF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-31",
|
||||
content: ")",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-16",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "97wperY-uI8_OA5W5UX94Q5keKEoIobaKXQ6V8Tcd5I-17",
|
||||
content: "}",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "w2rmoAdPXgC70S4pDXgHIIwz8Jqq0QKYf1B6jvTMUJ8-34",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
window.__BLOCK = { id: "code-diff", effect: "diff", seq: "diff", duration: 6 };
|
||||
</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 diff(tl, surface, states, start) {
|
||||
var DEL = 0.55;
|
||||
var ADD = 0.6;
|
||||
var ops = lineDiff(splitLines(states[0]), splitLines(states[1]));
|
||||
var parts = makeScene(surface, false);
|
||||
var code = parts.code;
|
||||
var delEls = [];
|
||||
var addEls = [];
|
||||
ops.forEach(function (op) {
|
||||
var ln = document.createElement("div");
|
||||
ln.className =
|
||||
"diff-line" +
|
||||
(op.type === "del" ? " diff-del" : op.type === "add" ? " diff-add" : "");
|
||||
var sign = document.createElement("span");
|
||||
sign.className = "diff-sign";
|
||||
sign.textContent = op.type === "del" ? "- " : op.type === "add" ? "+ " : " ";
|
||||
ln.appendChild(sign);
|
||||
op.line.tokens.forEach(function (t) {
|
||||
var s = document.createElement("span");
|
||||
s.className = "tok";
|
||||
s.textContent = t.content;
|
||||
s.style.color = t.color;
|
||||
ln.appendChild(s);
|
||||
});
|
||||
code.appendChild(ln);
|
||||
if (op.type === "del") delEls.push(ln);
|
||||
if (op.type === "add") addEls.push(ln);
|
||||
});
|
||||
tl.set(parts.scene, { opacity: 0 }, 0);
|
||||
tl.to(parts.scene, { opacity: 1, duration: FADE, ease: "power1.out" }, start);
|
||||
var atDel = start + FADE + 0.3;
|
||||
delEls.forEach(function (el, k) {
|
||||
tl.fromTo(
|
||||
el,
|
||||
{ height: 46, opacity: 1 },
|
||||
{ height: 0, opacity: 0, duration: DEL, ease: "power2.inOut" },
|
||||
atDel + k * 0.08,
|
||||
);
|
||||
});
|
||||
var atAdd = atDel + DEL + 0.15;
|
||||
addEls.forEach(function (el, k) {
|
||||
tl.set(el, { height: 0, opacity: 0 }, 0);
|
||||
tl.fromTo(
|
||||
el,
|
||||
{ height: 0, opacity: 0 },
|
||||
{ height: 46, opacity: 1, duration: ADD, ease: "power2.out" },
|
||||
atAdd + k * 0.12,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function buildEffect(tl, surface, spec) {
|
||||
var start = 0.45;
|
||||
var seq = T[spec.seq];
|
||||
if (spec.effect === "diff") diff(tl, surface, seq.states, start);
|
||||
}
|
||||
|
||||
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-diff"] = 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>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-diff" data-composition-src="compositions/code-diff.html" data-start="0" data-duration="6" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -1,36 +1,779 @@
|
||||
---
|
||||
title: "Code Highlight Sweep"
|
||||
description: "A highlight band sweeps across a target line while the surrounding context dims — draws the eye to one line. `line` is 0-based: `line: 1` targets the second displayed line (unlike code-scroll, whose target is 1-based)."
|
||||
description: "A highlight band sweeps across a target line while the surrounding context dims — draws the eye to one line."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-highlight.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-highlight.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-highlight.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Highlight Sweep preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-highlight
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-highlight" />
|
||||
|
||||
That writes one file: `compositions/code-highlight.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 5 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-highlight.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```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-composition-src="compositions/code-highlight.html"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</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 */}
|
||||
|
||||
|
||||
+1227
-13
File diff suppressed because it is too large
Load Diff
@@ -3,34 +3,511 @@ title: "Code Particle Assemble"
|
||||
description: "Thousands of GPU points scatter through space and fly to the exact glyph pixels of the code, resolving into readable syntax-highlighted text — a particle system, not a token tween."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-particle-assemble.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-particle-assemble.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-particle-assemble.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Particle Assemble preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-particle-assemble
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-particle-assemble" />
|
||||
|
||||
That writes one file: `compositions/code-particle-assemble.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-particle-assemble.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Code Particle Assemble</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/build/three.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;
|
||||
}
|
||||
#gl {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="code-particle-assemble"
|
||||
data-composition-src="compositions/code-particle-assemble.html"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<canvas id="gl" width="1920" height="1080"></canvas>
|
||||
</div>
|
||||
<script>
|
||||
window.__TOKENS = {
|
||||
assemble: {
|
||||
lang: "js",
|
||||
theme: "github-dark",
|
||||
bg: "#24292e",
|
||||
fg: "#e1e4e8",
|
||||
states: [
|
||||
{
|
||||
code: "const app = pipe(\n parse,\n optimize,\n emit,\n)",
|
||||
tokens: [
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-0",
|
||||
content: "const",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-1",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-2",
|
||||
content: "app",
|
||||
color: "#79B8FF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-3",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-4",
|
||||
content: "=",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-5",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-6",
|
||||
content: "pipe",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-7",
|
||||
content: "(",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-8",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-9",
|
||||
content: " ",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-10",
|
||||
content: "parse,",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-11",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-12",
|
||||
content: " ",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-13",
|
||||
content: "optimize,",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-14",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-15",
|
||||
content: " ",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-16",
|
||||
content: "emit,",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-17",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-18",
|
||||
content: ")",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "AFffx9qQBGKnMZ-vDjoaRwC68xQloGJNUSICY1myCK4-19",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
window.__BLOCK = {
|
||||
id: "code-particle-assemble",
|
||||
effect: "particles",
|
||||
seq: "assemble",
|
||||
duration: 8,
|
||||
seed: 23,
|
||||
};
|
||||
</script>
|
||||
<script>
|
||||
// hf-code-anim-gl.js — WebGL code-animation engine for the "crazy" Code Animations blocks.
|
||||
//
|
||||
// Bridges the baked Shiki tokens (window.__TOKENS) into Three.js: the tokens are drawn once
|
||||
// to an offscreen 2D canvas (a CanvasTexture), which then drives a lit 3D slab, a dissolve
|
||||
// shader, or a GPU particle system. Determinism per the project contract:
|
||||
// - render only via tl.eventCallback("onUpdate", …) + one initial render() — never rAF
|
||||
// - all randomness from a seeded mulberry32 — never Math.random / Date.now
|
||||
// - canvas drawn once after document.fonts.ready (final glyph metrics)
|
||||
// - paused GSAP timeline registered on window.__timelines[id]; HF seeks it per frame
|
||||
//
|
||||
// Each block sets window.__BLOCK = { id, effect, seq, duration, seed } and includes this engine.
|
||||
(function () {
|
||||
"use strict";
|
||||
var T = window.__TOKENS || {};
|
||||
|
||||
// ---- seeded PRNG (no Math.random) -----------------------------------------
|
||||
function mulberry32(a) {
|
||||
return function () {
|
||||
a |= 0;
|
||||
a = (a + 0x6d2b79f5) | 0;
|
||||
var t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
// ---- split a token state into lines of {content,color} --------------------
|
||||
function splitLines(state) {
|
||||
var lines = [[]];
|
||||
for (var i = 0; i < state.tokens.length; i++) {
|
||||
var t = state.tokens[i];
|
||||
if (t.content === "\n") {
|
||||
lines.push([]);
|
||||
continue;
|
||||
}
|
||||
lines[lines.length - 1].push(t);
|
||||
}
|
||||
while (lines.length && lines[lines.length - 1].length === 0) lines.pop();
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ---- THE BRIDGE: draw baked tokens to an offscreen 2D canvas ---------------
|
||||
// Returns { canvas, W, H, lines, lineH, padY, fontPx } so effects can sample it.
|
||||
function drawCodeToCanvas(state, opts) {
|
||||
opts = opts || {};
|
||||
var fontPx = opts.fontPx || 36;
|
||||
var lineH = opts.lineH || 54;
|
||||
var padX = opts.padX || 70;
|
||||
var padY = opts.padY || 60;
|
||||
var weight = opts.weight || 500;
|
||||
var font = weight + " " + fontPx + 'px "JetBrains Mono", monospace';
|
||||
|
||||
var lines = splitLines(state);
|
||||
var measure = document.createElement("canvas").getContext("2d");
|
||||
measure.font = font;
|
||||
var maxW = 0;
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var w = 0;
|
||||
for (var j = 0; j < lines[i].length; j++)
|
||||
w += measure.measureText(lines[i][j].content).width;
|
||||
if (w > maxW) maxW = w;
|
||||
}
|
||||
var W = Math.max(2, Math.ceil(maxW + padX * 2));
|
||||
var H = Math.max(2, Math.ceil(lines.length * lineH + padY * 2));
|
||||
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.width = W;
|
||||
canvas.height = H;
|
||||
var ctx = canvas.getContext("2d");
|
||||
if (opts.transparent) {
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
} else {
|
||||
ctx.fillStyle = opts.bg || "#0b0f17";
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
}
|
||||
ctx.textBaseline = "top";
|
||||
ctx.font = font;
|
||||
for (var li = 0; li < lines.length; li++) {
|
||||
var x = padX;
|
||||
var y = padY + li * lineH;
|
||||
for (var k = 0; k < lines[li].length; k++) {
|
||||
var tok = lines[li][k];
|
||||
ctx.fillStyle = tok.color;
|
||||
ctx.fillText(tok.content, x, y);
|
||||
x += ctx.measureText(tok.content).width;
|
||||
}
|
||||
}
|
||||
return {
|
||||
canvas: canvas,
|
||||
W: W,
|
||||
H: H,
|
||||
lines: lines.length,
|
||||
lineH: lineH,
|
||||
padY: padY,
|
||||
fontPx: fontPx,
|
||||
};
|
||||
}
|
||||
|
||||
function fxParticles(THREE, scene, camera, renderer, state, st, tl, dur, seed) {
|
||||
scene.background = new THREE.Color(0x05070b);
|
||||
var code = drawCodeToCanvas(state, {
|
||||
fontPx: 46,
|
||||
lineH: 70,
|
||||
padX: 70,
|
||||
padY: 64,
|
||||
weight: 700,
|
||||
});
|
||||
var cw = code.W;
|
||||
var ch = code.H;
|
||||
var data = code.canvas.getContext("2d").getImageData(0, 0, cw, ch).data;
|
||||
var rng = mulberry32(((seed || 1) * 2654435761) >>> 0);
|
||||
|
||||
var worldW = 13.0;
|
||||
var worldH = worldW * (ch / cw);
|
||||
|
||||
// sample glyph pixels (brighter than the dark bg) on a fixed grid
|
||||
var step = 2;
|
||||
var tx = [];
|
||||
var col = [];
|
||||
for (var y = 0; y < ch; y += step) {
|
||||
for (var x = 0; x < cw; x += step) {
|
||||
var idx = (y * cw + x) * 4;
|
||||
var r = data[idx];
|
||||
var g = data[idx + 1];
|
||||
var b = data[idx + 2];
|
||||
if (r + g + b < 120) continue; // skip background
|
||||
tx.push((x / cw - 0.5) * worldW, -(y / ch - 0.5) * worldH, 0);
|
||||
col.push(r / 255, g / 255, b / 255);
|
||||
}
|
||||
}
|
||||
var n = tx.length / 3;
|
||||
|
||||
var positions = new Float32Array(n * 3);
|
||||
var aOrigin = new Float32Array(n * 3);
|
||||
var aTarget = new Float32Array(tx);
|
||||
var aColor = new Float32Array(col);
|
||||
var aDelay = new Float32Array(n);
|
||||
for (var i = 0; i < n; i++) {
|
||||
var sx = (rng() * 2 - 1) * worldW * 1.15;
|
||||
var sy = (rng() * 2 - 1) * worldH * 1.7;
|
||||
var sz = (rng() * 2 - 1) * 9.0 - 2.0;
|
||||
aOrigin[i * 3] = sx;
|
||||
aOrigin[i * 3 + 1] = sy;
|
||||
aOrigin[i * 3 + 2] = sz;
|
||||
positions[i * 3] = sx;
|
||||
positions[i * 3 + 1] = sy;
|
||||
positions[i * 3 + 2] = sz;
|
||||
aDelay[i] = rng();
|
||||
}
|
||||
|
||||
var geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
||||
geo.setAttribute("aOrigin", new THREE.BufferAttribute(aOrigin, 3));
|
||||
geo.setAttribute("aTarget", new THREE.BufferAttribute(aTarget, 3));
|
||||
geo.setAttribute("aColor", new THREE.BufferAttribute(aColor, 3));
|
||||
geo.setAttribute("aDelay", new THREE.BufferAttribute(aDelay, 1));
|
||||
|
||||
var uniforms = { uProgress: { value: 0 }, uSize: { value: 3.4 } };
|
||||
var mat = new THREE.ShaderMaterial({
|
||||
uniforms: uniforms,
|
||||
transparent: true,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
blending: THREE.NormalBlending,
|
||||
vertexShader: [
|
||||
"attribute vec3 aOrigin; attribute vec3 aTarget; attribute vec3 aColor; attribute float aDelay;",
|
||||
"uniform float uProgress; uniform float uSize;",
|
||||
"varying vec3 vColor; varying float vP;",
|
||||
"void main(){",
|
||||
" float span = 0.62;",
|
||||
" float p = clamp((uProgress - aDelay*(1.0-span)) / span, 0.0, 1.0);",
|
||||
" p = p*p*(3.0-2.0*p);",
|
||||
" vec3 pos = mix(aOrigin, aTarget, p);",
|
||||
" vec4 mv = modelViewMatrix * vec4(pos, 1.0);",
|
||||
" gl_Position = projectionMatrix * mv;",
|
||||
" gl_PointSize = uSize * (60.0 / -mv.z) * (0.45 + 0.55*p);",
|
||||
" vColor = aColor; vP = p;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
fragmentShader: [
|
||||
"precision highp float;",
|
||||
"varying vec3 vColor; varying float vP;",
|
||||
"void main(){",
|
||||
" vec2 d = gl_PointCoord - 0.5;",
|
||||
" float r2 = dot(d, d);",
|
||||
" if (r2 > 0.25) discard;",
|
||||
" float a = smoothstep(0.25, 0.02, r2);",
|
||||
// brighter & whiter in flight, resolves to true color as it lands
|
||||
" vec3 c = mix(vColor + vec3(0.32)*(1.0-vP), vColor, vP);",
|
||||
" gl_FragColor = vec4(c, a * (0.7 + 0.3*vP));",
|
||||
"}",
|
||||
].join("\n"),
|
||||
});
|
||||
|
||||
var pts = new THREE.Points(geo, mat);
|
||||
scene.add(pts);
|
||||
|
||||
camera.position.set(0, 0, 10);
|
||||
var vfov = (camera.fov * Math.PI) / 180;
|
||||
var sa = 1920 / 1080;
|
||||
var fitH = worldH / 2 / Math.tan(vfov / 2) / 0.78;
|
||||
var fitW = worldW / 2 / (Math.tan(vfov / 2) * sa) / 0.78;
|
||||
camera.position.z = Math.max(fitH, fitW);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
tl.to(uniforms.uProgress, { value: 1.0, duration: dur * 0.72, ease: "power2.out" }, 0);
|
||||
|
||||
// uProgress is tweened by GSAP; the boot loop's render() draws once per frame.
|
||||
return function () {};
|
||||
}
|
||||
|
||||
// ---- dispatch + boot ------------------------------------------------------
|
||||
function buildEffect(THREE, scene, camera, renderer, spec, st, tl) {
|
||||
var state = T[spec.seq].states[0];
|
||||
var dur = spec.duration;
|
||||
if (spec.effect === "particles")
|
||||
return fxParticles(THREE, scene, camera, renderer, state, st, tl, dur, spec.seed);
|
||||
return function () {};
|
||||
}
|
||||
|
||||
// Free GPU resources (renderer, geometries, materials, textures) on page teardown.
|
||||
// One-shot renders exit per job so nothing leaks there; this guards long-lived
|
||||
// Studio/player sessions that reload. Fires only on unload — never during capture.
|
||||
function disposeGL(renderer, scene) {
|
||||
scene.traverse(function (o) {
|
||||
if (o.geometry && o.geometry.dispose) o.geometry.dispose();
|
||||
var mats = o.material ? (Array.isArray(o.material) ? o.material : [o.material]) : [];
|
||||
mats.forEach(function (m) {
|
||||
if (m.map && m.map.dispose) m.map.dispose();
|
||||
if (
|
||||
m.uniforms &&
|
||||
m.uniforms.uTex &&
|
||||
m.uniforms.uTex.value &&
|
||||
m.uniforms.uTex.value.dispose
|
||||
) {
|
||||
m.uniforms.uTex.value.dispose();
|
||||
}
|
||||
if (m.dispose) m.dispose();
|
||||
});
|
||||
});
|
||||
if (renderer.dispose) renderer.dispose();
|
||||
}
|
||||
|
||||
function go() {
|
||||
var spec = window.__BLOCK;
|
||||
var root = document.getElementById("root");
|
||||
var glcanvas = document.getElementById("gl");
|
||||
var W = 1920;
|
||||
var H = 1080;
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
window.__timelines[spec.id] = tl; // register synchronously
|
||||
|
||||
var THREE = window.THREE;
|
||||
var renderer = new THREE.WebGLRenderer({
|
||||
canvas: glcanvas,
|
||||
antialias: true,
|
||||
alpha: false,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.setSize(W, H, false);
|
||||
var scene = new THREE.Scene();
|
||||
var camera = new THREE.PerspectiveCamera(45, W / H, 0.1, 200);
|
||||
var st = {};
|
||||
|
||||
document.fonts.ready.then(function () {
|
||||
var renderFn = buildEffect(THREE, scene, camera, renderer, spec, st, tl);
|
||||
function render() {
|
||||
renderFn();
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
tl.eventCallback("onUpdate", render);
|
||||
var dur = parseFloat(root.dataset.duration) || tl.duration();
|
||||
tl.to({}, { duration: dur }, 0); // pad to full composition length
|
||||
render(); // paint frame 0
|
||||
window.addEventListener("beforeunload", function () {
|
||||
disposeGL(renderer, scene);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", go);
|
||||
else go();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-particle-assemble" data-composition-src="compositions/code-particle-assemble.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
+1571
-13
File diff suppressed because it is too large
Load Diff
@@ -3,34 +3,567 @@ title: "Code Shader Dissolve"
|
||||
description: "The code compiles into existence: a GPU fragment shader resolves it out of seeded noise with a chromatic dissolve front and edge glow, then holds crisp."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-shader-dissolve.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-shader-dissolve.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-shader-dissolve.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Shader Dissolve preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-shader-dissolve
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-shader-dissolve" />
|
||||
|
||||
That writes one file: `compositions/code-shader-dissolve.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 7 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-shader-dissolve.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Code Shader Dissolve</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/build/three.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;
|
||||
}
|
||||
#gl {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="code-shader-dissolve"
|
||||
data-composition-src="compositions/code-shader-dissolve.html"
|
||||
data-start="0"
|
||||
data-duration="7"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<canvas id="gl" width="1920" height="1080"></canvas>
|
||||
</div>
|
||||
<script>
|
||||
window.__TOKENS = {
|
||||
compile: {
|
||||
lang: "js",
|
||||
theme: "github-dark",
|
||||
bg: "#24292e",
|
||||
fg: "#e1e4e8",
|
||||
states: [
|
||||
{
|
||||
code: "function compile(src) {\n const ast = parse(src)\n const ir = optimize(ast)\n return emit(ir)\n}",
|
||||
tokens: [
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-0",
|
||||
content: "function",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-1",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-2",
|
||||
content: "compile",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-3",
|
||||
content: "(",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-4",
|
||||
content: "src",
|
||||
color: "#FFAB70",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-5",
|
||||
content: ") {",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-6",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-7",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-8",
|
||||
content: "const",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-9",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-10",
|
||||
content: "ast",
|
||||
color: "#79B8FF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-11",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-12",
|
||||
content: "=",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-13",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-14",
|
||||
content: "parse",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-15",
|
||||
content: "(src)",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-16",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-17",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-18",
|
||||
content: "const",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-19",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-20",
|
||||
content: "ir",
|
||||
color: "#79B8FF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-21",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-22",
|
||||
content: "=",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-23",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-24",
|
||||
content: "optimize",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-25",
|
||||
content: "(ast)",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-26",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-27",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-28",
|
||||
content: "return",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-29",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-30",
|
||||
content: "emit",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-31",
|
||||
content: "(ir)",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-32",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-33",
|
||||
content: "}",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "zGtBjd8ajYlOewpKFJD53Oc7W94UowxIfV0186O1l8A-34",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
window.__BLOCK = {
|
||||
id: "code-shader-dissolve",
|
||||
effect: "dissolve",
|
||||
seq: "compile",
|
||||
duration: 7,
|
||||
seed: 11,
|
||||
};
|
||||
</script>
|
||||
<script>
|
||||
// hf-code-anim-gl.js — WebGL code-animation engine for the "crazy" Code Animations blocks.
|
||||
//
|
||||
// Bridges the baked Shiki tokens (window.__TOKENS) into Three.js: the tokens are drawn once
|
||||
// to an offscreen 2D canvas (a CanvasTexture), which then drives a lit 3D slab, a dissolve
|
||||
// shader, or a GPU particle system. Determinism per the project contract:
|
||||
// - render only via tl.eventCallback("onUpdate", …) + one initial render() — never rAF
|
||||
// - all randomness from a seeded mulberry32 — never Math.random / Date.now
|
||||
// - canvas drawn once after document.fonts.ready (final glyph metrics)
|
||||
// - paused GSAP timeline registered on window.__timelines[id]; HF seeks it per frame
|
||||
//
|
||||
// Each block sets window.__BLOCK = { id, effect, seq, duration, seed } and includes this engine.
|
||||
(function () {
|
||||
"use strict";
|
||||
var T = window.__TOKENS || {};
|
||||
|
||||
// ---- seeded PRNG (no Math.random) -----------------------------------------
|
||||
function mulberry32(a) {
|
||||
return function () {
|
||||
a |= 0;
|
||||
a = (a + 0x6d2b79f5) | 0;
|
||||
var t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
// ---- split a token state into lines of {content,color} --------------------
|
||||
function splitLines(state) {
|
||||
var lines = [[]];
|
||||
for (var i = 0; i < state.tokens.length; i++) {
|
||||
var t = state.tokens[i];
|
||||
if (t.content === "\n") {
|
||||
lines.push([]);
|
||||
continue;
|
||||
}
|
||||
lines[lines.length - 1].push(t);
|
||||
}
|
||||
while (lines.length && lines[lines.length - 1].length === 0) lines.pop();
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ---- THE BRIDGE: draw baked tokens to an offscreen 2D canvas ---------------
|
||||
// Returns { canvas, W, H, lines, lineH, padY, fontPx } so effects can sample it.
|
||||
function drawCodeToCanvas(state, opts) {
|
||||
opts = opts || {};
|
||||
var fontPx = opts.fontPx || 36;
|
||||
var lineH = opts.lineH || 54;
|
||||
var padX = opts.padX || 70;
|
||||
var padY = opts.padY || 60;
|
||||
var weight = opts.weight || 500;
|
||||
var font = weight + " " + fontPx + 'px "JetBrains Mono", monospace';
|
||||
|
||||
var lines = splitLines(state);
|
||||
var measure = document.createElement("canvas").getContext("2d");
|
||||
measure.font = font;
|
||||
var maxW = 0;
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var w = 0;
|
||||
for (var j = 0; j < lines[i].length; j++)
|
||||
w += measure.measureText(lines[i][j].content).width;
|
||||
if (w > maxW) maxW = w;
|
||||
}
|
||||
var W = Math.max(2, Math.ceil(maxW + padX * 2));
|
||||
var H = Math.max(2, Math.ceil(lines.length * lineH + padY * 2));
|
||||
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.width = W;
|
||||
canvas.height = H;
|
||||
var ctx = canvas.getContext("2d");
|
||||
if (opts.transparent) {
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
} else {
|
||||
ctx.fillStyle = opts.bg || "#0b0f17";
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
}
|
||||
ctx.textBaseline = "top";
|
||||
ctx.font = font;
|
||||
for (var li = 0; li < lines.length; li++) {
|
||||
var x = padX;
|
||||
var y = padY + li * lineH;
|
||||
for (var k = 0; k < lines[li].length; k++) {
|
||||
var tok = lines[li][k];
|
||||
ctx.fillStyle = tok.color;
|
||||
ctx.fillText(tok.content, x, y);
|
||||
x += ctx.measureText(tok.content).width;
|
||||
}
|
||||
}
|
||||
return {
|
||||
canvas: canvas,
|
||||
W: W,
|
||||
H: H,
|
||||
lines: lines.length,
|
||||
lineH: lineH,
|
||||
padY: padY,
|
||||
fontPx: fontPx,
|
||||
};
|
||||
}
|
||||
|
||||
function fxDissolve(THREE, scene, camera, renderer, state, st, tl, dur, seed) {
|
||||
scene.background = new THREE.Color(0x05070b);
|
||||
var code = drawCodeToCanvas(state, {
|
||||
fontPx: 40,
|
||||
lineH: 60,
|
||||
padX: 90,
|
||||
padY: 80,
|
||||
weight: 600,
|
||||
});
|
||||
var tex = new THREE.CanvasTexture(code.canvas);
|
||||
tex.minFilter = THREE.LinearFilter;
|
||||
tex.magFilter = THREE.LinearFilter;
|
||||
tex.generateMipmaps = false;
|
||||
|
||||
var aspect = code.W / code.H;
|
||||
var planeH = 6.6;
|
||||
var planeW = planeH * aspect;
|
||||
|
||||
var uniforms = {
|
||||
uTex: { value: tex },
|
||||
uProgress: { value: 0 },
|
||||
uSeed: { value: (seed || 1) * 13.37 },
|
||||
};
|
||||
var mat = new THREE.ShaderMaterial({
|
||||
uniforms: uniforms,
|
||||
transparent: true,
|
||||
vertexShader: [
|
||||
"varying vec2 vUv;",
|
||||
"void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }",
|
||||
].join("\n"),
|
||||
fragmentShader: [
|
||||
"precision highp float;",
|
||||
"uniform sampler2D uTex;",
|
||||
"uniform float uProgress;",
|
||||
"uniform float uSeed;",
|
||||
"varying vec2 vUv;",
|
||||
"float hash(vec2 p){ p=fract(p*vec2(123.34,456.21)); p+=dot(p,p+45.32); return fract(p.x*p.y); }",
|
||||
"float noise(vec2 p){ vec2 i=floor(p),f=fract(p); float a=hash(i),b=hash(i+vec2(1.,0.)),c=hash(i+vec2(0.,1.)),d=hash(i+vec2(1.,1.)); vec2 u=f*f*(3.-2.*f); return mix(mix(a,b,u.x),mix(c,d,u.x),u.y); }",
|
||||
"void main(){",
|
||||
" vec2 uv = vUv;",
|
||||
// multi-octave seeded noise field
|
||||
" float n = noise(uv*16.0+uSeed)*0.6 + noise(uv*48.0+uSeed*1.7)*0.3 + noise(uv*120.0+uSeed*2.3)*0.1;",
|
||||
" float edge = 0.12;",
|
||||
" float reveal = smoothstep(n-edge, n+edge, uProgress);", // 0 hidden -> 1 shown
|
||||
// chromatic fringe strongest at the dissolving edge
|
||||
" float band = reveal*(1.0-reveal)*4.0;",
|
||||
" float off = band*0.010;",
|
||||
" float r = texture2D(uTex, uv+vec2(off,0.0)).r;",
|
||||
" float g = texture2D(uTex, uv).g;",
|
||||
" float b = texture2D(uTex, uv-vec2(off,0.0)).b;",
|
||||
" vec3 codeCol = vec3(r,g,b);",
|
||||
// additive cyan edge glow along the dissolve front
|
||||
" vec3 glow = vec3(0.30,0.72,1.0) * band * 0.9;",
|
||||
" vec3 outc = codeCol*reveal + glow;",
|
||||
" float a = max(reveal, band*0.8);",
|
||||
" gl_FragColor = vec4(outc, a);",
|
||||
"}",
|
||||
].join("\n"),
|
||||
});
|
||||
|
||||
var plane = new THREE.Mesh(new THREE.PlaneGeometry(planeW, planeH), mat);
|
||||
scene.add(plane);
|
||||
|
||||
camera.position.set(0, 0, 10);
|
||||
var vfov = (camera.fov * Math.PI) / 180;
|
||||
var screenAspect = 1920 / 1080;
|
||||
var fitH = planeH / 2 / Math.tan(vfov / 2) / 0.82;
|
||||
var fitW = planeW / 2 / (Math.tan(vfov / 2) * screenAspect) / 0.82;
|
||||
camera.position.z = Math.max(fitH, fitW);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
// compile in, then hold crisp. Overshoot to 1.15 so even max-noise pixels
|
||||
// (n up to ~1.0, edge 0.12) fully resolve — no leftover specks at rest.
|
||||
tl.to(uniforms.uProgress, { value: 1.15, duration: dur * 0.62, ease: "power1.inOut" }, 0);
|
||||
|
||||
// uProgress is tweened by GSAP; the boot loop's render() draws once per frame.
|
||||
return function () {};
|
||||
}
|
||||
|
||||
// ---- dispatch + boot ------------------------------------------------------
|
||||
function buildEffect(THREE, scene, camera, renderer, spec, st, tl) {
|
||||
var state = T[spec.seq].states[0];
|
||||
var dur = spec.duration;
|
||||
if (spec.effect === "dissolve")
|
||||
return fxDissolve(THREE, scene, camera, renderer, state, st, tl, dur, spec.seed);
|
||||
return function () {};
|
||||
}
|
||||
|
||||
// Free GPU resources (renderer, geometries, materials, textures) on page teardown.
|
||||
// One-shot renders exit per job so nothing leaks there; this guards long-lived
|
||||
// Studio/player sessions that reload. Fires only on unload — never during capture.
|
||||
function disposeGL(renderer, scene) {
|
||||
scene.traverse(function (o) {
|
||||
if (o.geometry && o.geometry.dispose) o.geometry.dispose();
|
||||
var mats = o.material ? (Array.isArray(o.material) ? o.material : [o.material]) : [];
|
||||
mats.forEach(function (m) {
|
||||
if (m.map && m.map.dispose) m.map.dispose();
|
||||
if (
|
||||
m.uniforms &&
|
||||
m.uniforms.uTex &&
|
||||
m.uniforms.uTex.value &&
|
||||
m.uniforms.uTex.value.dispose
|
||||
) {
|
||||
m.uniforms.uTex.value.dispose();
|
||||
}
|
||||
if (m.dispose) m.dispose();
|
||||
});
|
||||
});
|
||||
if (renderer.dispose) renderer.dispose();
|
||||
}
|
||||
|
||||
function go() {
|
||||
var spec = window.__BLOCK;
|
||||
var root = document.getElementById("root");
|
||||
var glcanvas = document.getElementById("gl");
|
||||
var W = 1920;
|
||||
var H = 1080;
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
window.__timelines[spec.id] = tl; // register synchronously
|
||||
|
||||
var THREE = window.THREE;
|
||||
var renderer = new THREE.WebGLRenderer({
|
||||
canvas: glcanvas,
|
||||
antialias: true,
|
||||
alpha: false,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.setSize(W, H, false);
|
||||
var scene = new THREE.Scene();
|
||||
var camera = new THREE.PerspectiveCamera(45, W / H, 0.1, 200);
|
||||
var st = {};
|
||||
|
||||
document.fonts.ready.then(function () {
|
||||
var renderFn = buildEffect(THREE, scene, camera, renderer, spec, st, tl);
|
||||
function render() {
|
||||
renderFn();
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
tl.eventCallback("onUpdate", render);
|
||||
var dur = parseFloat(root.dataset.duration) || tl.duration();
|
||||
tl.to({}, { duration: dur }, 0); // pad to full composition length
|
||||
render(); // paint frame 0
|
||||
window.addEventListener("beforeunload", function () {
|
||||
disposeGL(renderer, scene);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", go);
|
||||
else go();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-shader-dissolve" data-composition-src="compositions/code-shader-dissolve.html" data-start="0" data-duration="7" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,21 +3,279 @@ title: "Code Snippet - Apple Terminal Basic"
|
||||
description: "Apple Terminal Basic profile: white background, black text, per-character typing animation of a shell session."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-basic.mp4" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-basic.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Basic preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-snippet-apple-terminal-basic
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-basic" />
|
||||
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-basic.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-snippet-apple-terminal-basic.html`}>
|
||||
|
||||
```html index.html
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Apple Terminal Basic</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #ececec;
|
||||
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
#scene {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #e8e8e8 0%, #d8d8d8 100%);
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
width: 1400px;
|
||||
height: 700px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 30px 80px rgba(0, 0, 0, 0.25),
|
||||
0 10px 30px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 38px;
|
||||
background: #ececec;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
gap: 0;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #d0d0d0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.traffic-lights span {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.traffic-lights .close {
|
||||
background: #ff5f57;
|
||||
border: 1px solid #e0443e;
|
||||
}
|
||||
|
||||
.traffic-lights .minimize {
|
||||
background: #ffbd2e;
|
||||
border: 1px solid #dfa123;
|
||||
}
|
||||
|
||||
.traffic-lights .fullscreen {
|
||||
background: #28c840;
|
||||
border: 1px solid #1aab29;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 13px;
|
||||
color: #333333;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
.terminal-canvas {
|
||||
flex: 1;
|
||||
background: #ffffff;
|
||||
padding: 18px 20px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.output-lines {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
opacity: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.output-line.bold {
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.typed-command {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.cursor-block {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
background: #000000;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="scene"
|
||||
data-composition-id="code-snippet-apple-terminal-basic"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="terminal-window">
|
||||
<div class="titlebar">
|
||||
<div class="traffic-lights">
|
||||
<span class="close"></span>
|
||||
<span class="minimize"></span>
|
||||
<span class="fullscreen"></span>
|
||||
</div>
|
||||
<span class="window-title">bash — 80×24</span>
|
||||
</div>
|
||||
<div class="terminal-canvas">
|
||||
<div class="output-lines">
|
||||
<span class="output-line">Last login: Mon Jun 2 09:14:22 on ttys001</span>
|
||||
<span class="output-line">total 48</span>
|
||||
<span class="output-line bold">drwxr-xr-x 8 user staff 256 Jun 2 09:10 .</span>
|
||||
<span class="output-line">drwxr-xr-x 42 user staff 1344 Jun 1 18:22 ..</span>
|
||||
<span class="output-line">-rw-r--r-- 1 user staff 872 May 30 14:05 README.md</span>
|
||||
<span class="output-line bold">drwxr-xr-x 5 user staff 160 Jun 2 09:10 dist</span>
|
||||
<span class="output-line bold"
|
||||
>drwxr-xr-x 12 user staff 384 Jun 2 09:10 node_modules</span
|
||||
>
|
||||
<span class="output-line">-rw-r--r-- 1 user staff 1204 Jun 2 09:08 package.json</span>
|
||||
<span class="output-line bold">drwxr-xr-x 6 user staff 192 Jun 2 09:10 src</span>
|
||||
<span class="output-line">-rw-r--r-- 1 user staff 348 May 29 11:42 tsconfig.json</span>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
|
||||
><span class="cursor-block"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const command = "ls -la";
|
||||
const typedEl = document.querySelector(".typed-command");
|
||||
const cursorEl = document.querySelector(".cursor-block");
|
||||
const outputLines = document.querySelectorAll(".output-line");
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Phase 1 (0–0.5s): hold on empty prompt
|
||||
tl.set(typedEl, { text: "" }, 0);
|
||||
|
||||
// Phase 2 (0.5–2s): type command char by char
|
||||
const charDuration = 1.5 / command.length;
|
||||
command.split("").forEach((char, i) => {
|
||||
tl.add(
|
||||
() => {
|
||||
typedEl.textContent = command.slice(0, i + 1);
|
||||
},
|
||||
0.5 + i * charDuration,
|
||||
);
|
||||
});
|
||||
|
||||
// Phase 3 (2.2s): hide cursor, show first output line (login message)
|
||||
tl.set(cursorEl, { opacity: 0 }, 2.2);
|
||||
tl.set(outputLines[0], { opacity: 1 }, 2.3);
|
||||
|
||||
// Phase 4 (2.5s): clear command, show ls output line by line
|
||||
tl.add(() => {
|
||||
typedEl.textContent = "";
|
||||
}, 2.5);
|
||||
|
||||
outputLines.forEach((line, i) => {
|
||||
if (i === 0) return; // already shown
|
||||
tl.set(line, { opacity: 1 }, 2.8 + i * 0.08);
|
||||
});
|
||||
|
||||
// Phase 5 (4s): new prompt with blinking cursor
|
||||
tl.add(() => {
|
||||
const inputLine = document.querySelector(".input-line");
|
||||
const newPrompt = document.createElement("div");
|
||||
newPrompt.className = "input-line";
|
||||
newPrompt.innerHTML =
|
||||
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
|
||||
inputLine.style.display = "none";
|
||||
inputLine.parentNode.appendChild(newPrompt);
|
||||
}, 4.0);
|
||||
|
||||
// Phase 6 (4.2–10s): blink cursor 3 times then stop
|
||||
const blinkTimes = [4.2, 4.6, 5.0, 5.4, 5.8, 6.2];
|
||||
blinkTimes.forEach((t, i) => {
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
|
||||
}, t);
|
||||
});
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = "1";
|
||||
}, 6.6);
|
||||
|
||||
window.__timelines["code-snippet-apple-terminal-basic"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-composition-id="code-snippet-apple-terminal-basic"
|
||||
data-composition-src="compositions/code-snippet-apple-terminal-basic.html"
|
||||
@@ -29,8 +287,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
## Features
|
||||
|
||||
- Faithful recreation of macOS Terminal.app Basic profile — white background, black text, solid cursor
|
||||
- macOS window chrome with traffic light buttons (close, minimize, fullscreen) and centered title bar
|
||||
- Per-character GSAP typing animation — command types out character by character
|
||||
- Output lines reveal sequentially after command executes
|
||||
- Cursor blinks three times at the end then holds steady
|
||||
- Self-contained HTML — no external assets, only GSAP CDN
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,21 +3,274 @@ title: "Code Snippet - Apple Terminal Clear Dark"
|
||||
description: "Apple Terminal Clear Dark profile: semi-transparent dark background, per-character typing animation of a shell session."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-clear-dark.mp4" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-clear-dark.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Clear Dark preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-snippet-apple-terminal-clear-dark
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-clear-dark" />
|
||||
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-clear-dark.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-snippet-apple-terminal-clear-dark.html`}>
|
||||
|
||||
```html index.html
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Apple Terminal Clear Dark</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #1a1a1a;
|
||||
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
#scene {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #111111 100%);
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
width: 1400px;
|
||||
height: 700px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 30px 80px rgba(0, 0, 0, 0.7),
|
||||
0 10px 30px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 38px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.traffic-lights span {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.traffic-lights .close {
|
||||
background: #ff5f57;
|
||||
border: 1px solid #e0443e;
|
||||
}
|
||||
|
||||
.traffic-lights .minimize {
|
||||
background: #ffbd2e;
|
||||
border: 1px solid #dfa123;
|
||||
}
|
||||
|
||||
.traffic-lights .fullscreen {
|
||||
background: #28c840;
|
||||
border: 1px solid #1aab29;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 13px;
|
||||
color: #ffffff;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
.terminal-canvas {
|
||||
flex: 1;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
padding: 18px 20px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.output-lines {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
opacity: 0;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.output-line.bold {
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.output-line.dim {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #888888;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.typed-command {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.cursor-block {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
background: #888888;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="scene"
|
||||
data-composition-id="code-snippet-apple-terminal-clear-dark"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="terminal-window">
|
||||
<div class="titlebar">
|
||||
<div class="traffic-lights">
|
||||
<span class="close"></span>
|
||||
<span class="minimize"></span>
|
||||
<span class="fullscreen"></span>
|
||||
</div>
|
||||
<span class="window-title">bash — 80×24</span>
|
||||
</div>
|
||||
<div class="terminal-canvas">
|
||||
<div class="output-lines">
|
||||
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys002</span>
|
||||
<span class="output-line">Scanning dependencies...</span>
|
||||
<span class="output-line">Found 3 vulnerabilities (1 moderate, 2 low)</span>
|
||||
<span class="output-line dim"> package: lodash@4.17.20</span>
|
||||
<span class="output-line dim"> severity: moderate</span>
|
||||
<span class="output-line dim"> fix: lodash@4.17.21</span>
|
||||
<span class="output-line dim"> package: minimist@1.2.5</span>
|
||||
<span class="output-line dim"> severity: low</span>
|
||||
<span class="output-line dim"> fix: minimist@1.2.6</span>
|
||||
<span class="output-line bold">Run `npm audit fix` to fix them.</span>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
|
||||
><span class="cursor-block"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const command = "npm audit";
|
||||
const typedEl = document.querySelector(".typed-command");
|
||||
const cursorEl = document.querySelector(".cursor-block");
|
||||
const outputLines = document.querySelectorAll(".output-line");
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set(typedEl, { text: "" }, 0);
|
||||
|
||||
const charDuration = 1.5 / command.length;
|
||||
command.split("").forEach((char, i) => {
|
||||
tl.add(
|
||||
() => {
|
||||
typedEl.textContent = command.slice(0, i + 1);
|
||||
},
|
||||
0.5 + i * charDuration,
|
||||
);
|
||||
});
|
||||
|
||||
tl.set(cursorEl, { opacity: 0 }, 2.2);
|
||||
tl.set(outputLines[0], { opacity: 1 }, 2.3);
|
||||
|
||||
tl.add(() => {
|
||||
typedEl.textContent = "";
|
||||
}, 2.5);
|
||||
|
||||
outputLines.forEach((line, i) => {
|
||||
if (i === 0) return;
|
||||
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
|
||||
});
|
||||
|
||||
tl.add(() => {
|
||||
const inputLine = document.querySelector(".input-line");
|
||||
const newPrompt = document.createElement("div");
|
||||
newPrompt.className = "input-line";
|
||||
newPrompt.innerHTML =
|
||||
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
|
||||
inputLine.style.display = "none";
|
||||
inputLine.parentNode.appendChild(newPrompt);
|
||||
}, 4.2);
|
||||
|
||||
const blinkTimes = [4.4, 4.8, 5.2, 5.6, 6.0, 6.4];
|
||||
blinkTimes.forEach((t, i) => {
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
|
||||
}, t);
|
||||
});
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = "1";
|
||||
}, 6.8);
|
||||
|
||||
window.__timelines["code-snippet-apple-terminal-clear-dark"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-composition-id="code-snippet-apple-terminal-clear-dark"
|
||||
data-composition-src="compositions/code-snippet-apple-terminal-clear-dark.html"
|
||||
@@ -29,8 +282,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
## Features
|
||||
|
||||
- Faithful recreation of macOS Terminal.app Clear Dark profile — semi-transparent black background with white text
|
||||
- macOS window chrome with traffic light buttons and centered title bar
|
||||
- Per-character GSAP typing animation — command types out character by character
|
||||
- Output lines reveal sequentially after command executes
|
||||
- Cursor blinks three times at the end then holds steady
|
||||
- Self-contained HTML — no external assets, only GSAP CDN
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,21 +3,274 @@ title: "Code Snippet - Apple Terminal Clear Light"
|
||||
description: "Apple Terminal Clear Light profile: semi-transparent light background, per-character typing animation of a shell session."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-clear-light.mp4" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-clear-light.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Clear Light preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-snippet-apple-terminal-clear-light
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-clear-light" />
|
||||
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-clear-light.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-snippet-apple-terminal-clear-light.html`}>
|
||||
|
||||
```html index.html
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Apple Terminal Clear Light</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
#scene {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
width: 1400px;
|
||||
height: 700px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 30px 80px rgba(0, 0, 0, 0.18),
|
||||
0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 38px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
position: relative;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.traffic-lights span {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.traffic-lights .close {
|
||||
background: #ff5f57;
|
||||
border: 1px solid #e0443e;
|
||||
}
|
||||
|
||||
.traffic-lights .minimize {
|
||||
background: #ffbd2e;
|
||||
border: 1px solid #dfa123;
|
||||
}
|
||||
|
||||
.traffic-lights .fullscreen {
|
||||
background: #28c840;
|
||||
border: 1px solid #1aab29;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 13px;
|
||||
color: #333333;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
.terminal-canvas {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
padding: 18px 20px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #000000;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.output-lines {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
opacity: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.output-line.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.output-line.muted {
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #888888;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.typed-command {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.cursor-block {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
background: #888888;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="scene"
|
||||
data-composition-id="code-snippet-apple-terminal-clear-light"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="terminal-window">
|
||||
<div class="titlebar">
|
||||
<div class="traffic-lights">
|
||||
<span class="close"></span>
|
||||
<span class="minimize"></span>
|
||||
<span class="fullscreen"></span>
|
||||
</div>
|
||||
<span class="window-title">bash — 80×24</span>
|
||||
</div>
|
||||
<div class="terminal-canvas">
|
||||
<div class="output-lines">
|
||||
<span class="output-line muted">Last login: Mon Jun 2 09:14:22 on ttys003</span>
|
||||
<span class="output-line bold">git log --oneline -8</span>
|
||||
<span class="output-line">a3f2c19 fix: correct off-by-one in pagination</span>
|
||||
<span class="output-line">b7e8d04 feat: add dark mode toggle</span>
|
||||
<span class="output-line">c1a9f55 refactor: extract auth helpers</span>
|
||||
<span class="output-line">d4e2b31 test: add unit tests for parser</span>
|
||||
<span class="output-line">e6c8a72 docs: update API reference</span>
|
||||
<span class="output-line">f9b1d44 chore: bump dependencies</span>
|
||||
<span class="output-line">2a7e890 feat: implement search endpoint</span>
|
||||
<span class="output-line">3c5f012 init: project bootstrap</span>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">user@Mac myproject % </span><span class="typed-command"></span
|
||||
><span class="cursor-block"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const command = "git log --oneline -8";
|
||||
const typedEl = document.querySelector(".typed-command");
|
||||
const cursorEl = document.querySelector(".cursor-block");
|
||||
const outputLines = document.querySelectorAll(".output-line");
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set(typedEl, { text: "" }, 0);
|
||||
|
||||
const charDuration = 1.5 / command.length;
|
||||
command.split("").forEach((char, i) => {
|
||||
tl.add(
|
||||
() => {
|
||||
typedEl.textContent = command.slice(0, i + 1);
|
||||
},
|
||||
0.5 + i * charDuration,
|
||||
);
|
||||
});
|
||||
|
||||
tl.set(cursorEl, { opacity: 0 }, 2.2);
|
||||
tl.set(outputLines[0], { opacity: 1 }, 2.3);
|
||||
|
||||
tl.add(() => {
|
||||
typedEl.textContent = "";
|
||||
}, 2.5);
|
||||
|
||||
outputLines.forEach((line, i) => {
|
||||
if (i === 0) return;
|
||||
tl.set(line, { opacity: 1 }, 2.8 + i * 0.09);
|
||||
});
|
||||
|
||||
tl.add(() => {
|
||||
const inputLine = document.querySelector(".input-line");
|
||||
const newPrompt = document.createElement("div");
|
||||
newPrompt.className = "input-line";
|
||||
newPrompt.innerHTML =
|
||||
'<span class="prompt">user@Mac myproject % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
|
||||
inputLine.style.display = "none";
|
||||
inputLine.parentNode.appendChild(newPrompt);
|
||||
}, 4.0);
|
||||
|
||||
const blinkTimes = [4.2, 4.6, 5.0, 5.4, 5.8, 6.2];
|
||||
blinkTimes.forEach((t, i) => {
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
|
||||
}, t);
|
||||
});
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = "1";
|
||||
}, 6.6);
|
||||
|
||||
window.__timelines["code-snippet-apple-terminal-clear-light"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-composition-id="code-snippet-apple-terminal-clear-light"
|
||||
data-composition-src="compositions/code-snippet-apple-terminal-clear-light.html"
|
||||
@@ -29,8 +282,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
## Features
|
||||
|
||||
- Faithful recreation of macOS Terminal.app Clear Light profile — semi-transparent white background with black text
|
||||
- macOS window chrome with traffic light buttons and centered title bar
|
||||
- Per-character GSAP typing animation — command types out character by character
|
||||
- Output lines reveal sequentially after command executes
|
||||
- Cursor blinks three times at the end then holds steady
|
||||
- Self-contained HTML — no external assets, only GSAP CDN
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,21 +3,273 @@ title: "Code Snippet - Apple Terminal Grass"
|
||||
description: "Apple Terminal Grass profile: black background with green text, per-character typing animation of a shell session."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-grass.mp4" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-grass.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Grass preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-snippet-apple-terminal-grass
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-grass" />
|
||||
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-grass.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-snippet-apple-terminal-grass.html`}>
|
||||
|
||||
```html index.html
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Apple Terminal Grass</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
#scene {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(ellipse at center, #0a1a0a 0%, #000000 70%);
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
width: 1400px;
|
||||
height: 700px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 30px 80px rgba(0, 193, 0, 0.15),
|
||||
0 10px 30px rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 38px;
|
||||
background: #1c1c1c;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #00c100;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.traffic-lights span {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.traffic-lights .close {
|
||||
background: #ff5f57;
|
||||
border: 1px solid #e0443e;
|
||||
}
|
||||
|
||||
.traffic-lights .minimize {
|
||||
background: #ffbd2e;
|
||||
border: 1px solid #dfa123;
|
||||
}
|
||||
|
||||
.traffic-lights .fullscreen {
|
||||
background: #28c840;
|
||||
border: 1px solid #1aab29;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 13px;
|
||||
color: #00c100;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
.terminal-canvas {
|
||||
flex: 1;
|
||||
background: #000000;
|
||||
padding: 18px 20px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #00c100;
|
||||
}
|
||||
|
||||
.output-lines {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
opacity: 0;
|
||||
color: #00c100;
|
||||
}
|
||||
|
||||
.output-line.bold {
|
||||
font-weight: bold;
|
||||
color: #00c100;
|
||||
}
|
||||
|
||||
.output-line.dim {
|
||||
color: #006600;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #00c100;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.typed-command {
|
||||
color: #00c100;
|
||||
}
|
||||
|
||||
.cursor-block {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
background: #00c100;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="scene"
|
||||
data-composition-id="code-snippet-apple-terminal-grass"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="terminal-window">
|
||||
<div class="titlebar">
|
||||
<div class="traffic-lights">
|
||||
<span class="close"></span>
|
||||
<span class="minimize"></span>
|
||||
<span class="fullscreen"></span>
|
||||
</div>
|
||||
<span class="window-title">bash — 80×24</span>
|
||||
</div>
|
||||
<div class="terminal-canvas">
|
||||
<div class="output-lines">
|
||||
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys004</span>
|
||||
<span class="output-line">Running test suite...</span>
|
||||
<span class="output-line dim"> ✓ should initialize with defaults (2ms)</span>
|
||||
<span class="output-line dim"> ✓ should parse config correctly (4ms)</span>
|
||||
<span class="output-line dim"> ✓ should handle empty input (1ms)</span>
|
||||
<span class="output-line dim"> ✓ should reject invalid tokens (3ms)</span>
|
||||
<span class="output-line dim"> ✓ should emit events on change (5ms)</span>
|
||||
<span class="output-line dim"> ✓ should clean up on destroy (2ms)</span>
|
||||
<span class="output-line bold"> 6 passing (18ms)</span>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
|
||||
><span class="cursor-block"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const command = "npm test";
|
||||
const typedEl = document.querySelector(".typed-command");
|
||||
const cursorEl = document.querySelector(".cursor-block");
|
||||
const outputLines = document.querySelectorAll(".output-line");
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set(typedEl, { text: "" }, 0);
|
||||
|
||||
const charDuration = 1.5 / command.length;
|
||||
command.split("").forEach((char, i) => {
|
||||
tl.add(
|
||||
() => {
|
||||
typedEl.textContent = command.slice(0, i + 1);
|
||||
},
|
||||
0.5 + i * charDuration,
|
||||
);
|
||||
});
|
||||
|
||||
tl.set(cursorEl, { opacity: 0 }, 2.2);
|
||||
tl.set(outputLines[0], { opacity: 1 }, 2.3);
|
||||
|
||||
tl.add(() => {
|
||||
typedEl.textContent = "";
|
||||
}, 2.5);
|
||||
|
||||
outputLines.forEach((line, i) => {
|
||||
if (i === 0) return;
|
||||
tl.set(line, { opacity: 1 }, 2.8 + i * 0.12);
|
||||
});
|
||||
|
||||
tl.add(() => {
|
||||
const inputLine = document.querySelector(".input-line");
|
||||
const newPrompt = document.createElement("div");
|
||||
newPrompt.className = "input-line";
|
||||
newPrompt.innerHTML =
|
||||
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
|
||||
inputLine.style.display = "none";
|
||||
inputLine.parentNode.appendChild(newPrompt);
|
||||
}, 4.0);
|
||||
|
||||
const blinkTimes = [4.2, 4.6, 5.0, 5.4, 5.8, 6.2];
|
||||
blinkTimes.forEach((t, i) => {
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
|
||||
}, t);
|
||||
});
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = "1";
|
||||
}, 6.6);
|
||||
|
||||
window.__timelines["code-snippet-apple-terminal-grass"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-composition-id="code-snippet-apple-terminal-grass"
|
||||
data-composition-src="compositions/code-snippet-apple-terminal-grass.html"
|
||||
@@ -29,8 +281,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
## Features
|
||||
|
||||
- Faithful recreation of macOS Terminal.app Grass profile — black background with #00C100 green text
|
||||
- macOS window chrome with green-accented title bar and traffic light buttons
|
||||
- Per-character GSAP typing animation — command types out character by character
|
||||
- Output lines reveal sequentially after command executes
|
||||
- Cursor blinks three times at the end then holds steady
|
||||
- Self-contained HTML — no external assets, only GSAP CDN
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,21 +3,278 @@ title: "Code Snippet - Apple Terminal Homebrew"
|
||||
description: "Apple Terminal Homebrew profile: black background with bright green text and lime cursor, per-character typing animation of a shell session."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-homebrew.mp4" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-homebrew.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Homebrew preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-snippet-apple-terminal-homebrew
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-homebrew" />
|
||||
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-homebrew.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-snippet-apple-terminal-homebrew.html`}>
|
||||
|
||||
```html index.html
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Apple Terminal Homebrew</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
#scene {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(ellipse at center, #0d1a0d 0%, #000000 70%);
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
width: 1400px;
|
||||
height: 700px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 30px 80px rgba(0, 207, 0, 0.18),
|
||||
0 10px 30px rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 38px;
|
||||
background: #1c1c1c;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #006900;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.traffic-lights span {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.traffic-lights .close {
|
||||
background: #ff5f57;
|
||||
border: 1px solid #e0443e;
|
||||
}
|
||||
|
||||
.traffic-lights .minimize {
|
||||
background: #ffbd2e;
|
||||
border: 1px solid #dfa123;
|
||||
}
|
||||
|
||||
.traffic-lights .fullscreen {
|
||||
background: #28c840;
|
||||
border: 1px solid #1aab29;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 13px;
|
||||
color: #00cf00;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
.terminal-canvas {
|
||||
flex: 1;
|
||||
background: #000000;
|
||||
padding: 18px 20px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #00cf00;
|
||||
}
|
||||
|
||||
.output-lines {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
opacity: 0;
|
||||
color: #00cf00;
|
||||
}
|
||||
|
||||
.output-line.bold {
|
||||
font-weight: bold;
|
||||
color: #00de00;
|
||||
}
|
||||
|
||||
.output-line.dim {
|
||||
color: #006600;
|
||||
}
|
||||
|
||||
.output-line.bright {
|
||||
color: #48f800;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #48f800;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.typed-command {
|
||||
color: #00cf00;
|
||||
}
|
||||
|
||||
.cursor-block {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
background: #48f800;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="scene"
|
||||
data-composition-id="code-snippet-apple-terminal-homebrew"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="terminal-window">
|
||||
<div class="titlebar">
|
||||
<div class="traffic-lights">
|
||||
<span class="close"></span>
|
||||
<span class="minimize"></span>
|
||||
<span class="fullscreen"></span>
|
||||
</div>
|
||||
<span class="window-title">bash — 80×24</span>
|
||||
</div>
|
||||
<div class="terminal-canvas">
|
||||
<div class="output-lines">
|
||||
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys005</span>
|
||||
<span class="output-line">==> Updating Homebrew...</span>
|
||||
<span class="output-line dim">Already up-to-date.</span>
|
||||
<span class="output-line">==> Outdated formulae</span>
|
||||
<span class="output-line">node 20.11.0 -> 22.3.0</span>
|
||||
<span class="output-line">python 3.11.6 -> 3.12.4</span>
|
||||
<span class="output-line">git 2.43.0 -> 2.45.2</span>
|
||||
<span class="output-line">ffmpeg 6.1.1 -> 7.0.0</span>
|
||||
<span class="output-line bright">==> Run `brew upgrade` to update 4 formulae.</span>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
|
||||
><span class="cursor-block"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const command = "brew outdated";
|
||||
const typedEl = document.querySelector(".typed-command");
|
||||
const cursorEl = document.querySelector(".cursor-block");
|
||||
const outputLines = document.querySelectorAll(".output-line");
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set(typedEl, { text: "" }, 0);
|
||||
|
||||
const charDuration = 1.5 / command.length;
|
||||
command.split("").forEach((char, i) => {
|
||||
tl.add(
|
||||
() => {
|
||||
typedEl.textContent = command.slice(0, i + 1);
|
||||
},
|
||||
0.5 + i * charDuration,
|
||||
);
|
||||
});
|
||||
|
||||
tl.set(cursorEl, { opacity: 0 }, 2.2);
|
||||
tl.set(outputLines[0], { opacity: 1 }, 2.3);
|
||||
|
||||
tl.add(() => {
|
||||
typedEl.textContent = "";
|
||||
}, 2.5);
|
||||
|
||||
outputLines.forEach((line, i) => {
|
||||
if (i === 0) return;
|
||||
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
|
||||
});
|
||||
|
||||
tl.add(() => {
|
||||
const inputLine = document.querySelector(".input-line");
|
||||
const newPrompt = document.createElement("div");
|
||||
newPrompt.className = "input-line";
|
||||
newPrompt.innerHTML =
|
||||
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
|
||||
inputLine.style.display = "none";
|
||||
inputLine.parentNode.appendChild(newPrompt);
|
||||
}, 4.2);
|
||||
|
||||
const blinkTimes = [4.4, 4.8, 5.2, 5.6, 6.0, 6.4];
|
||||
blinkTimes.forEach((t, i) => {
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
|
||||
}, t);
|
||||
});
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = "1";
|
||||
}, 6.8);
|
||||
|
||||
window.__timelines["code-snippet-apple-terminal-homebrew"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-composition-id="code-snippet-apple-terminal-homebrew"
|
||||
data-composition-src="compositions/code-snippet-apple-terminal-homebrew.html"
|
||||
@@ -29,8 +286,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
## Features
|
||||
|
||||
- Faithful recreation of macOS Terminal.app Homebrew profile — black background with #00CF00 text and #48F800 cursor
|
||||
- macOS window chrome with traffic light buttons and centered title bar
|
||||
- Per-character GSAP typing animation — command types out character by character
|
||||
- Output lines reveal sequentially after command executes
|
||||
- Cursor blinks three times at the end then holds steady
|
||||
- Self-contained HTML — no external assets, only GSAP CDN
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,21 +3,278 @@ title: "Code Snippet - Apple Terminal Man Page"
|
||||
description: "Apple Terminal Man Page profile: pale yellow background with black text, per-character typing animation of a shell session."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-man-page.mp4" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-man-page.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Man Page preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-snippet-apple-terminal-man-page
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-man-page" />
|
||||
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-man-page.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-snippet-apple-terminal-man-page.html`}>
|
||||
|
||||
```html index.html
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Apple Terminal Man Page</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
#scene {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #ede87a 0%, #ddd566 100%);
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
width: 1400px;
|
||||
height: 700px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 30px 80px rgba(0, 0, 0, 0.2),
|
||||
0 10px 30px rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 38px;
|
||||
background: #f0e680;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #c8be40;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.traffic-lights span {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.traffic-lights .close {
|
||||
background: #ff5f57;
|
||||
border: 1px solid #e0443e;
|
||||
}
|
||||
|
||||
.traffic-lights .minimize {
|
||||
background: #ffbd2e;
|
||||
border: 1px solid #dfa123;
|
||||
}
|
||||
|
||||
.traffic-lights .fullscreen {
|
||||
background: #28c840;
|
||||
border: 1px solid #1aab29;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 13px;
|
||||
color: #333333;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
.terminal-canvas {
|
||||
flex: 1;
|
||||
background: #fef49c;
|
||||
padding: 18px 20px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.output-lines {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
opacity: 0;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.output-line.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.output-line.indent {
|
||||
padding-left: 24px;
|
||||
color: #222222;
|
||||
}
|
||||
|
||||
.output-line.section {
|
||||
font-weight: bold;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.typed-command {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.cursor-block {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
background: #000000;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="scene"
|
||||
data-composition-id="code-snippet-apple-terminal-man-page"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="terminal-window">
|
||||
<div class="titlebar">
|
||||
<div class="traffic-lights">
|
||||
<span class="close"></span>
|
||||
<span class="minimize"></span>
|
||||
<span class="fullscreen"></span>
|
||||
</div>
|
||||
<span class="window-title">man — 80×24</span>
|
||||
</div>
|
||||
<div class="terminal-canvas">
|
||||
<div class="output-lines">
|
||||
<span class="output-line section">CURL(1) General Commands Manual CURL(1)</span>
|
||||
<span class="output-line"></span>
|
||||
<span class="output-line section">NAME</span>
|
||||
<span class="output-line indent">curl -- transfer a URL</span>
|
||||
<span class="output-line"></span>
|
||||
<span class="output-line section">SYNOPSIS</span>
|
||||
<span class="output-line indent">curl [options / URLs]</span>
|
||||
<span class="output-line"></span>
|
||||
<span class="output-line section">DESCRIPTION</span>
|
||||
<span class="output-line indent"
|
||||
>curl is a tool for transferring data from or to a server using URLs.</span
|
||||
>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
|
||||
><span class="cursor-block"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const command = "man curl";
|
||||
const typedEl = document.querySelector(".typed-command");
|
||||
const cursorEl = document.querySelector(".cursor-block");
|
||||
const outputLines = document.querySelectorAll(".output-line");
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set(typedEl, { text: "" }, 0);
|
||||
|
||||
const charDuration = 1.5 / command.length;
|
||||
command.split("").forEach((char, i) => {
|
||||
tl.add(
|
||||
() => {
|
||||
typedEl.textContent = command.slice(0, i + 1);
|
||||
},
|
||||
0.5 + i * charDuration,
|
||||
);
|
||||
});
|
||||
|
||||
tl.set(cursorEl, { opacity: 0 }, 2.2);
|
||||
|
||||
tl.add(() => {
|
||||
typedEl.textContent = "";
|
||||
}, 2.4);
|
||||
|
||||
outputLines.forEach((line, i) => {
|
||||
tl.set(line, { opacity: 1 }, 2.5 + i * 0.1);
|
||||
});
|
||||
|
||||
tl.add(() => {
|
||||
const inputLine = document.querySelector(".input-line");
|
||||
const newPrompt = document.createElement("div");
|
||||
newPrompt.className = "input-line";
|
||||
newPrompt.innerHTML =
|
||||
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
|
||||
inputLine.style.display = "none";
|
||||
inputLine.parentNode.appendChild(newPrompt);
|
||||
}, 4.0);
|
||||
|
||||
const blinkTimes = [4.2, 4.6, 5.0, 5.4, 5.8, 6.2];
|
||||
blinkTimes.forEach((t, i) => {
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
|
||||
}, t);
|
||||
});
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = "1";
|
||||
}, 6.6);
|
||||
|
||||
window.__timelines["code-snippet-apple-terminal-man-page"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-composition-id="code-snippet-apple-terminal-man-page"
|
||||
data-composition-src="compositions/code-snippet-apple-terminal-man-page.html"
|
||||
@@ -29,8 +286,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
## Features
|
||||
|
||||
- Faithful recreation of macOS Terminal.app Man Page profile — pale yellow background with black text
|
||||
- Shows a realistic man page output structure with sections and indented content
|
||||
- macOS window chrome with traffic light buttons and centered title bar
|
||||
- Per-character GSAP typing animation — command types out character by character
|
||||
- Output lines reveal sequentially after command executes
|
||||
- Self-contained HTML — no external assets, only GSAP CDN
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,21 +3,274 @@ title: "Code Snippet - Apple Terminal Novel"
|
||||
description: "Apple Terminal Novel profile: warm parchment background with dark brown text, per-character typing animation of a shell session."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-novel.mp4" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-novel.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Novel preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-snippet-apple-terminal-novel
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-novel" />
|
||||
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-novel.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-snippet-apple-terminal-novel.html`}>
|
||||
|
||||
```html index.html
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Apple Terminal Novel</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
#scene {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #c9a86c 0%, #b8955a 100%);
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
width: 1400px;
|
||||
height: 700px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 30px 80px rgba(62, 41, 0, 0.3),
|
||||
0 10px 30px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 38px;
|
||||
background: #c9a86c;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #a88040;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.traffic-lights span {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.traffic-lights .close {
|
||||
background: #ff5f57;
|
||||
border: 1px solid #e0443e;
|
||||
}
|
||||
|
||||
.traffic-lights .minimize {
|
||||
background: #ffbd2e;
|
||||
border: 1px solid #dfa123;
|
||||
}
|
||||
|
||||
.traffic-lights .fullscreen {
|
||||
background: #28c840;
|
||||
border: 1px solid #1aab29;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 13px;
|
||||
color: #3e2900;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
.terminal-canvas {
|
||||
flex: 1;
|
||||
background: #dfc18a;
|
||||
padding: 18px 20px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #3e2900;
|
||||
}
|
||||
|
||||
.output-lines {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
opacity: 0;
|
||||
color: #3e2900;
|
||||
}
|
||||
|
||||
.output-line.bold {
|
||||
font-weight: bold;
|
||||
color: #3e2900;
|
||||
}
|
||||
|
||||
.output-line.muted {
|
||||
color: #7a5a20;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #3e2900;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.typed-command {
|
||||
color: #3e2900;
|
||||
}
|
||||
|
||||
.cursor-block {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
background: #3e2900;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="scene"
|
||||
data-composition-id="code-snippet-apple-terminal-novel"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="terminal-window">
|
||||
<div class="titlebar">
|
||||
<div class="traffic-lights">
|
||||
<span class="close"></span>
|
||||
<span class="minimize"></span>
|
||||
<span class="fullscreen"></span>
|
||||
</div>
|
||||
<span class="window-title">bash — 80×24</span>
|
||||
</div>
|
||||
<div class="terminal-canvas">
|
||||
<div class="output-lines">
|
||||
<span class="output-line muted">Last login: Mon Jun 2 09:14:22 on ttys006</span>
|
||||
<span class="output-line muted">Python 3.12.4 (main, Jun 6 2024, 18:26:44)</span>
|
||||
<span class="output-line muted"
|
||||
>Type "help", "copyright", "credits" or "license" for more information.</span
|
||||
>
|
||||
<span class="output-line bold">>>> import hyperframes</span>
|
||||
<span class="output-line bold">>>> hf = hyperframes.Composition("intro.html")</span>
|
||||
<span class="output-line">>>> hf.duration</span>
|
||||
<span class="output-line">12.0</span>
|
||||
<span class="output-line">>>> hf.dimensions</span>
|
||||
<span class="output-line">Dimensions(width=1920, height=1080)</span>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
|
||||
><span class="cursor-block"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const command = "python3";
|
||||
const typedEl = document.querySelector(".typed-command");
|
||||
const cursorEl = document.querySelector(".cursor-block");
|
||||
const outputLines = document.querySelectorAll(".output-line");
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set(typedEl, { text: "" }, 0);
|
||||
|
||||
const charDuration = 1.5 / command.length;
|
||||
command.split("").forEach((char, i) => {
|
||||
tl.add(
|
||||
() => {
|
||||
typedEl.textContent = command.slice(0, i + 1);
|
||||
},
|
||||
0.5 + i * charDuration,
|
||||
);
|
||||
});
|
||||
|
||||
tl.set(cursorEl, { opacity: 0 }, 2.2);
|
||||
tl.set(outputLines[0], { opacity: 1 }, 2.3);
|
||||
|
||||
tl.add(() => {
|
||||
typedEl.textContent = "";
|
||||
}, 2.5);
|
||||
|
||||
outputLines.forEach((line, i) => {
|
||||
if (i === 0) return;
|
||||
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
|
||||
});
|
||||
|
||||
tl.add(() => {
|
||||
const inputLine = document.querySelector(".input-line");
|
||||
const newPrompt = document.createElement("div");
|
||||
newPrompt.className = "input-line";
|
||||
newPrompt.innerHTML =
|
||||
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
|
||||
inputLine.style.display = "none";
|
||||
inputLine.parentNode.appendChild(newPrompt);
|
||||
}, 4.2);
|
||||
|
||||
const blinkTimes = [4.4, 4.8, 5.2, 5.6, 6.0, 6.4];
|
||||
blinkTimes.forEach((t, i) => {
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
|
||||
}, t);
|
||||
});
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = "1";
|
||||
}, 6.8);
|
||||
|
||||
window.__timelines["code-snippet-apple-terminal-novel"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-composition-id="code-snippet-apple-terminal-novel"
|
||||
data-composition-src="compositions/code-snippet-apple-terminal-novel.html"
|
||||
@@ -29,8 +282,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
## Features
|
||||
|
||||
- Faithful recreation of macOS Terminal.app Novel profile — warm parchment background with #3E2900 dark brown text
|
||||
- macOS window chrome with traffic light buttons and centered title bar
|
||||
- Per-character GSAP typing animation — command types out character by character
|
||||
- Output lines reveal sequentially after command executes
|
||||
- Cursor blinks three times at the end then holds steady
|
||||
- Self-contained HTML — no external assets, only GSAP CDN
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,21 +3,283 @@ title: "Code Snippet - Apple Terminal Ocean"
|
||||
description: "Apple Terminal Ocean profile: deep blue background with white text, per-character typing animation of a shell session."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-ocean.mp4" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-ocean.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Ocean preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-snippet-apple-terminal-ocean
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-ocean" />
|
||||
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-ocean.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-snippet-apple-terminal-ocean.html`}>
|
||||
|
||||
```html index.html
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Apple Terminal Ocean</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #1a3f9e;
|
||||
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
#scene {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(ellipse at center, #224fbe 0%, #0e2878 100%);
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
width: 1400px;
|
||||
height: 700px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 30px 80px rgba(0, 0, 0, 0.6),
|
||||
0 10px 30px rgba(34, 79, 190, 0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 38px;
|
||||
background: #1a3f9e;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.traffic-lights span {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.traffic-lights .close {
|
||||
background: #ff5f57;
|
||||
border: 1px solid #e0443e;
|
||||
}
|
||||
|
||||
.traffic-lights .minimize {
|
||||
background: #ffbd2e;
|
||||
border: 1px solid #dfa123;
|
||||
}
|
||||
|
||||
.traffic-lights .fullscreen {
|
||||
background: #28c840;
|
||||
border: 1px solid #1aab29;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 13px;
|
||||
color: #ffffff;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
.terminal-canvas {
|
||||
flex: 1;
|
||||
background: #224fbe;
|
||||
padding: 18px 20px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.output-lines {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
opacity: 0;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.output-line.bold {
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.output-line.dim {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.output-line.success {
|
||||
color: #7fffb3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.typed-command {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.cursor-block {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
background: #ffffff;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="scene"
|
||||
data-composition-id="code-snippet-apple-terminal-ocean"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="terminal-window">
|
||||
<div class="titlebar">
|
||||
<div class="traffic-lights">
|
||||
<span class="close"></span>
|
||||
<span class="minimize"></span>
|
||||
<span class="fullscreen"></span>
|
||||
</div>
|
||||
<span class="window-title">bash — 80×24</span>
|
||||
</div>
|
||||
<div class="terminal-canvas">
|
||||
<div class="output-lines">
|
||||
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys007</span>
|
||||
<span class="output-line bold">docker ps</span>
|
||||
<span class="output-line dim">CONTAINER ID IMAGE COMMAND CREATED</span>
|
||||
<span class="output-line"
|
||||
>a1b2c3d4e5f6 nginx:latest "/docker-entrypoint.…" 2 hours ago</span
|
||||
>
|
||||
<span class="output-line"
|
||||
>b2c3d4e5f6a7 postgres:15 "docker-entrypoint.s…" 2 hours ago</span
|
||||
>
|
||||
<span class="output-line"
|
||||
>c3d4e5f6a7b8 redis:alpine "docker-entrypoint.s…" 2 hours ago</span
|
||||
>
|
||||
<span class="output-line success">3 containers running</span>
|
||||
<span class="output-line dim">Use 'docker logs CONTAINER' to view output</span>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
|
||||
><span class="cursor-block"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const command = "docker ps";
|
||||
const typedEl = document.querySelector(".typed-command");
|
||||
const cursorEl = document.querySelector(".cursor-block");
|
||||
const outputLines = document.querySelectorAll(".output-line");
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set(typedEl, { text: "" }, 0);
|
||||
|
||||
const charDuration = 1.5 / command.length;
|
||||
command.split("").forEach((char, i) => {
|
||||
tl.add(
|
||||
() => {
|
||||
typedEl.textContent = command.slice(0, i + 1);
|
||||
},
|
||||
0.5 + i * charDuration,
|
||||
);
|
||||
});
|
||||
|
||||
tl.set(cursorEl, { opacity: 0 }, 2.2);
|
||||
tl.set(outputLines[0], { opacity: 1 }, 2.3);
|
||||
|
||||
tl.add(() => {
|
||||
typedEl.textContent = "";
|
||||
}, 2.5);
|
||||
|
||||
outputLines.forEach((line, i) => {
|
||||
if (i === 0) return;
|
||||
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
|
||||
});
|
||||
|
||||
tl.add(() => {
|
||||
const inputLine = document.querySelector(".input-line");
|
||||
const newPrompt = document.createElement("div");
|
||||
newPrompt.className = "input-line";
|
||||
newPrompt.innerHTML =
|
||||
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
|
||||
inputLine.style.display = "none";
|
||||
inputLine.parentNode.appendChild(newPrompt);
|
||||
}, 4.2);
|
||||
|
||||
const blinkTimes = [4.4, 4.8, 5.2, 5.6, 6.0, 6.4];
|
||||
blinkTimes.forEach((t, i) => {
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
|
||||
}, t);
|
||||
});
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = "1";
|
||||
}, 6.8);
|
||||
|
||||
window.__timelines["code-snippet-apple-terminal-ocean"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-composition-id="code-snippet-apple-terminal-ocean"
|
||||
data-composition-src="compositions/code-snippet-apple-terminal-ocean.html"
|
||||
@@ -29,8 +291,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
## Features
|
||||
|
||||
- Faithful recreation of macOS Terminal.app Ocean profile — #224FBE blue background with white text
|
||||
- macOS window chrome with traffic light buttons and centered title bar
|
||||
- Per-character GSAP typing animation — command types out character by character
|
||||
- Output lines reveal sequentially after command executes
|
||||
- Cursor blinks three times at the end then holds steady
|
||||
- Self-contained HTML — no external assets, only GSAP CDN
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,21 +3,277 @@ title: "Code Snippet - Apple Terminal Pro"
|
||||
description: "Apple Terminal Pro profile: black background with grey text and lime green cursor, per-character typing animation of a shell session."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-pro.mp4" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-pro.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Pro preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-snippet-apple-terminal-pro
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-pro" />
|
||||
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-pro.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-snippet-apple-terminal-pro.html`}>
|
||||
|
||||
```html index.html
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Apple Terminal Pro</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
#scene {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(ellipse at center, #141414 0%, #000000 70%);
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
width: 1400px;
|
||||
height: 700px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 30px 80px rgba(136, 255, 103, 0.08),
|
||||
0 10px 30px rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 38px;
|
||||
background: #1c1c1c;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #333333;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.traffic-lights span {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.traffic-lights .close {
|
||||
background: #ff5f57;
|
||||
border: 1px solid #e0443e;
|
||||
}
|
||||
|
||||
.traffic-lights .minimize {
|
||||
background: #ffbd2e;
|
||||
border: 1px solid #dfa123;
|
||||
}
|
||||
|
||||
.traffic-lights .fullscreen {
|
||||
background: #28c840;
|
||||
border: 1px solid #1aab29;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 13px;
|
||||
color: #bbbbbb;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
.terminal-canvas {
|
||||
flex: 1;
|
||||
background: #000000;
|
||||
padding: 18px 20px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #bbbbbb;
|
||||
}
|
||||
|
||||
.output-lines {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
opacity: 0;
|
||||
color: #bbbbbb;
|
||||
}
|
||||
|
||||
.output-line.bold {
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.output-line.accent {
|
||||
color: #88ff67;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.output-line.dim {
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #88ff67;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.typed-command {
|
||||
color: #bbbbbb;
|
||||
}
|
||||
|
||||
.cursor-block {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
background: #88ff67;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="scene"
|
||||
data-composition-id="code-snippet-apple-terminal-pro"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="terminal-window">
|
||||
<div class="titlebar">
|
||||
<div class="traffic-lights">
|
||||
<span class="close"></span>
|
||||
<span class="minimize"></span>
|
||||
<span class="fullscreen"></span>
|
||||
</div>
|
||||
<span class="window-title">bash — 80×24</span>
|
||||
</div>
|
||||
<div class="terminal-canvas">
|
||||
<div class="output-lines">
|
||||
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys008</span>
|
||||
<span class="output-line dim">Building production bundle...</span>
|
||||
<span class="output-line"> vite v5.3.1 building for production...</span>
|
||||
<span class="output-line"> ✓ 142 modules transformed.</span>
|
||||
<span class="output-line"> dist/index.html 0.48 kB</span>
|
||||
<span class="output-line"> dist/assets/index-Cv3rN.js 284.12 kB</span>
|
||||
<span class="output-line"> dist/assets/index-Du4mP.css 42.08 kB</span>
|
||||
<span class="output-line accent"> ✓ built in 2.31s</span>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">user@Mac myapp % </span><span class="typed-command"></span
|
||||
><span class="cursor-block"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const command = "npm run build";
|
||||
const typedEl = document.querySelector(".typed-command");
|
||||
const cursorEl = document.querySelector(".cursor-block");
|
||||
const outputLines = document.querySelectorAll(".output-line");
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set(typedEl, { text: "" }, 0);
|
||||
|
||||
const charDuration = 1.5 / command.length;
|
||||
command.split("").forEach((char, i) => {
|
||||
tl.add(
|
||||
() => {
|
||||
typedEl.textContent = command.slice(0, i + 1);
|
||||
},
|
||||
0.5 + i * charDuration,
|
||||
);
|
||||
});
|
||||
|
||||
tl.set(cursorEl, { opacity: 0 }, 2.2);
|
||||
tl.set(outputLines[0], { opacity: 1 }, 2.3);
|
||||
|
||||
tl.add(() => {
|
||||
typedEl.textContent = "";
|
||||
}, 2.5);
|
||||
|
||||
outputLines.forEach((line, i) => {
|
||||
if (i === 0) return;
|
||||
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
|
||||
});
|
||||
|
||||
tl.add(() => {
|
||||
const inputLine = document.querySelector(".input-line");
|
||||
const newPrompt = document.createElement("div");
|
||||
newPrompt.className = "input-line";
|
||||
newPrompt.innerHTML =
|
||||
'<span class="prompt">user@Mac myapp % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
|
||||
inputLine.style.display = "none";
|
||||
inputLine.parentNode.appendChild(newPrompt);
|
||||
}, 4.2);
|
||||
|
||||
const blinkTimes = [4.4, 4.8, 5.2, 5.6, 6.0, 6.4];
|
||||
blinkTimes.forEach((t, i) => {
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
|
||||
}, t);
|
||||
});
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = "1";
|
||||
}, 6.8);
|
||||
|
||||
window.__timelines["code-snippet-apple-terminal-pro"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-composition-id="code-snippet-apple-terminal-pro"
|
||||
data-composition-src="compositions/code-snippet-apple-terminal-pro.html"
|
||||
@@ -29,8 +285,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
## Features
|
||||
|
||||
- Faithful recreation of macOS Terminal.app Pro profile — black background, #BBBBBB grey text, #88FF67 lime cursor
|
||||
- macOS window chrome with traffic light buttons and centered title bar
|
||||
- Per-character GSAP typing animation — command types out character by character
|
||||
- Output lines reveal sequentially after command executes
|
||||
- Cursor blinks three times at the end then holds steady
|
||||
- Self-contained HTML — no external assets, only GSAP CDN
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,21 +3,274 @@ title: "Code Snippet - Apple Terminal Red Sands"
|
||||
description: "Apple Terminal Red Sands profile: deep red background with sandy text, per-character typing animation of a shell session."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-red-sands.mp4" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-red-sands.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Red Sands preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-snippet-apple-terminal-red-sands
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-red-sands" />
|
||||
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-red-sands.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-snippet-apple-terminal-red-sands.html`}>
|
||||
|
||||
```html index.html
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Apple Terminal Red Sands</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #7b140e;
|
||||
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
#scene {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(ellipse at center, #8a1a12 0%, #4a0a06 100%);
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
width: 1400px;
|
||||
height: 700px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 30px 80px rgba(0, 0, 0, 0.7),
|
||||
0 10px 30px rgba(123, 20, 14, 0.4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 38px;
|
||||
background: #5e0f0a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #8b2018;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.traffic-lights span {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.traffic-lights .close {
|
||||
background: #ff5f57;
|
||||
border: 1px solid #e0443e;
|
||||
}
|
||||
|
||||
.traffic-lights .minimize {
|
||||
background: #ffbd2e;
|
||||
border: 1px solid #dfa123;
|
||||
}
|
||||
|
||||
.traffic-lights .fullscreen {
|
||||
background: #28c840;
|
||||
border: 1px solid #1aab29;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 13px;
|
||||
color: #ffe1b9;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
.terminal-canvas {
|
||||
flex: 1;
|
||||
background: #7b140e;
|
||||
padding: 18px 20px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #ffe1b9;
|
||||
}
|
||||
|
||||
.output-lines {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
opacity: 0;
|
||||
color: #ffe1b9;
|
||||
}
|
||||
|
||||
.output-line.bold {
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.output-line.dim {
|
||||
color: #c4916a;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #ffe1b9;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.typed-command {
|
||||
color: #ffe1b9;
|
||||
}
|
||||
|
||||
.cursor-block {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
background: #ffffff;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="scene"
|
||||
data-composition-id="code-snippet-apple-terminal-red-sands"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="terminal-window">
|
||||
<div class="titlebar">
|
||||
<div class="traffic-lights">
|
||||
<span class="close"></span>
|
||||
<span class="minimize"></span>
|
||||
<span class="fullscreen"></span>
|
||||
</div>
|
||||
<span class="window-title">bash — 80×24</span>
|
||||
</div>
|
||||
<div class="terminal-canvas">
|
||||
<div class="output-lines">
|
||||
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys009</span>
|
||||
<span class="output-line">Fetching https://api.example.com/status</span>
|
||||
<span class="output-line dim"> HTTP/2 200</span>
|
||||
<span class="output-line dim"> content-type: application/json</span>
|
||||
<span class="output-line dim"> x-request-id: a3f9c21e-04b2-4d78</span>
|
||||
<span class="output-line">{</span>
|
||||
<span class="output-line"> "status": "healthy",</span>
|
||||
<span class="output-line"> "version": "2.4.1",</span>
|
||||
<span class="output-line bold"> "uptime": "14d 6h 32m"</span>
|
||||
<span class="output-line">}</span>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
|
||||
><span class="cursor-block"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const command = "curl -s api.example.com/status | jq";
|
||||
const typedEl = document.querySelector(".typed-command");
|
||||
const cursorEl = document.querySelector(".cursor-block");
|
||||
const outputLines = document.querySelectorAll(".output-line");
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set(typedEl, { text: "" }, 0);
|
||||
|
||||
const charDuration = 1.5 / command.length;
|
||||
command.split("").forEach((char, i) => {
|
||||
tl.add(
|
||||
() => {
|
||||
typedEl.textContent = command.slice(0, i + 1);
|
||||
},
|
||||
0.5 + i * charDuration,
|
||||
);
|
||||
});
|
||||
|
||||
tl.set(cursorEl, { opacity: 0 }, 2.2);
|
||||
tl.set(outputLines[0], { opacity: 1 }, 2.3);
|
||||
|
||||
tl.add(() => {
|
||||
typedEl.textContent = "";
|
||||
}, 2.5);
|
||||
|
||||
outputLines.forEach((line, i) => {
|
||||
if (i === 0) return;
|
||||
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
|
||||
});
|
||||
|
||||
tl.add(() => {
|
||||
const inputLine = document.querySelector(".input-line");
|
||||
const newPrompt = document.createElement("div");
|
||||
newPrompt.className = "input-line";
|
||||
newPrompt.innerHTML =
|
||||
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
|
||||
inputLine.style.display = "none";
|
||||
inputLine.parentNode.appendChild(newPrompt);
|
||||
}, 4.4);
|
||||
|
||||
const blinkTimes = [4.6, 5.0, 5.4, 5.8, 6.2, 6.6];
|
||||
blinkTimes.forEach((t, i) => {
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
|
||||
}, t);
|
||||
});
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = "1";
|
||||
}, 7.0);
|
||||
|
||||
window.__timelines["code-snippet-apple-terminal-red-sands"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-composition-id="code-snippet-apple-terminal-red-sands"
|
||||
data-composition-src="compositions/code-snippet-apple-terminal-red-sands.html"
|
||||
@@ -29,8 +282,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
## Features
|
||||
|
||||
- Faithful recreation of macOS Terminal.app Red Sands profile — #7B140E red background with #FFE1B9 sandy text
|
||||
- macOS window chrome with traffic light buttons and centered title bar
|
||||
- Per-character GSAP typing animation — command types out character by character
|
||||
- Output lines reveal sequentially after command executes
|
||||
- Cursor blinks three times at the end then holds steady
|
||||
- Self-contained HTML — no external assets, only GSAP CDN
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,21 +3,280 @@ title: "Code Snippet - Apple Terminal Silver Aerogel"
|
||||
description: "Apple Terminal Silver Aerogel profile: dark grey background with white text, per-character typing animation of a shell session."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-silver-aerogel.mp4" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-silver-aerogel.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Silver Aerogel preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-snippet-apple-terminal-silver-aerogel
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-silver-aerogel" />
|
||||
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-silver-aerogel.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-snippet-apple-terminal-silver-aerogel.html`}>
|
||||
|
||||
```html index.html
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Apple Terminal Silver Aerogel</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #2a2a2a;
|
||||
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
#scene {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(ellipse at center, #3a3a3a 0%, #1c1c1c 100%);
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
width: 1400px;
|
||||
height: 700px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 30px 80px rgba(0, 0, 0, 0.6),
|
||||
0 10px 30px rgba(232, 232, 232, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 38px;
|
||||
background: #2a2a2a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #444444;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.traffic-lights span {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.traffic-lights .close {
|
||||
background: #ff5f57;
|
||||
border: 1px solid #e0443e;
|
||||
}
|
||||
|
||||
.traffic-lights .minimize {
|
||||
background: #ffbd2e;
|
||||
border: 1px solid #dfa123;
|
||||
}
|
||||
|
||||
.traffic-lights .fullscreen {
|
||||
background: #28c840;
|
||||
border: 1px solid #1aab29;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 13px;
|
||||
color: #ffffff;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
.terminal-canvas {
|
||||
flex: 1;
|
||||
background: #3d3d3d;
|
||||
padding: 18px 20px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.output-lines {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
opacity: 0;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.output-line.bold {
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.output-line.dim {
|
||||
color: #aaaaaa;
|
||||
}
|
||||
|
||||
.output-line.success {
|
||||
color: #7fffb3;
|
||||
}
|
||||
|
||||
.output-line.warn {
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #e8e8e8;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.typed-command {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.cursor-block {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
background: #e8e8e8;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="scene"
|
||||
data-composition-id="code-snippet-apple-terminal-silver-aerogel"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="terminal-window">
|
||||
<div class="titlebar">
|
||||
<div class="traffic-lights">
|
||||
<span class="close"></span>
|
||||
<span class="minimize"></span>
|
||||
<span class="fullscreen"></span>
|
||||
</div>
|
||||
<span class="window-title">bash — 80×24</span>
|
||||
</div>
|
||||
<div class="terminal-canvas">
|
||||
<div class="output-lines">
|
||||
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys010</span>
|
||||
<span class="output-line">Checking system resources...</span>
|
||||
<span class="output-line">Filesystem Size Used Avail Use%</span>
|
||||
<span class="output-line">/dev/disk3s1s1 460G 142G 302G 32%</span>
|
||||
<span class="output-line dim">/dev/disk3s6 460G 5.0G 302G 2%</span>
|
||||
<span class="output-line">/dev/disk3s2 460G 11G 302G 4%</span>
|
||||
<span class="output-line warn">Memory: 14.2 GB / 16.0 GB used (89%)</span>
|
||||
<span class="output-line success">CPU: 8-core, 12% load average</span>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
|
||||
><span class="cursor-block"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const command = "df -h && vm_stat";
|
||||
const typedEl = document.querySelector(".typed-command");
|
||||
const cursorEl = document.querySelector(".cursor-block");
|
||||
const outputLines = document.querySelectorAll(".output-line");
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set(typedEl, { text: "" }, 0);
|
||||
|
||||
const charDuration = 1.5 / command.length;
|
||||
command.split("").forEach((char, i) => {
|
||||
tl.add(
|
||||
() => {
|
||||
typedEl.textContent = command.slice(0, i + 1);
|
||||
},
|
||||
0.5 + i * charDuration,
|
||||
);
|
||||
});
|
||||
|
||||
tl.set(cursorEl, { opacity: 0 }, 2.2);
|
||||
tl.set(outputLines[0], { opacity: 1 }, 2.3);
|
||||
|
||||
tl.add(() => {
|
||||
typedEl.textContent = "";
|
||||
}, 2.5);
|
||||
|
||||
outputLines.forEach((line, i) => {
|
||||
if (i === 0) return;
|
||||
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
|
||||
});
|
||||
|
||||
tl.add(() => {
|
||||
const inputLine = document.querySelector(".input-line");
|
||||
const newPrompt = document.createElement("div");
|
||||
newPrompt.className = "input-line";
|
||||
newPrompt.innerHTML =
|
||||
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
|
||||
inputLine.style.display = "none";
|
||||
inputLine.parentNode.appendChild(newPrompt);
|
||||
}, 4.2);
|
||||
|
||||
const blinkTimes = [4.4, 4.8, 5.2, 5.6, 6.0, 6.4];
|
||||
blinkTimes.forEach((t, i) => {
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
|
||||
}, t);
|
||||
});
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = "1";
|
||||
}, 6.8);
|
||||
|
||||
window.__timelines["code-snippet-apple-terminal-silver-aerogel"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-composition-id="code-snippet-apple-terminal-silver-aerogel"
|
||||
data-composition-src="compositions/code-snippet-apple-terminal-silver-aerogel.html"
|
||||
@@ -29,8 +288,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
## Features
|
||||
|
||||
- Faithful recreation of macOS Terminal.app Silver Aerogel profile — #3D3D3D dark grey background with white text
|
||||
- macOS window chrome with traffic light buttons and centered title bar
|
||||
- Per-character GSAP typing animation — command types out character by character
|
||||
- Output lines reveal sequentially after command executes
|
||||
- Cursor blinks three times at the end then holds steady
|
||||
- Self-contained HTML — no external assets, only GSAP CDN
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,21 +3,277 @@ title: "Code Snippet - Apple Terminal Solid Colors"
|
||||
description: "Apple Terminal Solid Colors profile: deep purple background with white text, per-character typing animation of a shell session."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-solid-colors.mp4" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-snippet-apple-terminal-solid-colors.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet - Apple Terminal Solid Colors preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-snippet-apple-terminal-solid-colors
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-solid-colors" />
|
||||
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-solid-colors.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-snippet-apple-terminal-solid-colors.html`}>
|
||||
|
||||
```html index.html
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Apple Terminal Solid Colors</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #4c0099;
|
||||
font-family: "SF Mono", Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
#scene {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(ellipse at center, #5a00b3 0%, #2e0060 100%);
|
||||
}
|
||||
|
||||
.terminal-window {
|
||||
width: 1400px;
|
||||
height: 700px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 30px 80px rgba(0, 0, 0, 0.7),
|
||||
0 10px 30px rgba(76, 0, 153, 0.4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
height: 38px;
|
||||
background: #3a0077;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #6600bb;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.traffic-lights span {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.traffic-lights .close {
|
||||
background: #ff5f57;
|
||||
border: 1px solid #e0443e;
|
||||
}
|
||||
|
||||
.traffic-lights .minimize {
|
||||
background: #ffbd2e;
|
||||
border: 1px solid #dfa123;
|
||||
}
|
||||
|
||||
.traffic-lights .fullscreen {
|
||||
background: #28c840;
|
||||
border: 1px solid #1aab29;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 13px;
|
||||
color: #ffffff;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.1px;
|
||||
}
|
||||
|
||||
.terminal-canvas {
|
||||
flex: 1;
|
||||
background: #4c0099;
|
||||
padding: 18px 20px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.output-lines {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
opacity: 0;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.output-line.bold {
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.output-line.dim {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.output-line.accent {
|
||||
color: #cc99ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #cc99ff;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.typed-command {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.cursor-block {
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
background: #ffffff;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="scene"
|
||||
data-composition-id="code-snippet-apple-terminal-solid-colors"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="terminal-window">
|
||||
<div class="titlebar">
|
||||
<div class="traffic-lights">
|
||||
<span class="close"></span>
|
||||
<span class="minimize"></span>
|
||||
<span class="fullscreen"></span>
|
||||
</div>
|
||||
<span class="window-title">bash — 80×24</span>
|
||||
</div>
|
||||
<div class="terminal-canvas">
|
||||
<div class="output-lines">
|
||||
<span class="output-line dim">Last login: Mon Jun 2 09:14:22 on ttys011</span>
|
||||
<span class="output-line">Initializing HyperFrames project...</span>
|
||||
<span class="output-line dim"> Creating project structure</span>
|
||||
<span class="output-line dim"> Copying template files</span>
|
||||
<span class="output-line dim"> Installing dependencies</span>
|
||||
<span class="output-line"> added 142 packages in 3.4s</span>
|
||||
<span class="output-line accent"> ✓ Project ready at ./my-video</span>
|
||||
<span class="output-line bold"> Run: cd my-video && npx hyperframes preview</span>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span
|
||||
><span class="cursor-block"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const command = "npx hyperframes init my-video";
|
||||
const typedEl = document.querySelector(".typed-command");
|
||||
const cursorEl = document.querySelector(".cursor-block");
|
||||
const outputLines = document.querySelectorAll(".output-line");
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.set(typedEl, { text: "" }, 0);
|
||||
|
||||
const charDuration = 1.5 / command.length;
|
||||
command.split("").forEach((char, i) => {
|
||||
tl.add(
|
||||
() => {
|
||||
typedEl.textContent = command.slice(0, i + 1);
|
||||
},
|
||||
0.5 + i * charDuration,
|
||||
);
|
||||
});
|
||||
|
||||
tl.set(cursorEl, { opacity: 0 }, 2.2);
|
||||
tl.set(outputLines[0], { opacity: 1 }, 2.3);
|
||||
|
||||
tl.add(() => {
|
||||
typedEl.textContent = "";
|
||||
}, 2.5);
|
||||
|
||||
outputLines.forEach((line, i) => {
|
||||
if (i === 0) return;
|
||||
tl.set(line, { opacity: 1 }, 2.8 + i * 0.1);
|
||||
});
|
||||
|
||||
tl.add(() => {
|
||||
const inputLine = document.querySelector(".input-line");
|
||||
const newPrompt = document.createElement("div");
|
||||
newPrompt.className = "input-line";
|
||||
newPrompt.innerHTML =
|
||||
'<span class="prompt">user@Mac ~ % </span><span class="typed-command"></span><span class="cursor-block" id="final-cursor"></span>';
|
||||
inputLine.style.display = "none";
|
||||
inputLine.parentNode.appendChild(newPrompt);
|
||||
}, 4.2);
|
||||
|
||||
const blinkTimes = [4.4, 4.8, 5.2, 5.6, 6.0, 6.4];
|
||||
blinkTimes.forEach((t, i) => {
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = i % 2 === 0 ? "0" : "1";
|
||||
}, t);
|
||||
});
|
||||
tl.add(() => {
|
||||
const fc = document.getElementById("final-cursor");
|
||||
if (fc) fc.style.opacity = "1";
|
||||
}, 6.8);
|
||||
|
||||
window.__timelines["code-snippet-apple-terminal-solid-colors"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-composition-id="code-snippet-apple-terminal-solid-colors"
|
||||
data-composition-src="compositions/code-snippet-apple-terminal-solid-colors.html"
|
||||
@@ -29,8 +285,14 @@ It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
## Features
|
||||
|
||||
- Faithful recreation of macOS Terminal.app Solid Colors profile — #4C0099 deep purple background with white text
|
||||
- macOS window chrome with traffic light buttons and centered title bar
|
||||
- Per-character GSAP typing animation — command types out character by character
|
||||
- Output lines reveal sequentially after command executes
|
||||
- Cursor blinks three times at the end then holds steady
|
||||
- Self-contained HTML — no external assets, only GSAP CDN
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,34 +3,697 @@ title: "Code Snippet Flight"
|
||||
description: "Discrete code snippets fly in from the side and assemble into a stacked program, staggered. Block-level FLIP."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-flight.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-flight.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-snippet-flight.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Snippet Flight preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-snippet-flight
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-snippet-flight" />
|
||||
|
||||
That writes one file: `compositions/code-snippet-flight.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 6 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-snippet-flight.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Code Snippet Flight</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-snippet-flight"
|
||||
data-composition-src="compositions/code-snippet-flight.html"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<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">pipeline.js</span> — Code Snippet Flight</span
|
||||
>
|
||||
</div>
|
||||
<div class="code-surface" id="surface"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__TOKENS = {
|
||||
flight: {
|
||||
lang: "js",
|
||||
theme: "github-dark",
|
||||
bg: "#24292e",
|
||||
fg: "#e1e4e8",
|
||||
states: [
|
||||
{
|
||||
code: "const x = parse(input)",
|
||||
tokens: [
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-0",
|
||||
content: "const",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-1",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-2",
|
||||
content: "x",
|
||||
color: "#79B8FF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-3",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-4",
|
||||
content: "=",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-5",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-6",
|
||||
content: "parse",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-7",
|
||||
content: "(input)",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-8",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "const y = transform(x)",
|
||||
tokens: [
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-0",
|
||||
content: "const",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-1",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "ulDeJnFQa2Vy-61y5zTTA7sNfq7d4aUawNMAxQ6sVgg-2",
|
||||
content: "y",
|
||||
color: "#79B8FF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-3",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-4",
|
||||
content: "=",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-5",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "ulDeJnFQa2Vy-61y5zTTA7sNfq7d4aUawNMAxQ6sVgg-6",
|
||||
content: "transform",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "ulDeJnFQa2Vy-61y5zTTA7sNfq7d4aUawNMAxQ6sVgg-7",
|
||||
content: "(x)",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "ulDeJnFQa2Vy-61y5zTTA7sNfq7d4aUawNMAxQ6sVgg-8",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "const z = render(y)",
|
||||
tokens: [
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-0",
|
||||
content: "const",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-1",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "wFuhzZNydG-3inU2s1h1gU8Kivya8CAP9gl9S4MBQj4-2",
|
||||
content: "z",
|
||||
color: "#79B8FF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-3",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-4",
|
||||
content: "=",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "qABRRXThYBzvlFJqemsh1aauYJqWMIPHhxkj-uoxex4-5",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "wFuhzZNydG-3inU2s1h1gU8Kivya8CAP9gl9S4MBQj4-6",
|
||||
content: "render",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "wFuhzZNydG-3inU2s1h1gU8Kivya8CAP9gl9S4MBQj4-7",
|
||||
content: "(y)",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "wFuhzZNydG-3inU2s1h1gU8Kivya8CAP9gl9S4MBQj4-8",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
window.__BLOCK = { id: "code-snippet-flight", effect: "flight", seq: "flight", duration: 6 };
|
||||
</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 flight(tl, surface, states, start) {
|
||||
var FLY = 0.95;
|
||||
var STAGGER = 0.26;
|
||||
var GAP = 20;
|
||||
var parts = makeScene(surface, false);
|
||||
parts.code.remove();
|
||||
var scene = parts.scene;
|
||||
var target = document.createElement("div");
|
||||
target.className = "flight-target";
|
||||
scene.appendChild(target);
|
||||
var W = surface.clientWidth;
|
||||
var H = surface.clientHeight;
|
||||
|
||||
var blocks = states.map(function (st) {
|
||||
var blk = document.createElement("div");
|
||||
blk.className = "flight-block";
|
||||
renderState(blk, st);
|
||||
scene.appendChild(blk);
|
||||
return blk;
|
||||
});
|
||||
var n = blocks.length;
|
||||
|
||||
// Pass 1: measure natural size, then make every chip the SAME width (symmetry).
|
||||
var maxW = 0;
|
||||
var chipH = 0;
|
||||
blocks.forEach(function (blk) {
|
||||
var r = blk.getBoundingClientRect();
|
||||
if (r.width > maxW) maxW = r.width;
|
||||
if (r.height > chipH) chipH = r.height;
|
||||
});
|
||||
blocks.forEach(function (blk) {
|
||||
blk.style.width = maxW + "px";
|
||||
});
|
||||
|
||||
// Pass 2: evenly-stacked, left-aligned, group centered in the surface.
|
||||
var stackH = n * chipH + (n - 1) * GAP;
|
||||
var destLeft = Math.round((W - maxW) / 2);
|
||||
var topY = Math.round((H - stackH) / 2);
|
||||
blocks.forEach(function (blk, i) {
|
||||
var destY = topY + i * (chipH + GAP);
|
||||
blk.style.left = destLeft + "px";
|
||||
blk.style.top = destY + "px";
|
||||
// fly straight in from the left to the shared left edge — clean, no skew
|
||||
var srcX = destLeft - W * 0.32;
|
||||
tl.fromTo(
|
||||
blk,
|
||||
{ x: srcX - destLeft, opacity: 0, scale: 0.96 },
|
||||
{ x: 0, opacity: 1, scale: 1, duration: FLY, ease: "power3.out" },
|
||||
start + FADE + i * STAGGER,
|
||||
);
|
||||
});
|
||||
|
||||
// Even-padded panel tightly wrapping the uniform stack.
|
||||
var padX = 30;
|
||||
var padY = 24;
|
||||
target.style.left = destLeft - padX + "px";
|
||||
target.style.top = topY - padY + "px";
|
||||
target.style.width = maxW + padX * 2 + "px";
|
||||
target.style.height = stackH + padY * 2 + "px";
|
||||
var landAt = start + FADE + (n - 1) * STAGGER + FLY;
|
||||
tl.set(scene, { opacity: 1 }, 0);
|
||||
tl.to(target, { opacity: 1, duration: 0.5, ease: "power1.out" }, landAt - 0.5);
|
||||
}
|
||||
|
||||
function buildEffect(tl, surface, spec) {
|
||||
var start = 0.45;
|
||||
var seq = T[spec.seq];
|
||||
if (spec.effect === "flight") flight(tl, surface, seq.states, start);
|
||||
}
|
||||
|
||||
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 });
|
||||
window.__timelines["code-snippet-flight"] = tl; // register synchronously
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", go);
|
||||
else go();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-snippet-flight" data-composition-src="compositions/code-snippet-flight.html" data-start="0" data-duration="6" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,34 +3,795 @@ title: "Code Typing"
|
||||
description: "Token-streamed typing reveal with a caret that tracks the frontier — deterministic, no CSS animation."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-typing.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-typing.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/code-typing.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Code Typing preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add code-typing
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add code-typing" />
|
||||
|
||||
That writes one file: `compositions/code-typing.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 5 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`code-typing.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Code Typing</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-typing"
|
||||
data-composition-src="compositions/code-typing.html"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<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 Typing</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-typing", effect: "typing", seq: "feature", 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 typing(tl, surface, state, start) {
|
||||
var PER = 0.028; // seconds per character — smooth, fast typewriter
|
||||
var parts = makeScene(surface, true);
|
||||
var code = parts.code;
|
||||
|
||||
// Per-CHARACTER render (granular reveal reads smoother than per-token pops).
|
||||
code.textContent = "";
|
||||
var line = document.createElement("div");
|
||||
line.className = "line";
|
||||
code.appendChild(line);
|
||||
var chars = [];
|
||||
for (var i = 0; i < state.tokens.length; i++) {
|
||||
var t = state.tokens[i];
|
||||
if (t.content === "\n") {
|
||||
line = document.createElement("div");
|
||||
line.className = "line";
|
||||
code.appendChild(line);
|
||||
continue;
|
||||
}
|
||||
for (var c = 0; c < t.content.length; c++) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "tok";
|
||||
span.textContent = t.content[c];
|
||||
span.style.color = t.color;
|
||||
if (t.fontStyle & 1) span.style.fontStyle = "italic";
|
||||
if (t.fontStyle & 2) span.style.fontWeight = "700";
|
||||
line.appendChild(span);
|
||||
chars.push({ el: span, ws: t.content[c] === " " });
|
||||
}
|
||||
}
|
||||
var last = code.lastChild;
|
||||
if (last && last.className === "line" && last.children.length === 0)
|
||||
code.removeChild(last);
|
||||
|
||||
fillGutter(parts.gutter, code.querySelectorAll(".line").length);
|
||||
var base = code.getBoundingClientRect();
|
||||
var caret = document.createElement("div");
|
||||
caret.className = "caret";
|
||||
code.appendChild(caret);
|
||||
|
||||
tl.set(parts.scene, { opacity: 0 }, 0);
|
||||
tl.to(parts.scene, { opacity: 1, duration: FADE, ease: "power1.out" }, start);
|
||||
|
||||
var t0 = start + FADE;
|
||||
if (chars.length) {
|
||||
var r0 = chars[0].el.getBoundingClientRect();
|
||||
tl.set(caret, { x: r0.left - base.left, y: r0.top - base.top, opacity: 1 }, 0);
|
||||
}
|
||||
chars.forEach(function (c, i) {
|
||||
var at = t0 + i * PER;
|
||||
tl.set(c.el, { opacity: 0 }, 0);
|
||||
// soft per-char fade (overlaps neighbors → smooth wave); whitespace appears instantly
|
||||
tl.to(c.el, { opacity: 1, duration: c.ws ? 0.01 : 0.12, ease: "power1.out" }, at);
|
||||
// caret glides at constant speed to each char's right edge (linear = no stop-start jitter)
|
||||
var r = c.el.getBoundingClientRect();
|
||||
tl.to(
|
||||
caret,
|
||||
{ x: r.right - base.left, y: r.top - base.top, duration: PER, ease: "none" },
|
||||
at,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function buildEffect(tl, surface, spec) {
|
||||
var start = 0.45;
|
||||
var seq = T[spec.seq];
|
||||
if (spec.effect === "typing") typing(tl, surface, seq.states[0], start);
|
||||
}
|
||||
|
||||
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-typing"] = 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>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-typing" data-composition-src="compositions/code-typing.html" data-start="0" data-duration="5" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
---
|
||||
title: "Cosmic Orb"
|
||||
description: "An iridescent soap-bubble planet: a slowly spinning sphere of nebula bands, dust lanes and twinkling stars wrapped in a refracting glass limb with chromatic aberration at the edge."
|
||||
---
|
||||
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
|
||||
|
||||
<VariablesExplorer
|
||||
previewSrc="/public/catalog/preview/blocks/cosmic-orb.html"
|
||||
compositionId="cosmic-orb"
|
||||
compositionSrc="compositions/cosmic-orb.html"
|
||||
variables={[{"id":"hue","type":"number","label":"Nebula hue","default":268,"min":0,"max":360,"step":1,"unit":"deg"},{"id":"accent","type":"number","label":"Accent hue","default":196,"min":0,"max":360,"step":1,"unit":"deg"},{"id":"spin","type":"number","label":"Spin rate","default":0.035,"min":0,"max":0.5,"step":0.005,"unit":"turns/s"},{"id":"stars","type":"number","label":"Star density","default":1,"min":0,"max":2,"step":0.05},{"id":"glow","type":"number","label":"Glow intensity","default":1,"min":0,"max":2,"step":0.05},{"id":"size","type":"number","label":"Orb size","default":0.86,"min":0.2,"max":1.2,"step":0.01},{"id":"pulse","type":"number","label":"Beat pulse","default":0,"min":0,"max":1,"step":0.01},{"id":"pulseEnvelope","type":"string","label":"Beat pulse envelope (comma-separated 0-1 samples)","default":"","placeholder":"0,0.2,1,0.4,0"},{"id":"backdrop","type":"color","label":"Backdrop","default":"#04040a"}]}
|
||||
>
|
||||
|
||||
```html cosmic-orb.html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Cosmic Orb</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
#co-root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#co-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #04040a;
|
||||
}
|
||||
#co-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="co-root"
|
||||
data-composition-id="cosmic-orb"
|
||||
data-root="true"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="10"
|
||||
data-composition-variables='[
|
||||
{"id":"hue","type":"number","label":"Nebula hue","default":268,"min":0,"max":360,"step":1,"unit":"deg"},
|
||||
{"id":"accent","type":"number","label":"Accent hue","default":196,"min":0,"max":360,"step":1,"unit":"deg"},
|
||||
{"id":"spin","type":"number","label":"Spin rate","default":0.035,"min":0,"max":0.5,"step":0.005,"unit":"turns/s"},
|
||||
{"id":"stars","type":"number","label":"Star density","default":1,"min":0,"max":2,"step":0.05},
|
||||
{"id":"glow","type":"number","label":"Glow intensity","default":1,"min":0,"max":2,"step":0.05},
|
||||
{"id":"size","type":"number","label":"Orb size","default":0.86,"min":0.2,"max":1.2,"step":0.01},
|
||||
{"id":"pulse","type":"number","label":"Beat pulse","default":0,"min":0,"max":1,"step":0.01},
|
||||
{"id":"pulseEnvelope","type":"string","label":"Beat pulse envelope (comma-separated 0-1 samples)","default":"","placeholder":"0,0.2,1,0.4,0"},
|
||||
{"id":"backdrop","type":"color","label":"Backdrop","default":"#04040a"}
|
||||
]'
|
||||
>
|
||||
<div id="co-backdrop"></div>
|
||||
<canvas id="co-canvas" width="1920" height="1080"></canvas>
|
||||
|
||||
<!-- Driver clip: gives HyperFrames a timed element to own on track 0. -->
|
||||
<div
|
||||
id="co-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="10"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var DUR = 10;
|
||||
var W = 1920;
|
||||
var H = 1080;
|
||||
|
||||
var ROOT = document.getElementById("co-root");
|
||||
var CS = getComputedStyle(ROOT);
|
||||
|
||||
// `data-composition-variables` is the single owner of every declared
|
||||
// default — parse it rather than repeating each default a second time
|
||||
// in JS. `window.__hyperframes` is NOT guaranteed to exist yet when this
|
||||
// inline script runs, so it can only ever be an override, never the
|
||||
// source. Precedence: host CSS custom property, runtime variable bag,
|
||||
// declared default.
|
||||
var DECL = {};
|
||||
JSON.parse(ROOT.getAttribute("data-composition-variables") || "[]").forEach(function (v) {
|
||||
DECL[v.id] = v.default;
|
||||
});
|
||||
var HF = window.__hyperframes;
|
||||
var V = (HF && HF.getVariables && HF.getVariables()) || {};
|
||||
|
||||
function raw(id) {
|
||||
var css = CS.getPropertyValue("--" + id.toLowerCase()).trim();
|
||||
if (css !== "") return css;
|
||||
if (V[id] !== undefined && V[id] !== "") return V[id];
|
||||
return DECL[id];
|
||||
}
|
||||
function num(id) {
|
||||
var n = parseFloat(raw(id));
|
||||
return isFinite(n) ? n : 0;
|
||||
}
|
||||
function str(id) {
|
||||
var s = raw(id);
|
||||
return typeof s === "string" ? s.trim() : "";
|
||||
}
|
||||
|
||||
var HUE = num("hue");
|
||||
var ACCENT = num("accent");
|
||||
var SPIN = num("spin");
|
||||
var STARS = num("stars");
|
||||
var GLOW = num("glow");
|
||||
var SIZE = num("size");
|
||||
var PULSE = num("pulse");
|
||||
var BACKDROP = str("backdrop");
|
||||
|
||||
document.getElementById("co-backdrop").style.background = BACKDROP;
|
||||
|
||||
// Beat hook. A precomputed envelope (comma-separated 0-1 samples spread
|
||||
// evenly across the clip) is sampled closed-form at time t — no audio
|
||||
// analysis, no accumulator. Empty envelope => the flat `pulse` value.
|
||||
var ENV = str("pulseEnvelope")
|
||||
.split(",")
|
||||
.map(function (s) {
|
||||
return parseFloat(s);
|
||||
})
|
||||
.filter(function (n) {
|
||||
return isFinite(n);
|
||||
});
|
||||
|
||||
function pulseAt(t) {
|
||||
if (ENV.length === 0) return PULSE;
|
||||
if (ENV.length === 1) return ENV[0];
|
||||
var x = Math.min(1, Math.max(0, t / DUR)) * (ENV.length - 1);
|
||||
var i = Math.floor(x);
|
||||
var j = Math.min(i + 1, ENV.length - 1);
|
||||
return ENV[i] + (ENV[j] - ENV[i]) * (x - i);
|
||||
}
|
||||
|
||||
var canvas = document.getElementById("co-canvas");
|
||||
var gl =
|
||||
canvas.getContext("webgl", {
|
||||
alpha: true,
|
||||
antialias: false,
|
||||
depth: false,
|
||||
stencil: false,
|
||||
preserveDrawingBuffer: true,
|
||||
powerPreference: "high-performance",
|
||||
}) ||
|
||||
canvas.getContext("experimental-webgl", {
|
||||
alpha: true,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
|
||||
var VERT = [
|
||||
"attribute vec2 aPos;",
|
||||
"void main() { gl_Position = vec4(aPos, 0.0, 1.0); }",
|
||||
].join("\n");
|
||||
|
||||
var FRAG = [
|
||||
"precision highp float;",
|
||||
"uniform vec2 uRes;",
|
||||
"uniform float uTime;",
|
||||
"uniform float uRadius;",
|
||||
"uniform float uSpin;",
|
||||
"uniform float uHue;",
|
||||
"uniform float uAccent;",
|
||||
"uniform float uStars;",
|
||||
"uniform float uGlow;",
|
||||
"uniform float uPulse;",
|
||||
"const float TAU = 6.28318531;",
|
||||
"",
|
||||
"// One cheap hash for every random-looking thing in here.",
|
||||
"float h1(float x) { return fract(sin(x * 127.1) * 43758.5453); }",
|
||||
"float h2(vec2 p) { return h1(dot(p, vec2(1.0, 157.31))); }",
|
||||
"",
|
||||
"vec3 hue2rgb(float h) {",
|
||||
" return clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);",
|
||||
"}",
|
||||
"",
|
||||
"vec3 rotX(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(p.x, c * p.y - s * p.z, s * p.y + c * p.z); }",
|
||||
"vec3 rotY(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(c * p.x + s * p.z, p.y, -s * p.x + c * p.z); }",
|
||||
"vec3 rotZ(vec3 p, float a) { float c = cos(a), s = sin(a); return vec3(c * p.x - s * p.y, s * p.x + c * p.y, p.z); }",
|
||||
"",
|
||||
"vec3 safeDir(vec3 v) {",
|
||||
" float l = dot(v, v);",
|
||||
" if (l < 1e-6) return vec3(0.0, 0.0, -1.0);",
|
||||
" return v * inversesqrt(l);",
|
||||
"}",
|
||||
"",
|
||||
"// Fixed axial tilt, then spin about the tilted polar axis.",
|
||||
"vec3 toBody(vec3 n, float spin) { return rotY(rotZ(rotX(n, -0.34), 0.42), -spin); }",
|
||||
"vec2 lonlat(vec3 d) { return vec2(atan(d.x, d.z), asin(clamp(d.y, -1.0, 1.0))); }",
|
||||
"",
|
||||
"float dens(float base) { return clamp(1.0 - (1.0 - base) * uStars, 0.0, 1.0); }",
|
||||
"",
|
||||
"// One star size-class: cell grid, seeded jitter, soft round core.",
|
||||
"float starField(vec2 q, float freq, float base, float rad) {",
|
||||
" vec2 g = q * freq;",
|
||||
" vec2 id = floor(g);",
|
||||
" float h = h2(id);",
|
||||
" if (h < dens(base)) return 0.0;",
|
||||
" vec2 c = vec2(h2(id + vec2(3.7, 1.3)), h2(id + vec2(9.1, 5.5)));",
|
||||
" float d = length(fract(g) - c);",
|
||||
" float core = smoothstep(rad, 0.0, d);",
|
||||
" // Twinkle: a per-star phase driven straight off uTime, no accumulator.",
|
||||
" float tw = 0.62 + 0.38 * sin(uTime * 2.4 + h * 53.7);",
|
||||
" return core * core * (0.45 + 0.55 * h1(h * 91.7)) * tw;",
|
||||
"}",
|
||||
"",
|
||||
"// 4-point diffraction glare, only worth paying for on the brightest class.",
|
||||
"float starGlare(vec2 q, float freq, float base) {",
|
||||
" vec2 g = q * freq;",
|
||||
" vec2 id = floor(g);",
|
||||
" float h = h2(id);",
|
||||
" if (h < dens(base)) return 0.0;",
|
||||
" vec2 c = vec2(h2(id + vec2(3.7, 1.3)), h2(id + vec2(9.1, 5.5)));",
|
||||
" vec2 dv = abs(fract(g) - c);",
|
||||
" float hx = exp(-dv.x * 70.0) * exp(-dv.y * 9.0);",
|
||||
" float hy = exp(-dv.y * 70.0) * exp(-dv.x * 9.0);",
|
||||
" return max(hx, hy) * 0.45;",
|
||||
"}",
|
||||
"",
|
||||
"// Three size-classes at ~6 / 11 / 19 cell frequency.",
|
||||
"float starsAll(vec2 q, float gain) {",
|
||||
" float s = starField(q, 6.0, 0.958, 0.20);",
|
||||
" s += starGlare(q, 6.0, 0.958);",
|
||||
" s += starField(q, 11.0, 0.920, 0.18) * 0.95;",
|
||||
" s += starField(q, 19.0, 0.865, 0.17) * 0.75;",
|
||||
" return s * gain;",
|
||||
"}",
|
||||
"",
|
||||
"vec3 nebula(vec2 q) {",
|
||||
" // Two sin-field turbulence layers.",
|
||||
" float t1 = sin(q.x * 3.1 + sin(q.y * 2.7) * 1.9) * sin(q.y * 4.3 + sin(q.x * 1.7) * 2.3);",
|
||||
" float t2 = sin(q.x * 7.9 + sin(q.y * 5.1) * 1.3) * sin(q.y * 9.7 + sin(q.x * 4.4) * 1.1);",
|
||||
" float turb = t1 * 0.66 + t2 * 0.34;",
|
||||
"",
|
||||
" // Galaxy bands, warped by the turbulence.",
|
||||
" float band = sin(q.y * 3.2 + turb * 1.7 + q.x * 0.55);",
|
||||
" float bands = pow(max(0.0, band * 0.5 + 0.5), 3.2);",
|
||||
"",
|
||||
" // Dust lanes: broad darkened swathes, not a thin curve.",
|
||||
" float lane = sin(q.y * 2.9 + q.x * 1.3 + t1 * 1.6);",
|
||||
" float dust = 0.22 + 0.78 * smoothstep(-0.45, 0.75, lane);",
|
||||
"",
|
||||
" // Core bulge.",
|
||||
" float dc = length(vec2((q.x - 0.55) * 0.5, (q.y + 0.12) * 1.1));",
|
||||
" float core = exp(-dc * dc * 2.6);",
|
||||
"",
|
||||
" // Two pocket glows.",
|
||||
" float dA = length(vec2((q.x + 1.95) * 0.62, (q.y - 0.52) * 1.45));",
|
||||
" float dB = length(vec2((q.x - 2.35) * 0.55, (q.y + 0.78) * 1.30));",
|
||||
" float pA = exp(-dA * dA * 4.0);",
|
||||
" float pB = exp(-dB * dB * 5.0);",
|
||||
"",
|
||||
" vec3 cNeb = hue2rgb(uHue / 360.0);",
|
||||
" vec3 cAcc = hue2rgb(uAccent / 360.0);",
|
||||
" vec3 cWarm = hue2rgb(uHue / 360.0 + 0.07);",
|
||||
"",
|
||||
" vec3 col = mix(cNeb, cWarm, 0.5 + 0.5 * turb) * bands * dust * 0.17;",
|
||||
" col += mix(cNeb, vec3(1.0), 0.50) * core * dust * 0.13;",
|
||||
" col += mix(cAcc, vec3(1.0), 0.60) * pA * 0.30 * uGlow;",
|
||||
" col += mix(cNeb, vec3(1.0), 0.50) * pB * 0.24 * uGlow;",
|
||||
" return col;",
|
||||
"}",
|
||||
"",
|
||||
"void main() {",
|
||||
" vec2 p = (gl_FragCoord.xy - uRes * 0.5) / uRadius;",
|
||||
" float r = length(p);",
|
||||
" float aa = 1.4 / uRadius;",
|
||||
" float spin = TAU * uSpin * uTime;",
|
||||
"",
|
||||
" vec3 cNeb = hue2rgb(uHue / 360.0);",
|
||||
" vec3 cAcc = hue2rgb(uAccent / 360.0);",
|
||||
"",
|
||||
" float mask = smoothstep(1.0 + aa, 1.0 - aa, r);",
|
||||
" float halo = exp(-max(0.0, r - 1.0) * 13.0) * (1.0 - mask);",
|
||||
"",
|
||||
" vec3 body = vec3(0.0);",
|
||||
" if (mask > 0.001) {",
|
||||
" float rc = min(r, 1.0);",
|
||||
" // Analytic sphere: the normal falls straight out of z = sqrt(1 - r^2).",
|
||||
" float z = sqrt(max(0.0, 1.0 - rc * rc));",
|
||||
" vec3 N = vec3(p.x, p.y, z);",
|
||||
" vec2 q = lonlat(toBody(safeDir(N), spin));",
|
||||
"",
|
||||
" float gain = 0.9 + 2.6 * uPulse;",
|
||||
" body = nebula(q);",
|
||||
" body += vec3(1.0) * starsAll(q, gain) * (0.95 + 0.15 * uGlow);",
|
||||
"",
|
||||
" // Limb darkening.",
|
||||
" body *= mix(0.32, 1.0, pow(z, 0.5));",
|
||||
"",
|
||||
" // Refract-sampled back layer, split per channel: the chromatic",
|
||||
" // aberration that reads as glass at the limb.",
|
||||
" vec3 I = vec3(0.0, 0.0, -1.0);",
|
||||
" vec3 dR = safeDir(refract(I, N, 1.0 / 1.09));",
|
||||
" vec3 dG = safeDir(refract(I, N, 1.0 / 1.13));",
|
||||
" vec3 dB = safeDir(refract(I, N, 1.0 / 1.17));",
|
||||
" vec2 qR = lonlat(toBody(dR, spin));",
|
||||
" vec2 qG = lonlat(toBody(dG, spin));",
|
||||
" vec2 qB = lonlat(toBody(dB, spin));",
|
||||
" vec3 back = vec3(starsAll(qR, gain), starsAll(qG, gain), starsAll(qB, gain));",
|
||||
" float fres = pow(1.0 - z, 1.8);",
|
||||
" body += back * fres * 2.2;",
|
||||
" body += nebula(qG) * fres * 0.8;",
|
||||
"",
|
||||
" // Aurora sheen riding the glass shell.",
|
||||
" float ang = atan(p.y, p.x);",
|
||||
" float sheen = smoothstep(0.72, 0.99, r) * smoothstep(1.0, 0.90, r);",
|
||||
" float w = 0.5 + 0.5 * sin(ang * 2.0 + spin * 1.6);",
|
||||
" body += (mix(cNeb, cAcc, w) * 0.55 + vec3(0.16)) * sheen * 0.30 * uGlow;",
|
||||
"",
|
||||
" // Three speculars, fixed in screen space: the key light does not spin.",
|
||||
" vec2 a1 = p - vec2(-0.34, 0.62);",
|
||||
" vec2 a2 = p - vec2(0.58, 0.60);",
|
||||
" vec2 a3 = p - vec2(0.24, -0.80);",
|
||||
" float sp = exp(-dot(a1, a1) * 30.0) * 0.30;",
|
||||
" sp += exp(-dot(a2, a2) * 150.0) * 0.80;",
|
||||
" sp += exp(-dot(a3, a3) * 70.0) * 0.30;",
|
||||
" body += vec3(1.0) * sp * (0.30 + 0.70 * pow(1.0 - z, 1.5)) * uGlow;",
|
||||
"",
|
||||
" // Fresnel void glow hugging the inside of the rim.",
|
||||
" body += mix(cAcc, vec3(1.0), 0.4) * pow(1.0 - z, 6.0) * 0.30 * uGlow;",
|
||||
" }",
|
||||
"",
|
||||
" vec3 haloCol = mix(cNeb, cAcc, 0.35);",
|
||||
" vec3 outCol = body * mask + haloCol * halo * 0.22 * uGlow;",
|
||||
" float outA = clamp(mask + halo * 0.16 * uGlow, 0.0, 1.0);",
|
||||
" gl_FragColor = vec4(clamp(outCol, 0.0, 4.0), outA);",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
var uni = {};
|
||||
var ready = false;
|
||||
|
||||
function compile(type, src) {
|
||||
var sh = gl.createShader(type);
|
||||
gl.shaderSource(sh, src);
|
||||
gl.compileShader(sh);
|
||||
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
||||
throw new Error("cosmic-orb shader: " + gl.getShaderInfoLog(sh));
|
||||
}
|
||||
return sh;
|
||||
}
|
||||
|
||||
if (gl) {
|
||||
var prog = gl.createProgram();
|
||||
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
|
||||
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
|
||||
gl.linkProgram(prog);
|
||||
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
||||
throw new Error("cosmic-orb link: " + gl.getProgramInfoLog(prog));
|
||||
}
|
||||
gl.useProgram(prog);
|
||||
|
||||
var buf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
||||
var loc = gl.getAttribLocation(prog, "aPos");
|
||||
gl.enableVertexAttribArray(loc);
|
||||
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
[
|
||||
"uRes",
|
||||
"uTime",
|
||||
"uRadius",
|
||||
"uSpin",
|
||||
"uHue",
|
||||
"uAccent",
|
||||
"uStars",
|
||||
"uGlow",
|
||||
"uPulse",
|
||||
].forEach(function (n) {
|
||||
uni[n] = gl.getUniformLocation(prog, n);
|
||||
});
|
||||
|
||||
gl.viewport(0, 0, W, H);
|
||||
gl.uniform2f(uni.uRes, W, H);
|
||||
gl.uniform1f(uni.uRadius, (SIZE * H) / 2);
|
||||
gl.uniform1f(uni.uSpin, SPIN);
|
||||
gl.uniform1f(uni.uHue, HUE);
|
||||
gl.uniform1f(uni.uAccent, ACCENT);
|
||||
gl.uniform1f(uni.uStars, STARS);
|
||||
gl.uniform1f(uni.uGlow, GLOW);
|
||||
ready = true;
|
||||
}
|
||||
|
||||
// Every frame is computed from t alone: spin is spin_rate * t, the
|
||||
// pulse is sampled from the envelope at t. No accumulators, no clocks.
|
||||
function draw(t) {
|
||||
if (!ready) return;
|
||||
gl.uniform1f(uni.uTime, t);
|
||||
gl.uniform1f(uni.uPulse, pulseAt(t));
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||
gl.flush();
|
||||
}
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
// The canvas is repainted from a property SETTER, not from onUpdate:
|
||||
// gsap's seek(t) suppresses events by default, so an onUpdate callback
|
||||
// silently never fires on a scrub and the canvas freezes on frame 0.
|
||||
// Tweened values are always written during render, suppressed or not,
|
||||
// so this fires on every seek — and hands us the frame time directly.
|
||||
var driver = { _t: 0 };
|
||||
Object.defineProperty(driver, "t", {
|
||||
get: function () {
|
||||
return this._t;
|
||||
},
|
||||
set: function (v) {
|
||||
this._t = v;
|
||||
draw(v);
|
||||
},
|
||||
});
|
||||
tl.to(driver, { t: DUR, duration: DUR, ease: "none" }, 0);
|
||||
window.__timelines["cosmic-orb"] = tl;
|
||||
|
||||
draw(0);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</VariablesExplorer>
|
||||
|
||||
## Install
|
||||
|
||||
<InstallCommand command="npx hyperframes add cosmic-orb" />
|
||||
|
||||
That writes one file: `compositions/cosmic-orb.html`.
|
||||
|
||||
## Add it to your video
|
||||
|
||||
It runs for 10 seconds at 1920×1080. Paste this into your composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="cosmic-orb"
|
||||
data-composition-src="compositions/cosmic-orb.html"
|
||||
data-start="0"
|
||||
data-duration="10"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Change how it looks
|
||||
|
||||
Set these CSS variables on the block:
|
||||
|
||||
- `--hue` — Nebula hue. Defaults to `268`.
|
||||
- `--accent` — Accent hue. Defaults to `196`.
|
||||
- `--spin` — Spin rate (turns per second). Defaults to `0.035`.
|
||||
- `--stars` — Star density. Defaults to `1`.
|
||||
- `--glow` — Glow intensity. Defaults to `1`.
|
||||
- `--size` — Orb size (fraction of frame height). Defaults to `0.86`.
|
||||
- `--pulse` — Beat pulse (0-1, flat value when no envelope is set). Defaults to `0`.
|
||||
- `--pulseenvelope` — Beat pulse envelope: comma-separated 0-1 samples spread evenly across the clip. Defaults to ``.
|
||||
- `--backdrop` — Backdrop. Defaults to `#04040a`.
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `webgl` `shader` `background` `showcase` `3d`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
||||
- [Build a richer composition](/go-further)
|
||||
@@ -3,34 +3,409 @@ title: "Cross Warp Morph"
|
||||
description: "Shader transition with cross-warped morphing"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/cross-warp-morph.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/cross-warp-morph.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/cross-warp-morph.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Cross Warp Morph preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add cross-warp-morph
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add cross-warp-morph" />
|
||||
|
||||
That writes one file: `compositions/cross-warp-morph.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`cross-warp-morph.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="cross-warp-morph"
|
||||
data-composition-src="compositions/cross-warp-morph.html"
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<style>
|
||||
[data-composition-id="main"] {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #283618;
|
||||
}
|
||||
|
||||
/* Blueprint grid */
|
||||
.bp-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.bp-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.bp-right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 720px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 60px 60px 60px 40px;
|
||||
}
|
||||
.bp-divider {
|
||||
position: absolute;
|
||||
left: 1200px;
|
||||
top: 60px;
|
||||
bottom: 60px;
|
||||
width: 1px;
|
||||
background: #a7c957;
|
||||
opacity: 0.15;
|
||||
}
|
||||
.bp-number {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4em;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 60px;
|
||||
color: #a7c957;
|
||||
}
|
||||
|
||||
/* Right panel typography */
|
||||
.bp-name {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 38px;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 20px;
|
||||
color: #a7c957;
|
||||
}
|
||||
.bp-plabel {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
color: #a7c957;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.bp-prompt {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
padding: 16px 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
color: #a7c957;
|
||||
background: rgba(167, 201, 87, 0.06);
|
||||
border: 1px solid rgba(167, 201, 87, 0.12);
|
||||
}
|
||||
.bp-desc {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
color: #8aaa9a;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* GL scenes (hidden, just for texture capture) */
|
||||
.scene {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.scene-label {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 140px;
|
||||
letter-spacing: 0.06em;
|
||||
opacity: 0.12;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="bp-grid"></div>
|
||||
<div class="bp-divider"></div>
|
||||
<div class="bp-number">13 / 14</div>
|
||||
|
||||
<!-- GL demo scenes (captured as textures, then hidden) -->
|
||||
<div id="s1" class="scene" style="background: #283618">
|
||||
<div class="scene-label" style="color: #a7c957">SCENE A</div>
|
||||
</div>
|
||||
<div id="s2" class="scene" style="background: #a7c957; opacity: 0">
|
||||
<div class="scene-label" style="color: #283618">SCENE B</div>
|
||||
</div>
|
||||
|
||||
<!-- GL canvas (left panel only) -->
|
||||
<canvas
|
||||
id="gl-canvas"
|
||||
width="1200"
|
||||
height="1080"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
"
|
||||
>
|
||||
</canvas>
|
||||
|
||||
<!-- Right panel metadata -->
|
||||
<div class="bp-right">
|
||||
<div class="bp-name">Cross-Warp Morph</div>
|
||||
<div class="bp-plabel">Prompt</div>
|
||||
<div class="bp-prompt">"use cross-warp morph shader transition"</div>
|
||||
<div class="bp-desc">
|
||||
Both scenes displace along a shared FBM noise field in opposite directions. Noise-driven
|
||||
blend boundary.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var glCanvas = document.getElementById("gl-canvas");
|
||||
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
|
||||
if (!gl) {
|
||||
console.warn("WebGL not available");
|
||||
window.__timelines["main"] = gsap.timeline({ paused: true });
|
||||
} else {
|
||||
gl.viewport(0, 0, 1200, 1080);
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
||||
var sceneTextures = {};
|
||||
|
||||
function captureScene(sceneId) {
|
||||
var scene = document.getElementById(sceneId);
|
||||
var origOpacity = scene.style.opacity;
|
||||
var origZ = scene.style.zIndex;
|
||||
scene.style.opacity = "1";
|
||||
scene.style.zIndex = "999";
|
||||
scene.offsetHeight;
|
||||
var c = document.createElement("canvas");
|
||||
c.width = 1200;
|
||||
c.height = 1080;
|
||||
var ctx = c.getContext("2d");
|
||||
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
|
||||
ctx.fillRect(0, 0, 1200, 1080);
|
||||
var sr = scene.getBoundingClientRect();
|
||||
var els = scene.querySelectorAll("*");
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
var el = els[i],
|
||||
cs = window.getComputedStyle(el);
|
||||
if (cs.display === "none" || cs.visibility === "hidden") continue;
|
||||
var r = el.getBoundingClientRect();
|
||||
if (r.width < 1 || r.height < 1) continue;
|
||||
var x = r.left - sr.left,
|
||||
y = r.top - sr.top,
|
||||
w = r.width,
|
||||
h = r.height;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
|
||||
var bg = cs.backgroundColor;
|
||||
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
}
|
||||
var hasChildEls = el.querySelector("div,span,img,video");
|
||||
var text = "";
|
||||
for (var j = 0; j < el.childNodes.length; j++)
|
||||
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
|
||||
text = text.trim();
|
||||
if (text && !hasChildEls) {
|
||||
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
|
||||
ctx.fillStyle = cs.color;
|
||||
if (cs.letterSpacing && cs.letterSpacing !== "normal")
|
||||
ctx.letterSpacing = cs.letterSpacing;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(text, x + w / 2, y + h / 2);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
scene.style.opacity = origOpacity;
|
||||
scene.style.zIndex = origZ;
|
||||
var tex = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
|
||||
sceneTextures[sceneId] = tex;
|
||||
}
|
||||
|
||||
var vertSrc =
|
||||
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
|
||||
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
|
||||
var quadBuf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.bufferData(
|
||||
gl.ARRAY_BUFFER,
|
||||
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
||||
gl.STATIC_DRAW,
|
||||
);
|
||||
function compileShader(src, type) {
|
||||
var s = gl.createShader(type);
|
||||
gl.shaderSource(s, src);
|
||||
gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
|
||||
console.error("Shader:", gl.getShaderInfoLog(s));
|
||||
return s;
|
||||
}
|
||||
function mkProg(fragSrc) {
|
||||
var p = gl.createProgram();
|
||||
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
|
||||
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
|
||||
gl.linkProgram(p);
|
||||
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
|
||||
console.error("Link:", gl.getProgramInfoLog(p));
|
||||
return p;
|
||||
}
|
||||
|
||||
var H =
|
||||
"precision mediump float;" +
|
||||
"varying vec2 v_uv;" +
|
||||
"uniform sampler2D u_from, u_to;" +
|
||||
"uniform float u_progress;" +
|
||||
"uniform vec2 u_resolution;\n";
|
||||
|
||||
var NQ =
|
||||
"float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}" +
|
||||
"float vnoise(vec2 p){vec2 i=floor(p),f=fract(p);" +
|
||||
"f=f*f*f*(f*(f*6.-15.)+10.);" +
|
||||
"return mix(mix(hash(i),hash(i+vec2(1,0)),f.x)," +
|
||||
"mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);}" +
|
||||
"float fbm(vec2 p){float v=0.,a=.5;" +
|
||||
"mat2 R=mat2(.8,.6,-.6,.8);" +
|
||||
"for(int i=0;i<5;i++){v+=a*vnoise(p);p=R*p*2.02;a*=.5;}return v;}";
|
||||
|
||||
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
|
||||
var progTrans = mkProg(
|
||||
H +
|
||||
NQ +
|
||||
"void main(){vec2 disp=vec2(fbm(v_uv*3.),fbm(v_uv*3.+vec2(7.3,3.7)))-.5;vec2 fromUv=clamp(v_uv+disp*u_progress*.5,0.,1.);vec2 toUv=clamp(v_uv-disp*(1.-u_progress)*.5,0.,1.);vec4 A=texture2D(u_from,fromUv);vec4 B=texture2D(u_to,toUv);float n=fbm(v_uv*4.+vec2(3.1,1.7));float blend=smoothstep(.4,.6,n+u_progress*1.2-.6);gl_FragColor=mix(A,B,blend);}",
|
||||
);
|
||||
|
||||
function renderShader(prog, texFrom, texTo, progress) {
|
||||
gl.useProgram(prog);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texFrom);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texTo);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
|
||||
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
|
||||
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
|
||||
var pos = gl.getAttribLocation(prog, "a_pos");
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.enableVertexAttribArray(pos);
|
||||
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
function easeInOut(p) {
|
||||
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
|
||||
}
|
||||
|
||||
captureScene("s1");
|
||||
captureScene("s2");
|
||||
glCanvas.style.display = "block";
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
document.querySelectorAll(".scene").forEach(function (s) {
|
||||
s.style.opacity = "0";
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({
|
||||
paused: true,
|
||||
onUpdate: function () {
|
||||
var t = tl.time();
|
||||
if (t >= 1.0 && t < 3.0) {
|
||||
renderShader(
|
||||
progTrans,
|
||||
sceneTextures["s1"],
|
||||
sceneTextures["s2"],
|
||||
easeInOut((t - 1.0) / 2.0),
|
||||
);
|
||||
} else if (t >= 3.0) {
|
||||
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
|
||||
} else {
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
|
||||
window.__timelines["main"] = tl;
|
||||
} // end if (gl)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="cross-warp-morph" data-composition-src="compositions/cross-warp-morph.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,35 +3,16 @@ title: "Data Chart"
|
||||
description: "Animated bar + line chart with staggered reveal, NYT-style typography, and value labels"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/data-chart.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/data-chart.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/data-chart.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Data Chart preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add data-chart
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add data-chart" />
|
||||
|
||||
That writes one file: `compositions/data-chart.html`.
|
||||
|
||||
## Add it to your video
|
||||
|
||||
It runs for 15 seconds at 1920×1080. Paste this into your composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="data-chart"
|
||||
data-composition-src="compositions/data-chart.html"
|
||||
data-start="0"
|
||||
data-duration="15"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Change the colors
|
||||
|
||||
Set these CSS variables on the block:
|
||||
@@ -39,6 +20,480 @@ Set these CSS variables on the block:
|
||||
- `--bg-color` — Background. Defaults to `#faf9f6`.
|
||||
- `--text-color` — Text color. Defaults to `#333333`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`data-chart.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Data Chart</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:wght@400;700&family=Libre+Franklin:wght@300;400;600&display=block"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<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 {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background-color: #faf9f6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="data-chart"
|
||||
data-composition-id="data-chart"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="15"
|
||||
>
|
||||
<div class="chart-container">
|
||||
<!-- Header Section -->
|
||||
<div class="header">
|
||||
<h1 class="headline">Monthly Revenue vs. Conversion Rate</h1>
|
||||
<p class="subtitle">Jan–Jun 2024, in thousands</p>
|
||||
<div class="key">
|
||||
<div class="key-item">
|
||||
<div class="key-box revenue-box"></div>
|
||||
<span class="key-text">Revenue</span>
|
||||
</div>
|
||||
<div class="key-item">
|
||||
<div class="key-line conversion-line"></div>
|
||||
<span class="key-text">Conversion Rate</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SVG Chart Area -->
|
||||
<svg class="chart-svg" viewBox="0 0 1600 700" preserveAspectRatio="xMidYMid meet">
|
||||
<!-- Gridlines Group -->
|
||||
<g class="gridlines"></g>
|
||||
|
||||
<!-- X-Axis Labels -->
|
||||
<g class="x-axis-labels"></g>
|
||||
|
||||
<!-- Bars Group (Revenue) -->
|
||||
<g class="bars-group"></g>
|
||||
|
||||
<!-- Bar Labels Group -->
|
||||
<g class="bar-labels-group"></g>
|
||||
|
||||
<!-- Line Group (Conversion Rate) -->
|
||||
<g class="line-group">
|
||||
<path class="conversion-path" fill="none" stroke="#326FA8" stroke-width="1.5" />
|
||||
<circle class="line-dot" r="5" fill="#326FA8" style="opacity: 0" />
|
||||
</g>
|
||||
|
||||
<!-- Line Labels Group -->
|
||||
<g class="line-labels-group"></g>
|
||||
|
||||
<!-- Baseline -->
|
||||
<line x1="100" y1="600" x2="1500" y2="600" stroke="black" stroke-width="1" />
|
||||
</svg>
|
||||
|
||||
<!-- Source Line -->
|
||||
<div class="source">Source: Internal analytics</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
[data-composition-id="data-chart"] {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background-color: #faf9f6;
|
||||
font-family: "Libre Franklin", sans-serif;
|
||||
color: #333;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .chart-container {
|
||||
width: 1600px;
|
||||
height: 900px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
transform: scale(1.15);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .header {
|
||||
margin-bottom: 40px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .headline {
|
||||
font-family: "Libre Baskerville", serif;
|
||||
font-size: 42px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
clip-path: inset(0 100% 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .subtitle {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin: 8px 0 16px 0;
|
||||
clip-path: inset(0 100% 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .key {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .key-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .key-box {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #5c5c5c;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .key-line {
|
||||
width: 16px;
|
||||
height: 1.5px;
|
||||
background-color: #326fa8;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .key-text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .chart-svg {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .source {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .grid-line {
|
||||
stroke: #e8e8e8;
|
||||
stroke-width: 0.5px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .axis-label {
|
||||
font-size: 14px;
|
||||
fill: #666;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .bar {
|
||||
fill: #5c5c5c;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .bar-label {
|
||||
font-size: 14px;
|
||||
fill: #333;
|
||||
text-anchor: middle;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-composition-id="data-chart"] .line-label {
|
||||
font-size: 14px;
|
||||
fill: #326fa8;
|
||||
font-weight: 600;
|
||||
text-anchor: middle;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const svg = document.querySelector('[data-composition-id="data-chart"] .chart-svg');
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Data
|
||||
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];
|
||||
const revenueData = [8, 12, 15, 11, 18, 22];
|
||||
const conversionData = [2.1, 2.8, 3.2, 2.9, 3.8, 4.2];
|
||||
|
||||
// Chart Dimensions
|
||||
const chartWidth = 1400;
|
||||
const chartHeight = 500;
|
||||
const startX = 100;
|
||||
const startY = 600;
|
||||
const barWidth = 80;
|
||||
const spacing = (chartWidth - barWidth * months.length) / (months.length + 1);
|
||||
|
||||
// Scales
|
||||
const maxRevenue = 25;
|
||||
const maxConversion = 5;
|
||||
|
||||
const getBarY = (val) => startY - (val / maxRevenue) * chartHeight;
|
||||
const getLineY = (val) => startY - (val / maxConversion) * chartHeight;
|
||||
const getX = (i) => startX + spacing + i * (barWidth + spacing) + barWidth / 2;
|
||||
|
||||
// Setup Elements
|
||||
const gridGroup = svg.querySelector(".gridlines");
|
||||
const xAxisGroup = svg.querySelector(".x-axis-labels");
|
||||
const barsGroup = svg.querySelector(".bars-group");
|
||||
const barLabelsGroup = svg.querySelector(".bar-labels-group");
|
||||
const lineLabelsGroup = svg.querySelector(".line-labels-group");
|
||||
const conversionPath = svg.querySelector(".conversion-path");
|
||||
const lineDot = svg.querySelector(".line-dot");
|
||||
|
||||
// Create Gridlines
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const y = startY - i * (chartHeight / 5);
|
||||
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
||||
line.setAttribute("x1", startX);
|
||||
line.setAttribute("y1", y);
|
||||
line.setAttribute("x2", startX + chartWidth);
|
||||
line.setAttribute("y2", y);
|
||||
line.setAttribute("class", "grid-line");
|
||||
gridGroup.appendChild(line);
|
||||
}
|
||||
|
||||
// Create X-Axis Labels & Bars & Bar Labels
|
||||
const linePoints = [];
|
||||
months.forEach((month, i) => {
|
||||
const x = getX(i);
|
||||
|
||||
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||
label.setAttribute("x", x);
|
||||
label.setAttribute("y", startY + 30);
|
||||
label.setAttribute("class", "axis-label");
|
||||
label.textContent = month;
|
||||
xAxisGroup.appendChild(label);
|
||||
|
||||
const barHeight = (revenueData[i] / maxRevenue) * chartHeight;
|
||||
const bar = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
bar.setAttribute("x", x - barWidth / 2);
|
||||
bar.setAttribute("y", startY);
|
||||
bar.setAttribute("width", barWidth);
|
||||
bar.setAttribute("height", 0);
|
||||
bar.setAttribute("class", "bar");
|
||||
barsGroup.appendChild(bar);
|
||||
|
||||
const barLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||
barLabel.setAttribute("x", x);
|
||||
barLabel.setAttribute("y", getBarY(revenueData[i]) - 10);
|
||||
barLabel.setAttribute("class", "bar-label");
|
||||
barLabel.textContent = `$${revenueData[i]}K`;
|
||||
barLabelsGroup.appendChild(barLabel);
|
||||
|
||||
linePoints.push(`${x},${getLineY(conversionData[i])}`);
|
||||
|
||||
const lineLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||
lineLabel.setAttribute("x", x);
|
||||
lineLabel.setAttribute("y", getLineY(conversionData[i]) - 15);
|
||||
lineLabel.setAttribute("class", "line-label");
|
||||
lineLabel.textContent = `${conversionData[i]}%`;
|
||||
lineLabelsGroup.appendChild(lineLabel);
|
||||
});
|
||||
|
||||
// Set Path Data
|
||||
conversionPath.setAttribute("d", `M ${linePoints.join(" L ")}`);
|
||||
const pathLength = conversionPath.getTotalLength();
|
||||
conversionPath.style.strokeDasharray = pathLength;
|
||||
conversionPath.style.strokeDashoffset = pathLength;
|
||||
|
||||
// --- ANIMATION TIMELINE ---
|
||||
|
||||
tl.to(
|
||||
'[data-composition-id="data-chart"] .headline',
|
||||
{
|
||||
clipPath: "inset(0 0% 0 0)",
|
||||
duration: 1.2,
|
||||
ease: "power1.inOut",
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
'[data-composition-id="data-chart"] .subtitle',
|
||||
{
|
||||
clipPath: "inset(0 0% 0 0)",
|
||||
duration: 1.2,
|
||||
ease: "power1.inOut",
|
||||
},
|
||||
0.2,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
'[data-composition-id="data-chart"] .key',
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.4,
|
||||
ease: "power2.out",
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
'[data-composition-id="data-chart"] .grid-line',
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.5,
|
||||
stagger: 0.25,
|
||||
ease: "power2.out",
|
||||
},
|
||||
0.5,
|
||||
);
|
||||
|
||||
// Bars
|
||||
const bars = barsGroup.querySelectorAll(".bar");
|
||||
const barLabels = barLabelsGroup.querySelectorAll(".bar-label");
|
||||
|
||||
bars.forEach((bar, i) => {
|
||||
const barHeight = (revenueData[i] / maxRevenue) * chartHeight;
|
||||
const startTime = 1.5 + i * 0.5;
|
||||
|
||||
tl.to(
|
||||
bar,
|
||||
{
|
||||
attr: { height: barHeight, y: startY - barHeight },
|
||||
duration: 0.8,
|
||||
ease: "power2.out",
|
||||
},
|
||||
startTime,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
barLabels[i],
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.3,
|
||||
ease: "power2.out",
|
||||
},
|
||||
startTime + 0.8,
|
||||
);
|
||||
});
|
||||
|
||||
// Line
|
||||
const lastBarStartTime = 1.5 + (months.length - 1) * 0.5;
|
||||
const lineStartTime = lastBarStartTime + 0.5;
|
||||
|
||||
tl.to(
|
||||
conversionPath,
|
||||
{
|
||||
strokeDashoffset: 0,
|
||||
duration: 3,
|
||||
ease: "none",
|
||||
},
|
||||
lineStartTime,
|
||||
);
|
||||
|
||||
// Line Dot & Labels
|
||||
tl.set(
|
||||
lineDot,
|
||||
{
|
||||
opacity: 1,
|
||||
attr: { cx: getX(0), cy: getLineY(conversionData[0]) },
|
||||
},
|
||||
lineStartTime,
|
||||
);
|
||||
|
||||
const lineLabels = lineLabelsGroup.querySelectorAll(".line-label");
|
||||
months.forEach((_, i) => {
|
||||
if (i > 0) {
|
||||
const x = getX(i);
|
||||
const y = getLineY(conversionData[i]);
|
||||
tl.to(
|
||||
lineDot,
|
||||
{
|
||||
attr: { cx: x, cy: y },
|
||||
duration: 3 / (months.length - 1),
|
||||
ease: "none",
|
||||
},
|
||||
lineStartTime + ((i - 1) / (months.length - 1)) * 3,
|
||||
);
|
||||
}
|
||||
|
||||
const labelTime = lineStartTime + (i / (months.length - 1)) * 3 + 0.2;
|
||||
|
||||
lineLabels[i].textContent = "0.0%";
|
||||
|
||||
tl.to(
|
||||
lineLabels[i],
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.1,
|
||||
ease: "power2.out",
|
||||
},
|
||||
labelTime,
|
||||
);
|
||||
|
||||
const dummy = { val: 0 };
|
||||
tl.to(
|
||||
dummy,
|
||||
{
|
||||
val: conversionData[i],
|
||||
duration: 0.8,
|
||||
ease: "power2.out",
|
||||
onUpdate: () => {
|
||||
lineLabels[i].textContent = dummy.val.toFixed(1) + "%";
|
||||
},
|
||||
},
|
||||
labelTime,
|
||||
);
|
||||
});
|
||||
|
||||
// Source Line
|
||||
tl.to(
|
||||
'[data-composition-id="data-chart"] .source',
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.5,
|
||||
ease: "power2.out",
|
||||
},
|
||||
">+0.5",
|
||||
);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["data-chart"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="data-chart" data-composition-src="compositions/data-chart.html" data-start="0" data-duration="15" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `data` `chart` `statistics`.
|
||||
|
||||
@@ -3,34 +3,412 @@ title: "Domain Warp Dissolve"
|
||||
description: "Shader transition with fractal noise domain warping"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/domain-warp-dissolve.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/domain-warp-dissolve.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/domain-warp-dissolve.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Domain Warp Dissolve preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add domain-warp-dissolve
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add domain-warp-dissolve" />
|
||||
|
||||
That writes one file: `compositions/domain-warp-dissolve.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`domain-warp-dissolve.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="domain-warp-dissolve"
|
||||
data-composition-src="compositions/domain-warp-dissolve.html"
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<style>
|
||||
[data-composition-id="main"] {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #0d1b2a;
|
||||
}
|
||||
|
||||
/* Blueprint grid */
|
||||
.bp-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.bp-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.bp-right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 720px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 60px 60px 60px 40px;
|
||||
}
|
||||
.bp-divider {
|
||||
position: absolute;
|
||||
left: 1200px;
|
||||
top: 60px;
|
||||
bottom: 60px;
|
||||
width: 1px;
|
||||
background: #00f5d4;
|
||||
opacity: 0.15;
|
||||
}
|
||||
.bp-number {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4em;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 60px;
|
||||
color: #00f5d4;
|
||||
}
|
||||
|
||||
/* Right panel typography */
|
||||
.bp-name {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 38px;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 20px;
|
||||
color: #00f5d4;
|
||||
}
|
||||
.bp-plabel {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
color: #00f5d4;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.bp-prompt {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
padding: 16px 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
color: #00f5d4;
|
||||
background: rgba(0, 245, 212, 0.06);
|
||||
border: 1px solid rgba(0, 245, 212, 0.12);
|
||||
}
|
||||
.bp-desc {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
color: #8aa8c0;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* GL scenes (hidden, just for texture capture) */
|
||||
.scene {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.scene-label {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 140px;
|
||||
letter-spacing: 0.06em;
|
||||
opacity: 0.12;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="bp-grid"></div>
|
||||
<div class="bp-divider"></div>
|
||||
<div class="bp-number">01 / 14</div>
|
||||
|
||||
<!-- GL demo scenes (captured as textures, then hidden) -->
|
||||
<div id="s1" class="scene" style="background: #0d1b2a">
|
||||
<div class="scene-label" style="color: #00f5d4">SCENE A</div>
|
||||
</div>
|
||||
<div id="s2" class="scene" style="background: #00f5d4; opacity: 0">
|
||||
<div class="scene-label" style="color: #0d1b2a">SCENE B</div>
|
||||
</div>
|
||||
|
||||
<!-- GL canvas (left panel only) -->
|
||||
<canvas
|
||||
id="gl-canvas"
|
||||
width="1200"
|
||||
height="1080"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
"
|
||||
>
|
||||
</canvas>
|
||||
|
||||
<!-- Right panel metadata -->
|
||||
<div class="bp-right">
|
||||
<div class="bp-name">Domain Warp Dissolve</div>
|
||||
<div class="bp-plabel">Prompt</div>
|
||||
<div class="bp-prompt">"use domain warp dissolve shader transition"</div>
|
||||
<div class="bp-desc">
|
||||
Cascaded fbm(p + fbm(p)) displaces both scenes along a warp field. Iridescent
|
||||
cosine-palette edge glow.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var glCanvas = document.getElementById("gl-canvas");
|
||||
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
|
||||
if (!gl) {
|
||||
console.warn("WebGL not available");
|
||||
window.__timelines["main"] = gsap.timeline({ paused: true });
|
||||
} else {
|
||||
gl.viewport(0, 0, 1200, 1080);
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
||||
var sceneTextures = {};
|
||||
|
||||
function captureScene(sceneId) {
|
||||
var scene = document.getElementById(sceneId);
|
||||
var origOpacity = scene.style.opacity;
|
||||
var origZ = scene.style.zIndex;
|
||||
scene.style.opacity = "1";
|
||||
scene.style.zIndex = "999";
|
||||
scene.offsetHeight;
|
||||
var c = document.createElement("canvas");
|
||||
c.width = 1200;
|
||||
c.height = 1080;
|
||||
var ctx = c.getContext("2d");
|
||||
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
|
||||
ctx.fillRect(0, 0, 1200, 1080);
|
||||
var sr = scene.getBoundingClientRect();
|
||||
var els = scene.querySelectorAll("*");
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
var el = els[i],
|
||||
cs = window.getComputedStyle(el);
|
||||
if (cs.display === "none" || cs.visibility === "hidden") continue;
|
||||
var r = el.getBoundingClientRect();
|
||||
if (r.width < 1 || r.height < 1) continue;
|
||||
var x = r.left - sr.left,
|
||||
y = r.top - sr.top,
|
||||
w = r.width,
|
||||
h = r.height;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
|
||||
var bg = cs.backgroundColor;
|
||||
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
}
|
||||
var hasChildEls = el.querySelector("div,span,img,video");
|
||||
var text = "";
|
||||
for (var j = 0; j < el.childNodes.length; j++)
|
||||
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
|
||||
text = text.trim();
|
||||
if (text && !hasChildEls) {
|
||||
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
|
||||
ctx.fillStyle = cs.color;
|
||||
if (cs.letterSpacing && cs.letterSpacing !== "normal")
|
||||
ctx.letterSpacing = cs.letterSpacing;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(text, x + w / 2, y + h / 2);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
scene.style.opacity = origOpacity;
|
||||
scene.style.zIndex = origZ;
|
||||
var tex = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
|
||||
sceneTextures[sceneId] = tex;
|
||||
}
|
||||
|
||||
var vertSrc =
|
||||
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
|
||||
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
|
||||
var quadBuf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.bufferData(
|
||||
gl.ARRAY_BUFFER,
|
||||
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
||||
gl.STATIC_DRAW,
|
||||
);
|
||||
function compileShader(src, type) {
|
||||
var s = gl.createShader(type);
|
||||
gl.shaderSource(s, src);
|
||||
gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
|
||||
console.error("Shader:", gl.getShaderInfoLog(s));
|
||||
return s;
|
||||
}
|
||||
function mkProg(fragSrc) {
|
||||
var p = gl.createProgram();
|
||||
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
|
||||
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
|
||||
gl.linkProgram(p);
|
||||
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
|
||||
console.error("Link:", gl.getProgramInfoLog(p));
|
||||
return p;
|
||||
}
|
||||
|
||||
var H =
|
||||
"precision mediump float;" +
|
||||
"varying vec2 v_uv;" +
|
||||
"uniform sampler2D u_from, u_to;" +
|
||||
"uniform float u_progress;" +
|
||||
"uniform vec2 u_resolution;\n";
|
||||
|
||||
var NQ =
|
||||
"float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}" +
|
||||
"float vnoise(vec2 p){vec2 i=floor(p),f=fract(p);" +
|
||||
"f=f*f*f*(f*(f*6.-15.)+10.);" +
|
||||
"return mix(mix(hash(i),hash(i+vec2(1,0)),f.x)," +
|
||||
"mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);}" +
|
||||
"float fbm(vec2 p){float v=0.,a=.5;" +
|
||||
"mat2 R=mat2(.8,.6,-.6,.8);" +
|
||||
"for(int i=0;i<5;i++){v+=a*vnoise(p);p=R*p*2.02;a*=.5;}return v;}";
|
||||
var CP =
|
||||
"vec3 palette(float t,vec3 a,vec3 b,vec3 c,vec3 d){" + "return a+b*cos(6.2832*(c*t+d));}";
|
||||
|
||||
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
|
||||
var progTrans = mkProg(
|
||||
H +
|
||||
NQ +
|
||||
CP +
|
||||
"void main(){vec2 q=vec2(fbm(v_uv*3.),fbm(v_uv*3.+vec2(5.2,1.3)));vec2 r=vec2(fbm(v_uv*3.+q*4.+vec2(1.7,9.2)),fbm(v_uv*3.+q*4.+vec2(8.3,2.8)));float n=fbm(v_uv*3.+r*2.);vec2 warpDir=(q-.5)*.4;vec4 A=texture2D(u_from,clamp(v_uv+warpDir*u_progress,0.,1.));vec4 B=texture2D(u_to,clamp(v_uv-warpDir*(1.-u_progress),0.,1.));float e=smoothstep(u_progress-.08,u_progress+.08,n);float ed=abs(n-u_progress);float em=smoothstep(.1,0.,ed)*(1.-step(1.,u_progress));vec3 ec=palette(ed*8.,vec3(.5),vec3(.5),vec3(1.),vec3(0.,.33,.67));gl_FragColor=vec4(mix(B,A,e).rgb+ec*em*2.,1.);}",
|
||||
);
|
||||
|
||||
function renderShader(prog, texFrom, texTo, progress) {
|
||||
gl.useProgram(prog);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texFrom);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texTo);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
|
||||
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
|
||||
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
|
||||
var pos = gl.getAttribLocation(prog, "a_pos");
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.enableVertexAttribArray(pos);
|
||||
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
function easeInOut(p) {
|
||||
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
|
||||
}
|
||||
|
||||
captureScene("s1");
|
||||
captureScene("s2");
|
||||
glCanvas.style.display = "block";
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
document.querySelectorAll(".scene").forEach(function (s) {
|
||||
s.style.opacity = "0";
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({
|
||||
paused: true,
|
||||
onUpdate: function () {
|
||||
var t = tl.time();
|
||||
if (t >= 1.0 && t < 3.0) {
|
||||
renderShader(
|
||||
progTrans,
|
||||
sceneTextures["s1"],
|
||||
sceneTextures["s2"],
|
||||
easeInOut((t - 1.0) / 2.0),
|
||||
);
|
||||
} else if (t >= 3.0) {
|
||||
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
|
||||
} else {
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
|
||||
window.__timelines["main"] = tl;
|
||||
} // end if (gl)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="domain-warp-dissolve" data-composition-src="compositions/domain-warp-dissolve.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ title: "Editorial Flash Overlay"
|
||||
description: "Finite neutral-warm light layers for a seek-safe camera-flash cut or social reveal"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/editorial-flash-overlay.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/editorial-flash-overlay.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/editorial-flash-overlay.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Editorial Flash Overlay preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add editorial-flash-overlay
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add editorial-flash-overlay" />
|
||||
|
||||
That writes one file: `compositions/editorial-flash-overlay.html`.
|
||||
|
||||
@@ -32,6 +32,144 @@ It runs for 4 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`editorial-flash-overlay.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#ef-flash {
|
||||
--hf-editorial-flash-wash: 255 253 250;
|
||||
--hf-editorial-flash-warm: 255 174 105;
|
||||
--hf-editorial-flash-cool: 183 218 255;
|
||||
position: absolute;
|
||||
inset: -12%;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#ef-flash .ef-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
#ef-wash {
|
||||
background: rgb(var(--hf-editorial-flash-wash));
|
||||
}
|
||||
|
||||
#ef-core {
|
||||
inset: -18%;
|
||||
background: radial-gradient(
|
||||
ellipse 68% 92% at 37% 47%,
|
||||
rgb(255 255 255) 0%,
|
||||
rgb(255 255 255 / 98%) 20%,
|
||||
rgb(var(--hf-editorial-flash-wash) / 86%) 45%,
|
||||
rgb(var(--hf-editorial-flash-warm) / 32%) 67%,
|
||||
transparent 86%
|
||||
);
|
||||
mix-blend-mode: screen;
|
||||
transform-origin: 38% 48%;
|
||||
}
|
||||
|
||||
#ef-sweep {
|
||||
inset: -28%;
|
||||
background: linear-gradient(
|
||||
108deg,
|
||||
transparent 21%,
|
||||
rgb(var(--hf-editorial-flash-warm) / 10%) 38%,
|
||||
rgb(var(--hf-editorial-flash-wash) / 90%) 49%,
|
||||
rgb(255 255 255 / 98%) 53%,
|
||||
rgb(var(--hf-editorial-flash-cool) / 24%) 62%,
|
||||
transparent 79%
|
||||
);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="editorial-flash-overlay"
|
||||
data-composition-id="editorial-flash-overlay"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="ef-flash" aria-hidden="true">
|
||||
<div id="ef-wash" class="ef-layer"></div>
|
||||
<div id="ef-core" class="ef-layer" data-layout-allow-overflow></div>
|
||||
<div id="ef-sweep" class="ef-layer" data-layout-allow-overflow></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var flash = document.getElementById("ef-flash");
|
||||
var host = flash && flash.closest("[data-composition-id]");
|
||||
if (!host) throw new Error("Editorial Flash could not find its composition host");
|
||||
|
||||
var compositionId = host.getAttribute("data-composition-id");
|
||||
var duration = Number(host.getAttribute("data-duration")) || 4;
|
||||
var hit = Math.max(0.12, duration * 0.58);
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.to({}, { duration: duration, ease: "none" }, 0);
|
||||
tl.to("#ef-wash", { opacity: 0.92, duration: 0.04, ease: "power4.in" }, hit);
|
||||
tl.fromTo(
|
||||
"#ef-core",
|
||||
{ opacity: 0, scale: 0.86, rotation: -5 },
|
||||
{ opacity: 1, scale: 1, rotation: -5, duration: 0.05, ease: "power4.in" },
|
||||
hit - 0.01,
|
||||
);
|
||||
tl.fromTo(
|
||||
"#ef-sweep",
|
||||
{ opacity: 0, xPercent: -12, rotation: -2 },
|
||||
{ opacity: 0.9, xPercent: 8, rotation: -2, duration: 0.04, ease: "power4.in" },
|
||||
hit,
|
||||
);
|
||||
tl.to("#ef-wash", { opacity: 0, duration: 0.18, ease: "power3.out" }, hit + 0.04);
|
||||
tl.to(
|
||||
"#ef-core",
|
||||
{ opacity: 0, scale: 1.18, rotation: -5, duration: 0.34, ease: "power2.out" },
|
||||
hit + 0.04,
|
||||
);
|
||||
tl.to(
|
||||
"#ef-sweep",
|
||||
{ opacity: 0, xPercent: 28, rotation: -2, duration: 0.3, ease: "power2.out" },
|
||||
hit + 0.04,
|
||||
);
|
||||
|
||||
window.__timelines[compositionId] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `flash` `editorial` `social` `light` `transition` `overlay` `media-treatment-overlay`.
|
||||
|
||||
@@ -3,34 +3,398 @@ title: "Flash Through White"
|
||||
description: "Shader transition with white flash crossfade"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flash-through-white.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flash-through-white.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/flash-through-white.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Flash Through White preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add flash-through-white
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add flash-through-white" />
|
||||
|
||||
That writes one file: `compositions/flash-through-white.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`flash-through-white.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="flash-through-white"
|
||||
data-composition-src="compositions/flash-through-white.html"
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<style>
|
||||
[data-composition-id="main"] {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #03071e;
|
||||
}
|
||||
|
||||
/* Blueprint grid */
|
||||
.bp-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.bp-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.bp-right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 720px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 60px 60px 60px 40px;
|
||||
}
|
||||
.bp-divider {
|
||||
position: absolute;
|
||||
left: 1200px;
|
||||
top: 60px;
|
||||
bottom: 60px;
|
||||
width: 1px;
|
||||
background: #ffba08;
|
||||
opacity: 0.15;
|
||||
}
|
||||
.bp-number {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4em;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 60px;
|
||||
color: #ffba08;
|
||||
}
|
||||
|
||||
/* Right panel typography */
|
||||
.bp-name {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 38px;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 20px;
|
||||
color: #ffba08;
|
||||
}
|
||||
.bp-plabel {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
color: #ffba08;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.bp-prompt {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
padding: 16px 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
color: #ffba08;
|
||||
background: rgba(255, 186, 8, 0.06);
|
||||
border: 1px solid rgba(255, 186, 8, 0.12);
|
||||
}
|
||||
.bp-desc {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
color: #888;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* GL scenes (hidden, just for texture capture) */
|
||||
.scene {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.scene-label {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 140px;
|
||||
letter-spacing: 0.06em;
|
||||
opacity: 0.12;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="bp-grid"></div>
|
||||
<div class="bp-divider"></div>
|
||||
<div class="bp-number">12 / 14</div>
|
||||
|
||||
<!-- GL demo scenes (captured as textures, then hidden) -->
|
||||
<div id="s1" class="scene" style="background: #03071e">
|
||||
<div class="scene-label" style="color: #ffba08">SCENE A</div>
|
||||
</div>
|
||||
<div id="s2" class="scene" style="background: #ffba08; opacity: 0">
|
||||
<div class="scene-label" style="color: #03071e">SCENE B</div>
|
||||
</div>
|
||||
|
||||
<!-- GL canvas (left panel only) -->
|
||||
<canvas
|
||||
id="gl-canvas"
|
||||
width="1200"
|
||||
height="1080"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
"
|
||||
>
|
||||
</canvas>
|
||||
|
||||
<!-- Right panel metadata -->
|
||||
<div class="bp-right">
|
||||
<div class="bp-name">Flash Through White</div>
|
||||
<div class="bp-plabel">Prompt</div>
|
||||
<div class="bp-prompt">"use flash through white shader transition"</div>
|
||||
<div class="bp-desc">
|
||||
Both scenes brighten to a white midpoint. Works on dark backgrounds where
|
||||
color-dip-to-black is invisible.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var glCanvas = document.getElementById("gl-canvas");
|
||||
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
|
||||
if (!gl) {
|
||||
console.warn("WebGL not available");
|
||||
window.__timelines["main"] = gsap.timeline({ paused: true });
|
||||
} else {
|
||||
gl.viewport(0, 0, 1200, 1080);
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
||||
var sceneTextures = {};
|
||||
|
||||
function captureScene(sceneId) {
|
||||
var scene = document.getElementById(sceneId);
|
||||
var origOpacity = scene.style.opacity;
|
||||
var origZ = scene.style.zIndex;
|
||||
scene.style.opacity = "1";
|
||||
scene.style.zIndex = "999";
|
||||
scene.offsetHeight;
|
||||
var c = document.createElement("canvas");
|
||||
c.width = 1200;
|
||||
c.height = 1080;
|
||||
var ctx = c.getContext("2d");
|
||||
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
|
||||
ctx.fillRect(0, 0, 1200, 1080);
|
||||
var sr = scene.getBoundingClientRect();
|
||||
var els = scene.querySelectorAll("*");
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
var el = els[i],
|
||||
cs = window.getComputedStyle(el);
|
||||
if (cs.display === "none" || cs.visibility === "hidden") continue;
|
||||
var r = el.getBoundingClientRect();
|
||||
if (r.width < 1 || r.height < 1) continue;
|
||||
var x = r.left - sr.left,
|
||||
y = r.top - sr.top,
|
||||
w = r.width,
|
||||
h = r.height;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
|
||||
var bg = cs.backgroundColor;
|
||||
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
}
|
||||
var hasChildEls = el.querySelector("div,span,img,video");
|
||||
var text = "";
|
||||
for (var j = 0; j < el.childNodes.length; j++)
|
||||
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
|
||||
text = text.trim();
|
||||
if (text && !hasChildEls) {
|
||||
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
|
||||
ctx.fillStyle = cs.color;
|
||||
if (cs.letterSpacing && cs.letterSpacing !== "normal")
|
||||
ctx.letterSpacing = cs.letterSpacing;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(text, x + w / 2, y + h / 2);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
scene.style.opacity = origOpacity;
|
||||
scene.style.zIndex = origZ;
|
||||
var tex = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
|
||||
sceneTextures[sceneId] = tex;
|
||||
}
|
||||
|
||||
var vertSrc =
|
||||
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
|
||||
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
|
||||
var quadBuf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.bufferData(
|
||||
gl.ARRAY_BUFFER,
|
||||
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
||||
gl.STATIC_DRAW,
|
||||
);
|
||||
function compileShader(src, type) {
|
||||
var s = gl.createShader(type);
|
||||
gl.shaderSource(s, src);
|
||||
gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
|
||||
console.error("Shader:", gl.getShaderInfoLog(s));
|
||||
return s;
|
||||
}
|
||||
function mkProg(fragSrc) {
|
||||
var p = gl.createProgram();
|
||||
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
|
||||
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
|
||||
gl.linkProgram(p);
|
||||
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
|
||||
console.error("Link:", gl.getProgramInfoLog(p));
|
||||
return p;
|
||||
}
|
||||
|
||||
var H =
|
||||
"precision mediump float;" +
|
||||
"varying vec2 v_uv;" +
|
||||
"uniform sampler2D u_from, u_to;" +
|
||||
"uniform float u_progress;" +
|
||||
"uniform vec2 u_resolution;\n";
|
||||
|
||||
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
|
||||
var progTrans = mkProg(
|
||||
H +
|
||||
"void main(){vec4 A=texture2D(u_from,v_uv),B=texture2D(u_to,v_uv);float toWhite=smoothstep(0.,.45,u_progress);vec3 fromC=mix(A.rgb,vec3(1.),toWhite);float fromWhite=1.-smoothstep(.5,1.,u_progress);vec3 toC=mix(B.rgb,vec3(1.),fromWhite);gl_FragColor=vec4(mix(fromC,toC,smoothstep(.35,.65,u_progress)),1.);}",
|
||||
);
|
||||
|
||||
function renderShader(prog, texFrom, texTo, progress) {
|
||||
gl.useProgram(prog);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texFrom);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texTo);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
|
||||
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
|
||||
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
|
||||
var pos = gl.getAttribLocation(prog, "a_pos");
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.enableVertexAttribArray(pos);
|
||||
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
function easeInOut(p) {
|
||||
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
|
||||
}
|
||||
|
||||
captureScene("s1");
|
||||
captureScene("s2");
|
||||
glCanvas.style.display = "block";
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
document.querySelectorAll(".scene").forEach(function (s) {
|
||||
s.style.opacity = "0";
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({
|
||||
paused: true,
|
||||
onUpdate: function () {
|
||||
var t = tl.time();
|
||||
if (t >= 1.0 && t < 3.0) {
|
||||
renderShader(
|
||||
progTrans,
|
||||
sceneTextures["s1"],
|
||||
sceneTextures["s2"],
|
||||
easeInOut((t - 1.0) / 2.0),
|
||||
);
|
||||
} else if (t >= 3.0) {
|
||||
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
|
||||
} else {
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
|
||||
window.__timelines["main"] = tl;
|
||||
} // end if (gl)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="flash-through-white" data-composition-src="compositions/flash-through-white.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,499 @@ title: "Flowchart Vertical"
|
||||
description: "Portrait animated decision tree with SVG connectors, sticky-note nodes, cursor interaction, and typing correction"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flowchart-vertical.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flowchart-vertical.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/flowchart-vertical.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Flowchart Vertical preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add flowchart-vertical
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add flowchart-vertical" />
|
||||
|
||||
That writes one file: `compositions/flowchart-vertical.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 12 seconds at 1440×2560. Paste this into your composition:
|
||||
<Accordion title={`flowchart-vertical.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1440, height=2560" />
|
||||
<title>Flowchart Vertical</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=block"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<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 {
|
||||
margin: 0;
|
||||
width: 1440px;
|
||||
height: 2560px;
|
||||
overflow: hidden;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="flowchart-vertical"
|
||||
data-composition-id="flowchart-vertical"
|
||||
data-composition-src="compositions/flowchart-vertical.html"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1440"
|
||||
data-height="2560"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
>
|
||||
<div class="canvas">
|
||||
<svg class="connectors" width="1440" height="2560" viewBox="0 0 1440 2560">
|
||||
<!-- Level 1 to 2 -->
|
||||
<path
|
||||
id="path-1-L"
|
||||
class="connector"
|
||||
d="M 720 210 L 720 360 L 440 360 L 440 530"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<path
|
||||
id="path-1-R"
|
||||
class="connector"
|
||||
d="M 720 210 L 720 360 L 1000 360 L 1000 530"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="4"
|
||||
/>
|
||||
|
||||
<!-- Level 2 to 3 (Left side) -->
|
||||
<path
|
||||
id="path-2-LL"
|
||||
class="connector"
|
||||
d="M 440 590 L 440 760 L 280 760 L 280 930"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<path
|
||||
id="path-2-LR"
|
||||
class="connector"
|
||||
d="M 440 590 L 440 760 L 600 760 L 600 930"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="4"
|
||||
/>
|
||||
|
||||
<!-- Level 2 to 3 (Right side) -->
|
||||
<path
|
||||
id="path-2-RL"
|
||||
class="connector"
|
||||
d="M 1000 590 L 1000 760 L 840 760 L 840 930"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<path
|
||||
id="path-2-RR"
|
||||
class="connector"
|
||||
d="M 1000 590 L 1000 760 L 1160 760 L 1160 930"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="4"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div id="label-yes" class="connector-label">Yes</div>
|
||||
<div id="label-not-sure" class="connector-label">Not sure</div>
|
||||
|
||||
<!-- Level 1 -->
|
||||
<div id="node-root" class="node yellow" style="left: 720px; top: 180px">
|
||||
Should I learn to code?
|
||||
</div>
|
||||
|
||||
<!-- Level 2 -->
|
||||
<div id="node-yes" class="node green" style="left: 440px; top: 560px">Yes</div>
|
||||
<div id="node-not-sure" class="node peach" style="left: 1000px; top: 560px">Not sure</div>
|
||||
|
||||
<!-- Level 3 -->
|
||||
<div id="node-python" class="node lavender" style="left: 280px; top: 960px">
|
||||
<div style="position: relative; display: inline-block">
|
||||
<span id="python-text">Start with Pythom</span>
|
||||
<svg class="squiggle-container" width="64" height="6" viewBox="0 0 64 6">
|
||||
<path
|
||||
id="squiggle"
|
||||
d="M 0 3 Q 2 0 4 3 T 8 3 T 12 3 T 16 3 T 20 3 T 24 3 T 28 3 T 32 3 T 36 3 T 40 3 T 44 3 T 48 3 T 52 3 T 56 3 T 60 3 T 64 3"
|
||||
fill="none"
|
||||
stroke="#E53935"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div id="node-nocode" class="node blue" style="left: 600px; top: 960px">
|
||||
Try no-code first
|
||||
</div>
|
||||
<div id="node-website" class="node pink" style="left: 840px; top: 960px">
|
||||
Build a personal website
|
||||
</div>
|
||||
<div id="node-course" class="node yellow" style="left: 1160px; top: 960px">
|
||||
Take a free intro course
|
||||
</div>
|
||||
|
||||
<!-- Cursor -->
|
||||
<div id="cursor" class="cursor-container">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M5.65376 12.3673H5.46026L5.31717 12.4976L0.500002 16.8829L0.500002 1.19841L11.7841 12.3673H5.65376Z"
|
||||
fill="white"
|
||||
stroke="black"
|
||||
/>
|
||||
</svg>
|
||||
<div class="cursor-tag">You</div>
|
||||
</div>
|
||||
|
||||
<!-- Emoji -->
|
||||
<div id="emoji-thumb" class="emoji">👍</div>
|
||||
|
||||
<!-- Overlay -->
|
||||
<div id="fade-overlay"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#flowchart-vertical {
|
||||
width: 1440px;
|
||||
height: 2560px;
|
||||
background-color: #ffffff;
|
||||
background-image: radial-gradient(#e5e5e5 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
#flowchart-vertical .canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
transform: none;
|
||||
transform-origin: 720px 640px;
|
||||
}
|
||||
|
||||
#flowchart-vertical .node {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
padding: 16px 24px;
|
||||
border-radius: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#flowchart-vertical .node.yellow {
|
||||
background-color: #e8d44d;
|
||||
}
|
||||
#flowchart-vertical .node.green {
|
||||
background-color: #c2e8a0;
|
||||
}
|
||||
#flowchart-vertical .node.peach {
|
||||
background-color: #f5c5a3;
|
||||
}
|
||||
#flowchart-vertical .node.lavender {
|
||||
background-color: #d4c5f9;
|
||||
}
|
||||
#flowchart-vertical .node.blue {
|
||||
background-color: #a8d8f0;
|
||||
}
|
||||
#flowchart-vertical .node.pink {
|
||||
background-color: #f8b4c8;
|
||||
}
|
||||
|
||||
#flowchart-vertical .connectors {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#flowchart-vertical .connector {
|
||||
stroke-dasharray: 1000;
|
||||
stroke-dashoffset: 1000;
|
||||
}
|
||||
|
||||
#flowchart-vertical .connector-label {
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
background: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
z-index: 6;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#flowchart-vertical #label-yes {
|
||||
left: 580px;
|
||||
top: 420px;
|
||||
}
|
||||
#flowchart-vertical #label-not-sure {
|
||||
left: 860px;
|
||||
top: 420px;
|
||||
}
|
||||
|
||||
#flowchart-vertical .cursor-container {
|
||||
position: absolute;
|
||||
left: 1440px;
|
||||
top: 2560px;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#flowchart-vertical .cursor-tag {
|
||||
background-color: #9747ff;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-left: 4px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
#flowchart-vertical .squiggle-container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -4px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#flowchart-vertical .emoji {
|
||||
position: absolute;
|
||||
font-size: 32px;
|
||||
left: 390px;
|
||||
top: 960px;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
#flowchart-vertical #fade-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
opacity: 0;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#flowchart-vertical .selection-border {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
border: 2px solid #0b84f3;
|
||||
border-radius: 16px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#flowchart-vertical .text-highlight {
|
||||
background-color: #d0e4ff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const S = "#flowchart-vertical";
|
||||
const nodePython = document.querySelector(S + " #node-python");
|
||||
const pythonText = document.querySelector(S + " #python-text");
|
||||
|
||||
// 1. Root node scales in
|
||||
tl.to(S + " #node-root", { scale: 1, duration: 0.4, ease: "power2.out" });
|
||||
|
||||
// 2. Hold
|
||||
tl.addLabel("hold1", "+=0.6");
|
||||
|
||||
// 3. Connectors draw
|
||||
tl.to(
|
||||
S + " .connector#path-1-L, " + S + " .connector#path-1-R",
|
||||
{ strokeDashoffset: 0, duration: 0.5, ease: "none" },
|
||||
"hold1",
|
||||
);
|
||||
|
||||
tl.to(
|
||||
S + " #label-yes, " + S + " #label-not-sure",
|
||||
{ opacity: 1, duration: 0.2 },
|
||||
"hold1+=0.25",
|
||||
);
|
||||
|
||||
// 4. Level 2 nodes
|
||||
tl.to(S + " #node-yes", { scale: 1, duration: 0.4, ease: "back.out(1.7)" }, "hold1+=0.4");
|
||||
tl.to(
|
||||
S + " #node-not-sure",
|
||||
{ scale: 1, duration: 0.4, ease: "back.out(1.7)" },
|
||||
"hold1+=0.6",
|
||||
);
|
||||
|
||||
// 5. Hold
|
||||
tl.addLabel("hold2", "+=0.5");
|
||||
|
||||
// 6. Level 2→3 connectors
|
||||
tl.to(
|
||||
S +
|
||||
" .connector#path-2-LL, " +
|
||||
S +
|
||||
" .connector#path-2-LR, " +
|
||||
S +
|
||||
" .connector#path-2-RL, " +
|
||||
S +
|
||||
" .connector#path-2-RR",
|
||||
{ strokeDashoffset: 0, duration: 0.4, ease: "none" },
|
||||
"hold2",
|
||||
);
|
||||
|
||||
// 7. Leaf nodes
|
||||
const leafNodes = [
|
||||
S + " #node-python",
|
||||
S + " #node-nocode",
|
||||
S + " #node-website",
|
||||
S + " #node-course",
|
||||
];
|
||||
leafNodes.forEach((id, i) => {
|
||||
tl.to(
|
||||
id,
|
||||
{ scale: 1, duration: 0.4, ease: "back.out(1.7)" },
|
||||
`hold2+=${0.3 + i * 0.15}`,
|
||||
);
|
||||
});
|
||||
|
||||
tl.to(S + " .squiggle-container", { opacity: 1, duration: 0.1 }, "hold2+=0.3");
|
||||
|
||||
// 8. Hold
|
||||
tl.addLabel("hold3", "+=0.8");
|
||||
|
||||
// 9. Cursor drifts in
|
||||
tl.to(S + " #cursor", { x: -1110, y: -1560, duration: 1, ease: "power1.inOut" }, "hold3");
|
||||
|
||||
// 10. Cursor clicks — selection border
|
||||
tl.add(() => {
|
||||
if (!nodePython) return;
|
||||
if (!nodePython.querySelector(".selection-border")) {
|
||||
const border = document.createElement("div");
|
||||
border.className = "selection-border";
|
||||
nodePython.appendChild(border);
|
||||
}
|
||||
gsap.set(S + " .selection-border", { opacity: 1 });
|
||||
}, "hold3+=1");
|
||||
|
||||
tl.to(
|
||||
S + " #cursor",
|
||||
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 1, overwrite: "auto" },
|
||||
"hold3+=1",
|
||||
);
|
||||
|
||||
// 11. Hold
|
||||
tl.addLabel("hold4", "+=0.3");
|
||||
|
||||
// 12. Double-click to highlight "Pythom"
|
||||
tl.to(
|
||||
S + " #cursor",
|
||||
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 3, overwrite: "auto" },
|
||||
"hold4",
|
||||
);
|
||||
tl.add(() => {
|
||||
if (!pythonText) return;
|
||||
pythonText.innerHTML = 'Start with <span class="text-highlight">Pythom</span>';
|
||||
}, "hold4+=0.1");
|
||||
|
||||
// 13. Hold
|
||||
tl.addLabel("hold5", "+=0.2");
|
||||
|
||||
// 14. Type correction: "Pythom" → "Python"
|
||||
const typingStart = tl.labels["hold5"];
|
||||
const words = ["P", "Py", "Pyt", "Pyth", "Pytho", "Python"];
|
||||
words.forEach((word, i) => {
|
||||
tl.add(
|
||||
() => {
|
||||
if (pythonText)
|
||||
pythonText.innerHTML = `Start with <span class="text-highlight">${word}</span>`;
|
||||
},
|
||||
typingStart + i * 0.05,
|
||||
);
|
||||
});
|
||||
|
||||
const typingEnd = typingStart + words.length * 0.05;
|
||||
tl.add(() => {
|
||||
if (pythonText) pythonText.innerHTML = "Start with Python";
|
||||
gsap.set(S + " .squiggle-container", { opacity: 0 });
|
||||
}, typingEnd);
|
||||
|
||||
// 15. Hold
|
||||
tl.addLabel("hold6", typingEnd + 0.5);
|
||||
|
||||
// 16. Cursor clicks away to deselect
|
||||
tl.to(S + " #cursor", { x: -1050, y: -1520, duration: 0.3, overwrite: "auto" }, "hold6");
|
||||
tl.to(
|
||||
S + " #cursor",
|
||||
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 1, overwrite: "auto" },
|
||||
"hold6+=0.3",
|
||||
);
|
||||
tl.to(S + " .selection-border", { opacity: 0, duration: 0.1 }, "hold6+=0.3");
|
||||
|
||||
// 17. Emoji pop
|
||||
tl.to(
|
||||
S + " #emoji-thumb",
|
||||
{ scale: 1, duration: 0.15, ease: "back.out(2)" },
|
||||
"hold6+=0.4",
|
||||
);
|
||||
|
||||
// 18. Hold
|
||||
tl.addLabel("hold7", "+=2");
|
||||
|
||||
// 19. Fade out
|
||||
tl.to(S + " #fade-overlay", { opacity: 1, duration: 0.5 }, "hold7");
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["flowchart-vertical"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="flowchart-vertical" data-composition-src="compositions/flowchart-vertical.html" data-start="0" data-duration="12" data-track-index="1" data-width="1440" data-height="2560"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,499 @@ title: "Flowchart"
|
||||
description: "Animated decision tree with SVG connectors, sticky-note nodes, cursor interaction, and typing correction"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flowchart.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flowchart.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/flowchart.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Flowchart preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add flowchart
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add flowchart" />
|
||||
|
||||
That writes one file: `compositions/flowchart.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`flowchart.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Flowchart</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=block"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<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 {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="flowchart"
|
||||
data-composition-id="flowchart"
|
||||
data-composition-src="compositions/flowchart.html"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
>
|
||||
<div class="canvas">
|
||||
<svg class="connectors" width="1920" height="1080" viewBox="0 0 1920 1080">
|
||||
<!-- Level 1 to 2 -->
|
||||
<path
|
||||
id="path-1-L"
|
||||
class="connector"
|
||||
d="M 960 130 L 960 200 L 600 200 L 600 270"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<path
|
||||
id="path-1-R"
|
||||
class="connector"
|
||||
d="M 960 130 L 960 200 L 1320 200 L 1320 270"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="4"
|
||||
/>
|
||||
|
||||
<!-- Level 2 to 3 (Left side) -->
|
||||
<path
|
||||
id="path-2-LL"
|
||||
class="connector"
|
||||
d="M 600 330 L 600 400 L 400 400 L 400 470"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<path
|
||||
id="path-2-LR"
|
||||
class="connector"
|
||||
d="M 600 330 L 600 400 L 800 400 L 800 470"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="4"
|
||||
/>
|
||||
|
||||
<!-- Level 2 to 3 (Right side) -->
|
||||
<path
|
||||
id="path-2-RL"
|
||||
class="connector"
|
||||
d="M 1320 330 L 1320 400 L 1120 400 L 1120 470"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<path
|
||||
id="path-2-RR"
|
||||
class="connector"
|
||||
d="M 1320 330 L 1320 400 L 1520 400 L 1520 470"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
stroke-width="4"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div id="label-yes" class="connector-label">Yes</div>
|
||||
<div id="label-not-sure" class="connector-label">Not sure</div>
|
||||
|
||||
<!-- Level 1 -->
|
||||
<div id="node-root" class="node yellow" style="left: 960px; top: 100px">
|
||||
Should I learn to code?
|
||||
</div>
|
||||
|
||||
<!-- Level 2 -->
|
||||
<div id="node-yes" class="node green" style="left: 600px; top: 300px">Yes</div>
|
||||
<div id="node-not-sure" class="node peach" style="left: 1320px; top: 300px">Not sure</div>
|
||||
|
||||
<!-- Level 3 -->
|
||||
<div id="node-python" class="node lavender" style="left: 400px; top: 500px">
|
||||
<div style="position: relative; display: inline-block">
|
||||
<span id="python-text">Start with Pythom</span>
|
||||
<svg class="squiggle-container" width="64" height="6" viewBox="0 0 64 6">
|
||||
<path
|
||||
id="squiggle"
|
||||
d="M 0 3 Q 2 0 4 3 T 8 3 T 12 3 T 16 3 T 20 3 T 24 3 T 28 3 T 32 3 T 36 3 T 40 3 T 44 3 T 48 3 T 52 3 T 56 3 T 60 3 T 64 3"
|
||||
fill="none"
|
||||
stroke="#E53935"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div id="node-nocode" class="node blue" style="left: 800px; top: 500px">
|
||||
Try no-code first
|
||||
</div>
|
||||
<div id="node-website" class="node pink" style="left: 1120px; top: 500px">
|
||||
Build a personal website
|
||||
</div>
|
||||
<div id="node-course" class="node yellow" style="left: 1520px; top: 500px">
|
||||
Take a free intro course
|
||||
</div>
|
||||
|
||||
<!-- Cursor -->
|
||||
<div id="cursor" class="cursor-container">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M5.65376 12.3673H5.46026L5.31717 12.4976L0.500002 16.8829L0.500002 1.19841L11.7841 12.3673H5.65376Z"
|
||||
fill="white"
|
||||
stroke="black"
|
||||
/>
|
||||
</svg>
|
||||
<div class="cursor-tag">You</div>
|
||||
</div>
|
||||
|
||||
<!-- Emoji -->
|
||||
<div id="emoji-thumb" class="emoji">👍</div>
|
||||
|
||||
<!-- Overlay -->
|
||||
<div id="fade-overlay"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
[data-composition-id="flowchart"] {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background-color: #ffffff;
|
||||
background-image: radial-gradient(#e5e5e5 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
[data-composition-id="flowchart"] .canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
transform: scale(1.2);
|
||||
transform-origin: 960px 300px;
|
||||
}
|
||||
|
||||
[data-composition-id="flowchart"] .node {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
padding: 16px 24px;
|
||||
border-radius: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
[data-composition-id="flowchart"] .node.yellow {
|
||||
background-color: #e8d44d;
|
||||
}
|
||||
[data-composition-id="flowchart"] .node.green {
|
||||
background-color: #c2e8a0;
|
||||
}
|
||||
[data-composition-id="flowchart"] .node.peach {
|
||||
background-color: #f5c5a3;
|
||||
}
|
||||
[data-composition-id="flowchart"] .node.lavender {
|
||||
background-color: #d4c5f9;
|
||||
}
|
||||
[data-composition-id="flowchart"] .node.blue {
|
||||
background-color: #a8d8f0;
|
||||
}
|
||||
[data-composition-id="flowchart"] .node.pink {
|
||||
background-color: #f8b4c8;
|
||||
}
|
||||
|
||||
[data-composition-id="flowchart"] .connectors {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-composition-id="flowchart"] .connector {
|
||||
stroke-dasharray: 1000;
|
||||
stroke-dashoffset: 1000;
|
||||
}
|
||||
|
||||
[data-composition-id="flowchart"] .connector-label {
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
background: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
z-index: 6;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
[data-composition-id="flowchart"] #label-yes {
|
||||
left: 780px;
|
||||
top: 230px;
|
||||
}
|
||||
[data-composition-id="flowchart"] #label-not-sure {
|
||||
left: 1140px;
|
||||
top: 230px;
|
||||
}
|
||||
|
||||
[data-composition-id="flowchart"] .cursor-container {
|
||||
position: absolute;
|
||||
left: 1920px;
|
||||
top: 1080px;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
[data-composition-id="flowchart"] .cursor-tag {
|
||||
background-color: #9747ff;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-left: 4px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
[data-composition-id="flowchart"] .squiggle-container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -4px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-composition-id="flowchart"] .emoji {
|
||||
position: absolute;
|
||||
font-size: 32px;
|
||||
left: 510px;
|
||||
top: 500px;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
[data-composition-id="flowchart"] #fade-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
opacity: 0;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-composition-id="flowchart"] .selection-border {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
border: 2px solid #0b84f3;
|
||||
border-radius: 16px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-composition-id="flowchart"] .text-highlight {
|
||||
background-color: #d0e4ff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
const S = '[data-composition-id="flowchart"]';
|
||||
const nodePython = document.querySelector(S + " #node-python");
|
||||
const pythonText = document.querySelector(S + " #python-text");
|
||||
|
||||
// 1. Root node scales in
|
||||
tl.to(S + " #node-root", { scale: 1, duration: 0.4, ease: "power2.out" });
|
||||
|
||||
// 2. Hold
|
||||
tl.addLabel("hold1", "+=0.6");
|
||||
|
||||
// 3. Connectors draw
|
||||
tl.to(
|
||||
S + " .connector#path-1-L, " + S + " .connector#path-1-R",
|
||||
{ strokeDashoffset: 0, duration: 0.5, ease: "none" },
|
||||
"hold1",
|
||||
);
|
||||
|
||||
tl.to(
|
||||
S + " #label-yes, " + S + " #label-not-sure",
|
||||
{ opacity: 1, duration: 0.2 },
|
||||
"hold1+=0.25",
|
||||
);
|
||||
|
||||
// 4. Level 2 nodes
|
||||
tl.to(S + " #node-yes", { scale: 1, duration: 0.4, ease: "back.out(1.7)" }, "hold1+=0.4");
|
||||
tl.to(
|
||||
S + " #node-not-sure",
|
||||
{ scale: 1, duration: 0.4, ease: "back.out(1.7)" },
|
||||
"hold1+=0.6",
|
||||
);
|
||||
|
||||
// 5. Hold
|
||||
tl.addLabel("hold2", "+=0.5");
|
||||
|
||||
// 6. Level 2→3 connectors
|
||||
tl.to(
|
||||
S +
|
||||
" .connector#path-2-LL, " +
|
||||
S +
|
||||
" .connector#path-2-LR, " +
|
||||
S +
|
||||
" .connector#path-2-RL, " +
|
||||
S +
|
||||
" .connector#path-2-RR",
|
||||
{ strokeDashoffset: 0, duration: 0.4, ease: "none" },
|
||||
"hold2",
|
||||
);
|
||||
|
||||
// 7. Leaf nodes
|
||||
const leafNodes = [
|
||||
S + " #node-python",
|
||||
S + " #node-nocode",
|
||||
S + " #node-website",
|
||||
S + " #node-course",
|
||||
];
|
||||
leafNodes.forEach((id, i) => {
|
||||
tl.to(
|
||||
id,
|
||||
{ scale: 1, duration: 0.4, ease: "back.out(1.7)" },
|
||||
`hold2+=${0.3 + i * 0.15}`,
|
||||
);
|
||||
});
|
||||
|
||||
tl.to(S + " .squiggle-container", { opacity: 1, duration: 0.1 }, "hold2+=0.3");
|
||||
|
||||
// 8. Hold
|
||||
tl.addLabel("hold3", "+=0.8");
|
||||
|
||||
// 9. Cursor drifts in
|
||||
tl.to(S + " #cursor", { x: -1470, y: -540, duration: 1, ease: "power1.inOut" }, "hold3");
|
||||
|
||||
// 10. Cursor clicks — selection border
|
||||
tl.add(() => {
|
||||
if (!nodePython) return;
|
||||
if (!nodePython.querySelector(".selection-border")) {
|
||||
const border = document.createElement("div");
|
||||
border.className = "selection-border";
|
||||
nodePython.appendChild(border);
|
||||
}
|
||||
gsap.set(S + " .selection-border", { opacity: 1 });
|
||||
}, "hold3+=1");
|
||||
|
||||
tl.to(
|
||||
S + " #cursor",
|
||||
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 1, overwrite: "auto" },
|
||||
"hold3+=1",
|
||||
);
|
||||
|
||||
// 11. Hold
|
||||
tl.addLabel("hold4", "+=0.3");
|
||||
|
||||
// 12. Double-click to highlight "Pythom"
|
||||
tl.to(
|
||||
S + " #cursor",
|
||||
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 3, overwrite: "auto" },
|
||||
"hold4",
|
||||
);
|
||||
tl.add(() => {
|
||||
if (!pythonText) return;
|
||||
pythonText.innerHTML = 'Start with <span class="text-highlight">Pythom</span>';
|
||||
}, "hold4+=0.1");
|
||||
|
||||
// 13. Hold
|
||||
tl.addLabel("hold5", "+=0.2");
|
||||
|
||||
// 14. Type correction: "Pythom" → "Python"
|
||||
const typingStart = tl.labels["hold5"];
|
||||
const words = ["P", "Py", "Pyt", "Pyth", "Pytho", "Python"];
|
||||
words.forEach((word, i) => {
|
||||
tl.add(
|
||||
() => {
|
||||
if (pythonText)
|
||||
pythonText.innerHTML = `Start with <span class="text-highlight">${word}</span>`;
|
||||
},
|
||||
typingStart + i * 0.05,
|
||||
);
|
||||
});
|
||||
|
||||
const typingEnd = typingStart + words.length * 0.05;
|
||||
tl.add(() => {
|
||||
if (pythonText) pythonText.innerHTML = "Start with Python";
|
||||
gsap.set(S + " .squiggle-container", { opacity: 0 });
|
||||
}, typingEnd);
|
||||
|
||||
// 15. Hold
|
||||
tl.addLabel("hold6", typingEnd + 0.5);
|
||||
|
||||
// 16. Cursor clicks away to deselect
|
||||
tl.to(S + " #cursor", { x: -1420, y: -500, duration: 0.3, overwrite: "auto" }, "hold6");
|
||||
tl.to(
|
||||
S + " #cursor",
|
||||
{ scale: 0.8, duration: 0.05, yoyo: true, repeat: 1, overwrite: "auto" },
|
||||
"hold6+=0.3",
|
||||
);
|
||||
tl.to(S + " .selection-border", { opacity: 0, duration: 0.1 }, "hold6+=0.3");
|
||||
|
||||
// 17. Emoji pop
|
||||
tl.to(
|
||||
S + " #emoji-thumb",
|
||||
{ scale: 1, duration: 0.15, ease: "back.out(2)" },
|
||||
"hold6+=0.4",
|
||||
);
|
||||
|
||||
// 18. Hold
|
||||
tl.addLabel("hold7", "+=2");
|
||||
|
||||
// 19. Fade out
|
||||
tl.to(S + " #fade-overlay", { opacity: 1, duration: 0.5 }, "hold7");
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["flowchart"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="flowchart" data-composition-src="compositions/flowchart.html" data-start="0" data-duration="12" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ title: "Freeze-Frame Dressing"
|
||||
description: "Timeline-driven paper, tape, and flash dressing for a freeze-frame or background-removed subject"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/freeze-frame-dressing.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/freeze-frame-dressing.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/freeze-frame-dressing.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Freeze-Frame Dressing preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add freeze-frame-dressing
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add freeze-frame-dressing" />
|
||||
|
||||
That writes one file: `compositions/freeze-frame-dressing.html`.
|
||||
|
||||
@@ -32,6 +32,135 @@ It runs for 4 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`freeze-frame-dressing.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#hf-freeze-paper {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
background:
|
||||
linear-gradient(115deg, rgb(248 84 73 / 86%) 0 16%, transparent 16%),
|
||||
linear-gradient(345deg, rgb(255 221 82 / 92%) 0 17%, transparent 17%),
|
||||
linear-gradient(rgb(244 240 224 / 78%), rgb(244 240 224 / 78%));
|
||||
clip-path: polygon(2% 8%, 97% 2%, 94% 94%, 6% 98%);
|
||||
mix-blend-mode: screen;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hf-freeze-tape {
|
||||
position: absolute;
|
||||
width: 12.5vmin;
|
||||
height: 2.9vmin;
|
||||
opacity: 0;
|
||||
background: rgb(255 236 168 / 76%);
|
||||
border: 1px solid rgb(110 91 55 / 18%);
|
||||
box-shadow: 0 0.3vmin 0.8vmin rgb(20 16 10 / 18%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#hf-freeze-tape-a {
|
||||
top: 8%;
|
||||
left: 12.5%;
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
|
||||
#hf-freeze-tape-b {
|
||||
right: 12.5%;
|
||||
bottom: 7%;
|
||||
transform: rotate(7deg);
|
||||
}
|
||||
|
||||
#hf-freeze-flash {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
background: #fff;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="freeze-frame-dressing"
|
||||
data-composition-id="freeze-frame-dressing"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="hf-freeze-paper" aria-hidden="true"></div>
|
||||
<div id="hf-freeze-tape-a" class="hf-freeze-tape" aria-hidden="true"></div>
|
||||
<div id="hf-freeze-tape-b" class="hf-freeze-tape" aria-hidden="true"></div>
|
||||
<div id="hf-freeze-flash" aria-hidden="true"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var paper = document.getElementById("hf-freeze-paper");
|
||||
var host = paper && paper.closest("[data-composition-id]");
|
||||
if (!host) throw new Error("Freeze-Frame Dressing could not find its composition host");
|
||||
|
||||
var compositionId = host.getAttribute("data-composition-id");
|
||||
var duration = Number(host.getAttribute("data-duration")) || 4;
|
||||
var hit = Math.max(0.1, duration * 0.24);
|
||||
var exit = Math.max(hit + 0.7, duration - 0.3);
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.to({}, { duration: duration, ease: "none" }, 0);
|
||||
tl.to("#hf-freeze-flash", { opacity: 0.95, duration: 0.06, ease: "power2.out" }, hit);
|
||||
tl.to("#hf-freeze-flash", { opacity: 0, duration: 0.24, ease: "power2.in" }, hit + 0.06);
|
||||
tl.to("#hf-freeze-paper", { opacity: 0.64, duration: 0.18, ease: "power2.out" }, hit + 0.04);
|
||||
tl.fromTo(
|
||||
"#hf-freeze-tape-a",
|
||||
{ opacity: 0, scale: 1.18 },
|
||||
{ opacity: 1, scale: 1, duration: 0.24, ease: "power2.out" },
|
||||
hit + 0.3,
|
||||
);
|
||||
tl.fromTo(
|
||||
"#hf-freeze-tape-b",
|
||||
{ opacity: 0, scale: 1.18 },
|
||||
{ opacity: 1, scale: 1, duration: 0.24, ease: "power2.out" },
|
||||
hit + 0.42,
|
||||
);
|
||||
tl.to(
|
||||
["#hf-freeze-paper", "#hf-freeze-tape-a", "#hf-freeze-tape-b"],
|
||||
{ opacity: 0, duration: 0.24, ease: "power2.in" },
|
||||
exit,
|
||||
);
|
||||
|
||||
window.__timelines[compositionId] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `freeze-frame` `cutout` `scrapbook` `paper` `social` `overlay` `media-treatment-overlay`.
|
||||
|
||||
@@ -0,0 +1,523 @@
|
||||
---
|
||||
title: "Gallery Tunnel"
|
||||
description: "An infinite corridor of panels: a square tunnel receding to a vanishing point, its floor, ceiling and walls tiled with saturated colour slabs and caller-supplied images that swell toward camera and exit past you, the far end dissolving into fog."
|
||||
---
|
||||
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
|
||||
|
||||
<VariablesExplorer
|
||||
previewSrc="/public/catalog/preview/blocks/gallery-tunnel.html"
|
||||
compositionId="gallery-tunnel"
|
||||
compositionSrc="compositions/gallery-tunnel.html"
|
||||
variables={[{"id":"images","type":"string","label":"Image slots: comma-separated paths, tiled into the panels (e.g. assets/shot-1.png,assets/shot-2.png)","default":"","placeholder":"assets/shot-1.png,assets/shot-2.png"},{"id":"imageMix","type":"number","label":"Image share of filled slots (0 = all colour, 1 = all images)","default":0.5,"min":0,"max":1,"step":0.05},{"id":"speed","type":"number","label":"Flight speed","default":3,"min":0,"max":8,"step":0.1,"unit":"segments/s"},{"id":"palette","type":"string","label":"Slab palette: comma-separated hex colours","default":"#FF6A00,#AB54F7,#EA3737,#0072E3,#00AA3C,#FFB200"},{"id":"grid","type":"number","label":"Grid density: slots per wall face per axis (2 = 4 per face, 16 per segment)","default":2,"min":1,"max":4,"step":1},{"id":"fill","type":"number","label":"Slot fill rate","default":0.5,"min":0,"max":1,"step":0.05},{"id":"fog","type":"number","label":"Fog distance: depth at which the corridor reaches pure backdrop","default":14.25,"min":3,"max":14.5,"step":0.25,"unit":"segments"},{"id":"edgeColor","type":"color","label":"Edge line colour","default":"#B0B0B0"},{"id":"edgeOpacity","type":"number","label":"Edge line opacity","default":0.5,"min":0,"max":1,"step":0.05},{"id":"backdrop","type":"color","label":"Backdrop and fog colour","default":"#000000"}]}
|
||||
>
|
||||
|
||||
```html gallery-tunnel.html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Gallery Tunnel</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/build/three.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
#gt-root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#gt-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #000000;
|
||||
}
|
||||
#gt-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
/* Off-screen decode well: the <img> elements live in the DOM so the page
|
||||
load event waits for them, but they are never composited themselves —
|
||||
WebGL reads them as textures. The well is clipped to 1px; the images
|
||||
inside keep their natural layout size on purpose, because texImage2D
|
||||
uploads an <img> at its LAID-OUT size — sizing them down with CSS
|
||||
uploads a 1x1 texture and every panel samples one dark pixel. */
|
||||
#gt-assets {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!--
|
||||
Content slots. Every panel in the corridor is a slot, and `images` fills a
|
||||
share of them with your own artwork — drop the files in the project's
|
||||
assets/ and list them, either by editing the `images` default below or from
|
||||
the host composition:
|
||||
|
||||
<div
|
||||
data-composition-id="gallery-tunnel"
|
||||
data-composition-src="compositions/gallery-tunnel.html"
|
||||
data-start="0" data-duration="12" data-track-index="0"
|
||||
data-width="1920" data-height="1080"
|
||||
style="--images: assets/shot-1.png, assets/shot-2.png, assets/shot-3.png; --imagemix: 0.7"
|
||||
></div>
|
||||
|
||||
Any number of images works; they are dealt to slots by hash, so one image
|
||||
recurs at different depths rather than appearing once. Each is contain-fit
|
||||
inside its slot, so a 16:9 screenshot keeps its aspect. `imageMix` sets the
|
||||
image-vs-colour split (1 = every filled slot is an image), `fill` how many
|
||||
slots are filled at all.
|
||||
-->
|
||||
<div
|
||||
id="gt-root"
|
||||
data-composition-id="gallery-tunnel"
|
||||
data-root="true"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-composition-variables='[
|
||||
{"id":"images","type":"string","label":"Image slots: comma-separated paths, tiled into the panels (e.g. assets/shot-1.png,assets/shot-2.png)","default":"","placeholder":"assets/shot-1.png,assets/shot-2.png"},
|
||||
{"id":"imageMix","type":"number","label":"Image share of filled slots (0 = all colour, 1 = all images)","default":0.5,"min":0,"max":1,"step":0.05},
|
||||
{"id":"speed","type":"number","label":"Flight speed","default":3,"min":0,"max":8,"step":0.1,"unit":"segments/s"},
|
||||
{"id":"palette","type":"string","label":"Slab palette: comma-separated hex colours","default":"#FF6A00,#AB54F7,#EA3737,#0072E3,#00AA3C,#FFB200"},
|
||||
{"id":"grid","type":"number","label":"Grid density: slots per wall face per axis (2 = 4 per face, 16 per segment)","default":2,"min":1,"max":4,"step":1},
|
||||
{"id":"fill","type":"number","label":"Slot fill rate","default":0.5,"min":0,"max":1,"step":0.05},
|
||||
{"id":"fog","type":"number","label":"Fog distance: depth at which the corridor reaches pure backdrop","default":14.25,"min":3,"max":14.5,"step":0.25,"unit":"segments"},
|
||||
{"id":"edgeColor","type":"color","label":"Edge line colour","default":"#B0B0B0"},
|
||||
{"id":"edgeOpacity","type":"number","label":"Edge line opacity","default":0.5,"min":0,"max":1,"step":0.05},
|
||||
{"id":"backdrop","type":"color","label":"Backdrop and fog colour","default":"#000000"}
|
||||
]'
|
||||
>
|
||||
<div id="gt-backdrop"></div>
|
||||
<canvas id="gt-canvas" width="1920" height="1080"></canvas>
|
||||
<div id="gt-assets" aria-hidden="true" data-layout-allow-overflow></div>
|
||||
|
||||
<!-- Driver clip: gives HyperFrames a timed element to own on track 0. -->
|
||||
<div
|
||||
id="gt-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var DUR = 12;
|
||||
var W = 1920;
|
||||
var H = 1080;
|
||||
|
||||
// Fixed tunnel geometry, measured off the reference.
|
||||
var TUNNEL_WIDTH = 2;
|
||||
var TUNNEL_HEIGHT = 1.8;
|
||||
var SEGMENT_DEPTH = 1;
|
||||
var NUM_SEGMENTS = 15;
|
||||
var LINE_RADIUS = 0.003;
|
||||
var FOV = 75;
|
||||
var INSET = 0.94; // slab gutter inside its cell, so tiles read as discrete
|
||||
var SKIN = 0.002; // push slabs just behind the wall plane so edges stay on top
|
||||
|
||||
var ROOT = document.getElementById("gt-root");
|
||||
var CS = getComputedStyle(ROOT);
|
||||
|
||||
// `data-composition-variables` is the single owner of every declared
|
||||
// default — parse it rather than repeating each default a second time
|
||||
// in JS. `window.__hyperframes` is NOT guaranteed to exist yet when this
|
||||
// inline script runs, so it can only ever be an override, never the
|
||||
// source. Precedence: host CSS custom property, runtime variable bag,
|
||||
// declared default.
|
||||
var DECL = {};
|
||||
JSON.parse(ROOT.getAttribute("data-composition-variables") || "[]").forEach(function (v) {
|
||||
DECL[v.id] = v.default;
|
||||
});
|
||||
var HF = window.__hyperframes;
|
||||
var V = (HF && HF.getVariables && HF.getVariables()) || {};
|
||||
|
||||
function raw(id) {
|
||||
var css = CS.getPropertyValue("--" + id.toLowerCase()).trim();
|
||||
if (css !== "") return css;
|
||||
if (V[id] !== undefined && V[id] !== "") return V[id];
|
||||
return DECL[id];
|
||||
}
|
||||
function num(id) {
|
||||
var n = parseFloat(raw(id));
|
||||
return isFinite(n) ? n : 0;
|
||||
}
|
||||
function str(id) {
|
||||
var s = raw(id);
|
||||
return typeof s === "string" ? s.trim() : "";
|
||||
}
|
||||
function list(id) {
|
||||
return str(id)
|
||||
.split(",")
|
||||
.map(function (s) {
|
||||
return s.trim();
|
||||
})
|
||||
.filter(function (s) {
|
||||
return s !== "";
|
||||
});
|
||||
}
|
||||
|
||||
var SPEED = num("speed");
|
||||
var GRID = Math.max(1, Math.min(4, Math.round(num("grid"))));
|
||||
var FILL = num("fill");
|
||||
var IMAGE_MIX = num("imageMix");
|
||||
var FOG_FAR = Math.max(3, Math.min(14.5, num("fog")));
|
||||
var EDGE_OPACITY = num("edgeOpacity");
|
||||
var EDGE_COLOR = str("edgeColor");
|
||||
var BACKDROP = str("backdrop");
|
||||
var PALETTE = list("palette");
|
||||
var IMAGE_SRCS = list("images");
|
||||
|
||||
document.getElementById("gt-backdrop").style.background = BACKDROP;
|
||||
|
||||
// How far behind the camera a segment travels before it recycles. Tying
|
||||
// it to the fog distance is the whole trick: the recycle boundary lands
|
||||
// at exactly FOG_FAR, so a segment reappears already fogged to pure
|
||||
// backdrop and nothing ever pops in at the far end. One owner for the
|
||||
// invariant — move the fog and the boundary follows.
|
||||
var BACK = Math.max(0, Math.min(1.5, NUM_SEGMENTS * SEGMENT_DEPTH - FOG_FAR));
|
||||
|
||||
// Deterministic 2D integer hash — the only source of "randomness".
|
||||
// Content at frame N is a pure function of (absolute segment, slot).
|
||||
function hash2(a, b) {
|
||||
var h = Math.imul(a | 0, 0x27d4eb2d) ^ Math.imul(b | 0, 0x165667b1);
|
||||
h = Math.imul(h ^ (h >>> 15), 0x2545f491);
|
||||
h ^= h >>> 13;
|
||||
return (h >>> 0) / 4294967296;
|
||||
}
|
||||
|
||||
var THREEJS = window.THREE;
|
||||
var canvas = document.getElementById("gt-canvas");
|
||||
var renderer = new THREEJS.WebGLRenderer({
|
||||
canvas: canvas,
|
||||
antialias: true,
|
||||
alpha: false,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.setSize(W, H, false);
|
||||
renderer.setClearColor(new THREEJS.Color(BACKDROP), 1);
|
||||
|
||||
var scene = new THREEJS.Scene();
|
||||
scene.fog = new THREEJS.Fog(new THREEJS.Color(BACKDROP), 1, FOG_FAR * SEGMENT_DEPTH);
|
||||
|
||||
// Camera sits still at the origin looking down -z; the corridor moves.
|
||||
// Same picture as a moving camera, and the modulo keeps every coordinate
|
||||
// bounded no matter how long the clip runs.
|
||||
var camera = new THREEJS.PerspectiveCamera(FOV, W / H, 0.05, 40);
|
||||
camera.position.set(0, 0, 0);
|
||||
camera.lookAt(0, 0, -1);
|
||||
|
||||
var PLANE = new THREEJS.PlaneGeometry(1, 1);
|
||||
|
||||
var COLOR_MATS = PALETTE.map(function (hex) {
|
||||
return new THREEJS.MeshBasicMaterial({ color: new THREEJS.Color(hex), fog: true });
|
||||
});
|
||||
if (COLOR_MATS.length === 0) {
|
||||
COLOR_MATS.push(new THREEJS.MeshBasicMaterial({ color: 0xffffff, fog: true }));
|
||||
}
|
||||
|
||||
// Caller-supplied content slots. Real <img> elements (so the page load
|
||||
// event waits for the decode) used directly as WebGL textures.
|
||||
var assets = document.getElementById("gt-assets");
|
||||
var IMAGES = IMAGE_SRCS.map(function (src, k) {
|
||||
var el = document.createElement("img");
|
||||
el.id = "gt-img-" + k;
|
||||
el.alt = "";
|
||||
el.src = src;
|
||||
assets.appendChild(el);
|
||||
|
||||
var tex = new THREEJS.Texture(el);
|
||||
tex.minFilter = THREEJS.LinearFilter;
|
||||
tex.magFilter = THREEJS.LinearFilter;
|
||||
tex.generateMipmaps = false;
|
||||
var rec = {
|
||||
aspect: 0,
|
||||
mat: new THREEJS.MeshBasicMaterial({ map: tex, fog: true }),
|
||||
};
|
||||
function ready() {
|
||||
if (!el.naturalWidth) return;
|
||||
rec.aspect = el.naturalWidth / el.naturalHeight;
|
||||
tex.needsUpdate = true;
|
||||
}
|
||||
el.addEventListener("load", ready);
|
||||
ready(); // a cache hit can complete before the listener is attached
|
||||
return rec;
|
||||
});
|
||||
|
||||
var edgeMat = new THREEJS.MeshBasicMaterial({
|
||||
color: new THREEJS.Color(EDGE_COLOR),
|
||||
fog: true,
|
||||
transparent: true,
|
||||
opacity: EDGE_OPACITY,
|
||||
});
|
||||
|
||||
var HW = TUNNEL_WIDTH / 2;
|
||||
var HH = TUNNEL_HEIGHT / 2;
|
||||
var cellZ = SEGMENT_DEPTH / GRID;
|
||||
var cellX = TUNNEL_WIDTH / GRID;
|
||||
var cellY = TUNNEL_HEIGHT / GRID;
|
||||
|
||||
// Four faces of a segment box. `place` maps a cell to a world position
|
||||
// inside the segment (origin at the segment's near boundary, growing
|
||||
// toward -z); `rot` orients the unit plane into that face, inward-facing.
|
||||
// `cw`/`ch` are the cell's extent in the plane's own local axes.
|
||||
var FACES = [
|
||||
{
|
||||
// floor
|
||||
rot: [-Math.PI / 2, 0, 0],
|
||||
cw: cellX,
|
||||
ch: cellZ,
|
||||
place: function (iu, iz) {
|
||||
return [-HW + (iu + 0.5) * cellX, -HH - SKIN, -(iz + 0.5) * cellZ];
|
||||
},
|
||||
},
|
||||
{
|
||||
// ceiling
|
||||
rot: [Math.PI / 2, 0, 0],
|
||||
cw: cellX,
|
||||
ch: cellZ,
|
||||
place: function (iu, iz) {
|
||||
return [-HW + (iu + 0.5) * cellX, HH + SKIN, -(iz + 0.5) * cellZ];
|
||||
},
|
||||
},
|
||||
{
|
||||
// left wall
|
||||
rot: [0, Math.PI / 2, 0],
|
||||
cw: cellZ,
|
||||
ch: cellY,
|
||||
place: function (iu, iz) {
|
||||
return [-HW - SKIN, -HH + (iu + 0.5) * cellY, -(iz + 0.5) * cellZ];
|
||||
},
|
||||
},
|
||||
{
|
||||
// right wall
|
||||
rot: [0, -Math.PI / 2, 0],
|
||||
cw: cellZ,
|
||||
ch: cellY,
|
||||
place: function (iu, iz) {
|
||||
return [HW + SKIN, -HH + (iu + 0.5) * cellY, -(iz + 0.5) * cellZ];
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
var UP = new THREEJS.Vector3(0, 1, 0);
|
||||
// Edges are solid tube geometry, not lines: a cylinder thickens with
|
||||
// perspective the way a real moulding does, which is the whole reason
|
||||
// the corridor reads as built rather than drawn.
|
||||
function tube(group, ax, ay, az, bx, by, bz) {
|
||||
var dx = bx - ax,
|
||||
dy = by - ay,
|
||||
dz = bz - az;
|
||||
var len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (len < 1e-6) return;
|
||||
var mesh = new THREEJS.Mesh(
|
||||
new THREEJS.CylinderGeometry(LINE_RADIUS, LINE_RADIUS, len, 6, 1),
|
||||
edgeMat,
|
||||
);
|
||||
mesh.position.set((ax + bx) / 2, (ay + by) / 2, (az + bz) / 2);
|
||||
mesh.quaternion.setFromUnitVectors(UP, new THREEJS.Vector3(dx / len, dy / len, dz / len));
|
||||
group.add(mesh);
|
||||
}
|
||||
|
||||
function buildEdges(group) {
|
||||
var D = SEGMENT_DEPTH;
|
||||
var k, j, u, z;
|
||||
// The four corner runs (and any interior wall divisions) come from the
|
||||
// side walls; floor/ceiling only contribute their interior lines, so
|
||||
// corners are drawn once and never double-blend at 50% opacity.
|
||||
for (k = 1; k < GRID; k++) {
|
||||
u = -HW + k * cellX;
|
||||
tube(group, u, -HH, 0, u, -HH, -D);
|
||||
tube(group, u, HH, 0, u, HH, -D);
|
||||
}
|
||||
for (k = 0; k <= GRID; k++) {
|
||||
u = -HH + k * cellY;
|
||||
tube(group, -HW, u, 0, -HW, u, -D);
|
||||
tube(group, HW, u, 0, HW, u, -D);
|
||||
}
|
||||
// Ring lines at each depth boundary. The far boundary belongs to the
|
||||
// next segment, so stop one short and the tiling stays seamless.
|
||||
for (j = 0; j < GRID; j++) {
|
||||
z = -j * cellZ;
|
||||
tube(group, -HW, -HH, z, HW, -HH, z);
|
||||
tube(group, -HW, HH, z, HW, HH, z);
|
||||
tube(group, -HW, -HH, z, -HW, HH, z);
|
||||
tube(group, HW, -HH, z, HW, HH, z);
|
||||
}
|
||||
}
|
||||
|
||||
var segs = [];
|
||||
for (var i = 0; i < NUM_SEGMENTS; i++) {
|
||||
var group = new THREEJS.Group();
|
||||
var panels = [];
|
||||
for (var f = 0; f < FACES.length; f++) {
|
||||
var face = FACES[f];
|
||||
for (var iz = 0; iz < GRID; iz++) {
|
||||
for (var iu = 0; iu < GRID; iu++) {
|
||||
var mesh = new THREEJS.Mesh(PLANE, COLOR_MATS[0]);
|
||||
var p = face.place(iu, iz);
|
||||
mesh.position.set(p[0], p[1], p[2]);
|
||||
mesh.rotation.set(face.rot[0], face.rot[1], face.rot[2]);
|
||||
mesh.visible = false;
|
||||
group.add(mesh);
|
||||
panels.push({ mesh: mesh, cw: face.cw * INSET, ch: face.ch * INSET });
|
||||
}
|
||||
}
|
||||
}
|
||||
buildEdges(group);
|
||||
scene.add(group);
|
||||
segs.push({ group: group, panels: panels });
|
||||
}
|
||||
|
||||
// Contain-fit so a supplied screenshot keeps its aspect inside the slot
|
||||
// instead of being squashed to the cell (wall cells are portrait).
|
||||
function fit(p, ar) {
|
||||
if (!ar) {
|
||||
p.mesh.scale.set(p.cw, p.ch, 1);
|
||||
return;
|
||||
}
|
||||
if (ar > p.cw / p.ch) p.mesh.scale.set(p.cw, p.cw / ar, 1);
|
||||
else p.mesh.scale.set(p.ch * ar, p.ch, 1);
|
||||
}
|
||||
|
||||
// Everything about frame t is computed from t alone. `d` is the distance
|
||||
// flown in segments; `a` is the ABSOLUTE segment number occupying pool
|
||||
// slot i right now, which is what content hashes and the every-other
|
||||
// rhythm key off — so a segment carries the same panels every time it is
|
||||
// looked at, no matter how the timeline is scrubbed.
|
||||
function draw(t) {
|
||||
var d = SPEED * t;
|
||||
for (var i = 0; i < NUM_SEGMENTS; i++) {
|
||||
var seg = segs[i];
|
||||
var a = i + NUM_SEGMENTS * Math.ceil((d - i - BACK) / NUM_SEGMENTS);
|
||||
seg.group.position.z = -(a - d) * SEGMENT_DEPTH;
|
||||
|
||||
var lit = a % 2 === 0;
|
||||
var panels = seg.panels;
|
||||
for (var s = 0; s < panels.length; s++) {
|
||||
var p = panels[s];
|
||||
if (!lit || hash2(a, s * 4 + 1) >= FILL) {
|
||||
p.mesh.visible = false;
|
||||
continue;
|
||||
}
|
||||
p.mesh.visible = true;
|
||||
if (IMAGES.length > 0 && hash2(a, s * 4 + 2) < IMAGE_MIX) {
|
||||
var im = IMAGES[Math.floor(hash2(a, s * 4 + 4) * IMAGES.length) % IMAGES.length];
|
||||
p.mesh.material = im.mat;
|
||||
fit(p, im.aspect);
|
||||
} else {
|
||||
var ci = Math.floor(hash2(a, s * 4 + 3) * COLOR_MATS.length) % COLOR_MATS.length;
|
||||
p.mesh.material = COLOR_MATS[ci];
|
||||
p.mesh.scale.set(p.cw, p.ch, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Repaint from a property SETTER, not from onUpdate: gsap's seek(t)
|
||||
// suppresses events by default, so an onUpdate callback silently never
|
||||
// fires on a scrub and the canvas freezes on frame 0. Tweened values are
|
||||
// always written during render, suppressed or not.
|
||||
var driver = { _t: 0 };
|
||||
Object.defineProperty(driver, "t", {
|
||||
get: function () {
|
||||
return this._t;
|
||||
},
|
||||
set: function (v) {
|
||||
this._t = v;
|
||||
draw(v);
|
||||
},
|
||||
});
|
||||
tl.to(driver, { t: DUR, duration: DUR, ease: "none" }, 0);
|
||||
window.__timelines["gallery-tunnel"] = tl;
|
||||
|
||||
draw(0);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</VariablesExplorer>
|
||||
|
||||
## Install
|
||||
|
||||
<InstallCommand command="npx hyperframes add gallery-tunnel" />
|
||||
|
||||
That writes one file: `compositions/gallery-tunnel.html`.
|
||||
|
||||
## Add it to your video
|
||||
|
||||
It runs for 12 seconds at 1920×1080. Paste this into your composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="gallery-tunnel"
|
||||
data-composition-src="compositions/gallery-tunnel.html"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Change how it looks
|
||||
|
||||
Set these CSS variables on the block:
|
||||
|
||||
- `--images` — Image slots: comma-separated paths, tiled into the panels (e.g. assets/shot-1.png,assets/shot-2.png). Defaults to ``.
|
||||
- `--imagemix` — Image share of filled slots (0 = all colour, 1 = all images). Defaults to `0.5`.
|
||||
- `--speed` — Flight speed (segments per second). Defaults to `3`.
|
||||
- `--palette` — Slab palette: comma-separated hex colours. Defaults to `#FF6A00,#AB54F7,#EA3737,#0072E3,#00AA3C,#FFB200`.
|
||||
- `--grid` — Grid density: slots per wall face per axis (2 = 4 per face, 16 per segment). Defaults to `2`.
|
||||
- `--fill` — Slot fill rate. Defaults to `0.5`.
|
||||
- `--fog` — Fog distance in segments: depth at which the corridor reaches pure backdrop. Defaults to `14.25`.
|
||||
- `--edgecolor` — Edge line colour. Defaults to `#B0B0B0`.
|
||||
- `--edgeopacity` — Edge line opacity. Defaults to `0.5`.
|
||||
- `--backdrop` — Backdrop and fog colour. Defaults to `#000000`.
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `webgl` `3d` `background` `showcase` `depth` `loop`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
||||
- [Build a richer composition](/go-further)
|
||||
+378
-14
@@ -3,34 +3,398 @@ title: "Glitch"
|
||||
description: "Shader transition with digital glitch artifacts"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/glitch.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/glitch.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/glitch.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Glitch preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add glitch
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add glitch" />
|
||||
|
||||
That writes one file: `compositions/glitch.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`glitch.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="glitch"
|
||||
data-composition-src="compositions/glitch.html"
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<style>
|
||||
[data-composition-id="main"] {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #293241;
|
||||
}
|
||||
|
||||
/* Blueprint grid */
|
||||
.bp-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.bp-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.bp-right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 720px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 60px 60px 60px 40px;
|
||||
}
|
||||
.bp-divider {
|
||||
position: absolute;
|
||||
left: 1200px;
|
||||
top: 60px;
|
||||
bottom: 60px;
|
||||
width: 1px;
|
||||
background: #ee6c4d;
|
||||
opacity: 0.15;
|
||||
}
|
||||
.bp-number {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4em;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 60px;
|
||||
color: #ee6c4d;
|
||||
}
|
||||
|
||||
/* Right panel typography */
|
||||
.bp-name {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 38px;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 20px;
|
||||
color: #ee6c4d;
|
||||
}
|
||||
.bp-plabel {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
color: #ee6c4d;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.bp-prompt {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
padding: 16px 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
color: #ee6c4d;
|
||||
background: rgba(238, 108, 77, 0.06);
|
||||
border: 1px solid rgba(238, 108, 77, 0.12);
|
||||
}
|
||||
.bp-desc {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
color: #98c1d9;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* GL scenes (hidden, just for texture capture) */
|
||||
.scene {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.scene-label {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 140px;
|
||||
letter-spacing: 0.06em;
|
||||
opacity: 0.12;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="bp-grid"></div>
|
||||
<div class="bp-divider"></div>
|
||||
<div class="bp-number">09 / 14</div>
|
||||
|
||||
<!-- GL demo scenes (captured as textures, then hidden) -->
|
||||
<div id="s1" class="scene" style="background: #293241">
|
||||
<div class="scene-label" style="color: #ee6c4d">SCENE A</div>
|
||||
</div>
|
||||
<div id="s2" class="scene" style="background: #ee6c4d; opacity: 0">
|
||||
<div class="scene-label" style="color: #293241">SCENE B</div>
|
||||
</div>
|
||||
|
||||
<!-- GL canvas (left panel only) -->
|
||||
<canvas
|
||||
id="gl-canvas"
|
||||
width="1200"
|
||||
height="1080"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
"
|
||||
>
|
||||
</canvas>
|
||||
|
||||
<!-- Right panel metadata -->
|
||||
<div class="bp-right">
|
||||
<div class="bp-name">Glitch</div>
|
||||
<div class="bp-plabel">Prompt</div>
|
||||
<div class="bp-prompt">"use glitch shader transition"</div>
|
||||
<div class="bp-desc">
|
||||
Scan lines, block scramble, chromatic aberration, brightness flicker, and color
|
||||
posterization.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var glCanvas = document.getElementById("gl-canvas");
|
||||
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
|
||||
if (!gl) {
|
||||
console.warn("WebGL not available");
|
||||
window.__timelines["main"] = gsap.timeline({ paused: true });
|
||||
} else {
|
||||
gl.viewport(0, 0, 1200, 1080);
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
||||
var sceneTextures = {};
|
||||
|
||||
function captureScene(sceneId) {
|
||||
var scene = document.getElementById(sceneId);
|
||||
var origOpacity = scene.style.opacity;
|
||||
var origZ = scene.style.zIndex;
|
||||
scene.style.opacity = "1";
|
||||
scene.style.zIndex = "999";
|
||||
scene.offsetHeight;
|
||||
var c = document.createElement("canvas");
|
||||
c.width = 1200;
|
||||
c.height = 1080;
|
||||
var ctx = c.getContext("2d");
|
||||
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
|
||||
ctx.fillRect(0, 0, 1200, 1080);
|
||||
var sr = scene.getBoundingClientRect();
|
||||
var els = scene.querySelectorAll("*");
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
var el = els[i],
|
||||
cs = window.getComputedStyle(el);
|
||||
if (cs.display === "none" || cs.visibility === "hidden") continue;
|
||||
var r = el.getBoundingClientRect();
|
||||
if (r.width < 1 || r.height < 1) continue;
|
||||
var x = r.left - sr.left,
|
||||
y = r.top - sr.top,
|
||||
w = r.width,
|
||||
h = r.height;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
|
||||
var bg = cs.backgroundColor;
|
||||
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
}
|
||||
var hasChildEls = el.querySelector("div,span,img,video");
|
||||
var text = "";
|
||||
for (var j = 0; j < el.childNodes.length; j++)
|
||||
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
|
||||
text = text.trim();
|
||||
if (text && !hasChildEls) {
|
||||
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
|
||||
ctx.fillStyle = cs.color;
|
||||
if (cs.letterSpacing && cs.letterSpacing !== "normal")
|
||||
ctx.letterSpacing = cs.letterSpacing;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(text, x + w / 2, y + h / 2);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
scene.style.opacity = origOpacity;
|
||||
scene.style.zIndex = origZ;
|
||||
var tex = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
|
||||
sceneTextures[sceneId] = tex;
|
||||
}
|
||||
|
||||
var vertSrc =
|
||||
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
|
||||
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
|
||||
var quadBuf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.bufferData(
|
||||
gl.ARRAY_BUFFER,
|
||||
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
||||
gl.STATIC_DRAW,
|
||||
);
|
||||
function compileShader(src, type) {
|
||||
var s = gl.createShader(type);
|
||||
gl.shaderSource(s, src);
|
||||
gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
|
||||
console.error("Shader:", gl.getShaderInfoLog(s));
|
||||
return s;
|
||||
}
|
||||
function mkProg(fragSrc) {
|
||||
var p = gl.createProgram();
|
||||
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
|
||||
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
|
||||
gl.linkProgram(p);
|
||||
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
|
||||
console.error("Link:", gl.getProgramInfoLog(p));
|
||||
return p;
|
||||
}
|
||||
|
||||
var H =
|
||||
"precision mediump float;" +
|
||||
"varying vec2 v_uv;" +
|
||||
"uniform sampler2D u_from, u_to;" +
|
||||
"uniform float u_progress;" +
|
||||
"uniform vec2 u_resolution;\n";
|
||||
|
||||
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
|
||||
var progTrans = mkProg(
|
||||
H +
|
||||
"float rand(vec2 co){return fract(sin(dot(co,vec2(12.9898,78.233)))*43758.5453);}void main(){float inten=u_progress*(1.-u_progress)*4.;float lineY=floor(v_uv.y*60.)/60.;float lineDisp=(rand(vec2(lineY,floor(u_progress*17.)))-.5)*.18*inten;vec2 block=floor(v_uv*vec2(12.,8.));float br=rand(block+vec2(floor(u_progress*11.)));float ba=step(.83,br)*inten;vec2 bd=(vec2(rand(block*2.1),rand(block*3.7))-.5)*.35*ba;vec2 uv=clamp(v_uv+vec2(lineDisp,0.)+bd,0.,1.);float shift=inten*.035;float r=texture2D(u_from,uv+vec2(shift,0.)).r;float g=texture2D(u_from,uv).g;float b=texture2D(u_from,uv-vec2(shift,0.)).b;vec3 col=vec3(r,g,b);col-=step(.5,fract(v_uv.y*u_resolution.y*.5))*.05*inten;col*=1.+(rand(vec2(floor(u_progress*23.)))-.5)*.3*inten;float levels=mix(256.,8.,inten*.5);col=floor(col*levels)/levels;gl_FragColor=mix(vec4(col,1.),texture2D(u_to,v_uv),u_progress);}",
|
||||
);
|
||||
|
||||
function renderShader(prog, texFrom, texTo, progress) {
|
||||
gl.useProgram(prog);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texFrom);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texTo);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
|
||||
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
|
||||
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
|
||||
var pos = gl.getAttribLocation(prog, "a_pos");
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.enableVertexAttribArray(pos);
|
||||
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
function easeInOut(p) {
|
||||
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
|
||||
}
|
||||
|
||||
captureScene("s1");
|
||||
captureScene("s2");
|
||||
glCanvas.style.display = "block";
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
document.querySelectorAll(".scene").forEach(function (s) {
|
||||
s.style.opacity = "0";
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({
|
||||
paused: true,
|
||||
onUpdate: function () {
|
||||
var t = tl.time();
|
||||
if (t >= 1.0 && t < 3.0) {
|
||||
renderShader(
|
||||
progTrans,
|
||||
sceneTextures["s1"],
|
||||
sceneTextures["s2"],
|
||||
easeInOut((t - 1.0) / 2.0),
|
||||
);
|
||||
} else if (t >= 3.0) {
|
||||
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
|
||||
} else {
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
|
||||
window.__timelines["main"] = tl;
|
||||
} // end if (gl)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="glitch" data-composition-src="compositions/glitch.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,397 @@ title: "Gravitational Lens"
|
||||
description: "Shader transition with gravitational lensing distortion"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/gravitational-lens.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/gravitational-lens.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/gravitational-lens.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Gravitational Lens preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add gravitational-lens
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add gravitational-lens" />
|
||||
|
||||
That writes one file: `compositions/gravitational-lens.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`gravitational-lens.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="gravitational-lens"
|
||||
data-composition-src="compositions/gravitational-lens.html"
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<style>
|
||||
[data-composition-id="main"] {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #10002b;
|
||||
}
|
||||
|
||||
/* Blueprint grid */
|
||||
.bp-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.bp-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.bp-right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 720px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 60px 60px 60px 40px;
|
||||
}
|
||||
.bp-divider {
|
||||
position: absolute;
|
||||
left: 1200px;
|
||||
top: 60px;
|
||||
bottom: 60px;
|
||||
width: 1px;
|
||||
background: #f20089;
|
||||
opacity: 0.15;
|
||||
}
|
||||
.bp-number {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4em;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 60px;
|
||||
color: #f20089;
|
||||
}
|
||||
|
||||
/* Right panel typography */
|
||||
.bp-name {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 38px;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 20px;
|
||||
color: #f20089;
|
||||
}
|
||||
.bp-plabel {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
color: #f20089;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.bp-prompt {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
padding: 16px 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
color: #f20089;
|
||||
background: rgba(242, 0, 137, 0.06);
|
||||
border: 1px solid rgba(242, 0, 137, 0.12);
|
||||
}
|
||||
.bp-desc {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
color: #a080a0;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* GL scenes (hidden, just for texture capture) */
|
||||
.scene {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.scene-label {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 140px;
|
||||
letter-spacing: 0.06em;
|
||||
opacity: 0.12;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="bp-grid"></div>
|
||||
<div class="bp-divider"></div>
|
||||
<div class="bp-number">06 / 14</div>
|
||||
|
||||
<!-- GL demo scenes (captured as textures, then hidden) -->
|
||||
<div id="s1" class="scene" style="background: #10002b">
|
||||
<div class="scene-label" style="color: #f20089">SCENE A</div>
|
||||
</div>
|
||||
<div id="s2" class="scene" style="background: #f20089; opacity: 0">
|
||||
<div class="scene-label" style="color: #10002b">SCENE B</div>
|
||||
</div>
|
||||
|
||||
<!-- GL canvas (left panel only) -->
|
||||
<canvas
|
||||
id="gl-canvas"
|
||||
width="1200"
|
||||
height="1080"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
"
|
||||
>
|
||||
</canvas>
|
||||
|
||||
<!-- Right panel metadata -->
|
||||
<div class="bp-right">
|
||||
<div class="bp-name">Gravitational Lens</div>
|
||||
<div class="bp-plabel">Prompt</div>
|
||||
<div class="bp-prompt">"use gravitational lens shader transition"</div>
|
||||
<div class="bp-desc">
|
||||
Content warps toward a gravity well with chromatic aberration and event horizon darkening.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var glCanvas = document.getElementById("gl-canvas");
|
||||
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
|
||||
if (!gl) {
|
||||
console.warn("WebGL not available");
|
||||
window.__timelines["main"] = gsap.timeline({ paused: true });
|
||||
} else {
|
||||
gl.viewport(0, 0, 1200, 1080);
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
||||
var sceneTextures = {};
|
||||
|
||||
function captureScene(sceneId) {
|
||||
var scene = document.getElementById(sceneId);
|
||||
var origOpacity = scene.style.opacity;
|
||||
var origZ = scene.style.zIndex;
|
||||
scene.style.opacity = "1";
|
||||
scene.style.zIndex = "999";
|
||||
scene.offsetHeight;
|
||||
var c = document.createElement("canvas");
|
||||
c.width = 1200;
|
||||
c.height = 1080;
|
||||
var ctx = c.getContext("2d");
|
||||
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
|
||||
ctx.fillRect(0, 0, 1200, 1080);
|
||||
var sr = scene.getBoundingClientRect();
|
||||
var els = scene.querySelectorAll("*");
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
var el = els[i],
|
||||
cs = window.getComputedStyle(el);
|
||||
if (cs.display === "none" || cs.visibility === "hidden") continue;
|
||||
var r = el.getBoundingClientRect();
|
||||
if (r.width < 1 || r.height < 1) continue;
|
||||
var x = r.left - sr.left,
|
||||
y = r.top - sr.top,
|
||||
w = r.width,
|
||||
h = r.height;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
|
||||
var bg = cs.backgroundColor;
|
||||
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
}
|
||||
var hasChildEls = el.querySelector("div,span,img,video");
|
||||
var text = "";
|
||||
for (var j = 0; j < el.childNodes.length; j++)
|
||||
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
|
||||
text = text.trim();
|
||||
if (text && !hasChildEls) {
|
||||
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
|
||||
ctx.fillStyle = cs.color;
|
||||
if (cs.letterSpacing && cs.letterSpacing !== "normal")
|
||||
ctx.letterSpacing = cs.letterSpacing;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(text, x + w / 2, y + h / 2);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
scene.style.opacity = origOpacity;
|
||||
scene.style.zIndex = origZ;
|
||||
var tex = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
|
||||
sceneTextures[sceneId] = tex;
|
||||
}
|
||||
|
||||
var vertSrc =
|
||||
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
|
||||
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
|
||||
var quadBuf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.bufferData(
|
||||
gl.ARRAY_BUFFER,
|
||||
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
||||
gl.STATIC_DRAW,
|
||||
);
|
||||
function compileShader(src, type) {
|
||||
var s = gl.createShader(type);
|
||||
gl.shaderSource(s, src);
|
||||
gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
|
||||
console.error("Shader:", gl.getShaderInfoLog(s));
|
||||
return s;
|
||||
}
|
||||
function mkProg(fragSrc) {
|
||||
var p = gl.createProgram();
|
||||
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
|
||||
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
|
||||
gl.linkProgram(p);
|
||||
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
|
||||
console.error("Link:", gl.getProgramInfoLog(p));
|
||||
return p;
|
||||
}
|
||||
|
||||
var H =
|
||||
"precision mediump float;" +
|
||||
"varying vec2 v_uv;" +
|
||||
"uniform sampler2D u_from, u_to;" +
|
||||
"uniform float u_progress;" +
|
||||
"uniform vec2 u_resolution;\n";
|
||||
|
||||
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
|
||||
var progTrans = mkProg(
|
||||
H +
|
||||
"void main(){vec4 B=texture2D(u_to,v_uv);vec2 uv=v_uv-.5;float dist=length(uv);float pull=u_progress*2.;float warpStr=pull*.3/(dist+.1);vec2 warped=clamp(v_uv-uv*warpStr,0.,1.);vec4 A=texture2D(u_from,warped);float horizon=smoothstep(0.,.3,dist/(1.-u_progress*.85+.001));float shift=pull*.02/(dist+.2);float r=texture2D(u_from,clamp(v_uv-uv*(warpStr+shift),0.,1.)).r;float b=texture2D(u_from,clamp(v_uv-uv*(warpStr-shift),0.,1.)).b;vec3 lensed=vec3(r,A.g,b)*horizon;gl_FragColor=vec4(mix(lensed,B.rgb,smoothstep(.3,.9,u_progress)),1.);}",
|
||||
);
|
||||
|
||||
function renderShader(prog, texFrom, texTo, progress) {
|
||||
gl.useProgram(prog);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texFrom);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texTo);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
|
||||
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
|
||||
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
|
||||
var pos = gl.getAttribLocation(prog, "a_pos");
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.enableVertexAttribArray(pos);
|
||||
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
function easeInOut(p) {
|
||||
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
|
||||
}
|
||||
|
||||
captureScene("s1");
|
||||
captureScene("s2");
|
||||
glCanvas.style.display = "block";
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
document.querySelectorAll(".scene").forEach(function (s) {
|
||||
s.style.opacity = "0";
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({
|
||||
paused: true,
|
||||
onUpdate: function () {
|
||||
var t = tl.time();
|
||||
if (t >= 1.0 && t < 3.0) {
|
||||
renderShader(
|
||||
progTrans,
|
||||
sceneTextures["s1"],
|
||||
sceneTextures["s2"],
|
||||
easeInOut((t - 1.0) / 2.0),
|
||||
);
|
||||
} else if (t >= 3.0) {
|
||||
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
|
||||
} else {
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
|
||||
window.__timelines["main"] = tl;
|
||||
} // end if (gl)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="gravitational-lens" data-composition-src="compositions/gravitational-lens.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -0,0 +1,448 @@
|
||||
---
|
||||
title: "Halftone Field"
|
||||
description: "A full-bleed halftone light field: a grid of dots whose size and colour track a slow flowing simplex-noise field, so broad bands of light sweep across a mostly-black frame like a giant LED wall showing a plasma. Built to run under type."
|
||||
---
|
||||
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
|
||||
|
||||
<VariablesExplorer
|
||||
previewSrc="/public/catalog/preview/blocks/halftone-field.html"
|
||||
compositionId="halftone-field"
|
||||
compositionSrc="compositions/halftone-field.html"
|
||||
variables={[{"id":"frequency","type":"number","label":"Field frequency","default":2,"min":1,"max":10,"step":0.1},{"id":"speed","type":"number","label":"Flow speed","default":5,"min":0,"max":10,"step":0.1},{"id":"cellSize","type":"number","label":"Cell size","default":54,"min":1,"max":100,"step":1},{"id":"gamma","type":"number","label":"Gamma (midtone crush)","default":12,"min":1,"max":20,"step":0.1},{"id":"paletteBias","type":"number","label":"Palette bias","default":2,"min":0,"max":20,"step":0.5},{"id":"palette1","type":"color","label":"Palette stop 1 (lows)","default":"#00AAFF"},{"id":"palette2","type":"color","label":"Palette stop 2","default":"#C2FF67"},{"id":"palette3","type":"color","label":"Palette stop 3","default":"#6600FF"},{"id":"palette4","type":"color","label":"Palette stop 4 (crests)","default":"#000000"},{"id":"background","type":"color","label":"Background","default":"#000000"},{"id":"quantize","type":"boolean","label":"Quantise to 30fps (stepped)","default":false}]}
|
||||
>
|
||||
|
||||
```html halftone-field.html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Halftone Field</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
#hlf-root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#hlf-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #000000;
|
||||
}
|
||||
#hlf-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="hlf-root"
|
||||
data-composition-id="halftone-field"
|
||||
data-root="true"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="10"
|
||||
data-composition-variables='[
|
||||
{"id":"frequency","type":"number","label":"Field frequency","default":2,"min":1,"max":10,"step":0.1},
|
||||
{"id":"speed","type":"number","label":"Flow speed","default":5,"min":0,"max":10,"step":0.1},
|
||||
{"id":"cellSize","type":"number","label":"Cell size","default":54,"min":1,"max":100,"step":1},
|
||||
{"id":"gamma","type":"number","label":"Gamma (midtone crush)","default":12,"min":1,"max":20,"step":0.1},
|
||||
{"id":"paletteBias","type":"number","label":"Palette bias","default":2,"min":0,"max":20,"step":0.5},
|
||||
{"id":"palette1","type":"color","label":"Palette stop 1 (lows)","default":"#00AAFF"},
|
||||
{"id":"palette2","type":"color","label":"Palette stop 2","default":"#C2FF67"},
|
||||
{"id":"palette3","type":"color","label":"Palette stop 3","default":"#6600FF"},
|
||||
{"id":"palette4","type":"color","label":"Palette stop 4 (crests)","default":"#000000"},
|
||||
{"id":"background","type":"color","label":"Background","default":"#000000"},
|
||||
{"id":"quantize","type":"boolean","label":"Quantise to 30fps (stepped)","default":false}
|
||||
]'
|
||||
>
|
||||
<div id="hlf-backdrop"></div>
|
||||
<canvas id="hlf-canvas" width="1920" height="1080"></canvas>
|
||||
|
||||
<!-- Driver clip: gives HyperFrames a timed element to own on track 0. -->
|
||||
<div
|
||||
id="hlf-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="10"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var DUR = 10;
|
||||
var W = 1920;
|
||||
var H = 1080;
|
||||
|
||||
var V = (window.__hyperframes && window.__hyperframes.getVariables()) || {};
|
||||
var CS = getComputedStyle(document.getElementById("hlf-root"));
|
||||
|
||||
// The runtime defines every declared variable as `--<id>` on the root,
|
||||
// so a host stylesheet can override one there too. Read the custom
|
||||
// property first, fall back to the declared value when it is unset.
|
||||
function raw(id) {
|
||||
var css = CS.getPropertyValue("--" + id.toLowerCase()).trim();
|
||||
return css !== "" ? css : V[id];
|
||||
}
|
||||
function num(id, fallback) {
|
||||
var n = parseFloat(raw(id));
|
||||
return isFinite(n) ? n : fallback;
|
||||
}
|
||||
function bool(id) {
|
||||
var v = raw(id);
|
||||
return v === true || /^(true|1|on|yes)$/i.test(String(v));
|
||||
}
|
||||
// #RGB / #RRGGBB -> [r, g, b] in 0..1. Anything else falls back.
|
||||
function rgb(id, fallback) {
|
||||
var s = String(raw(id) || "").trim();
|
||||
var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(s);
|
||||
if (!m) s = fallback;
|
||||
else s = "#" + m[1];
|
||||
var hex = s.slice(1);
|
||||
if (hex.length === 3) {
|
||||
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
||||
}
|
||||
var n = parseInt(hex, 16);
|
||||
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
|
||||
}
|
||||
// The reference exposes 1-10 / 1-100 / 1-20 dials and maps them onto
|
||||
// the ranges the shader actually wants.
|
||||
function mapLinear(x, a1, a2, b1, b2) {
|
||||
return b1 + ((x - a1) * (b2 - b1)) / (a2 - a1);
|
||||
}
|
||||
|
||||
var FREQ = mapLinear(num("frequency", 2), 1, 10, 0.3, 6);
|
||||
var SPEED = num("speed", 5) * 0.05;
|
||||
var CELL = mapLinear(num("cellSize", 54), 1, 100, 6, 60);
|
||||
var GAMMA = mapLinear(num("gamma", 12), 1, 20, 0.5, 8);
|
||||
var BIAS = num("paletteBias", 2) * 0.05;
|
||||
var PAL = [
|
||||
rgb("palette1", "#00AAFF"),
|
||||
rgb("palette2", "#C2FF67"),
|
||||
rgb("palette3", "#6600FF"),
|
||||
rgb("palette4", "#000000"),
|
||||
];
|
||||
var BG = rgb("background", "#000000");
|
||||
var QUANTIZE = bool("quantize");
|
||||
|
||||
document.getElementById("hlf-backdrop").style.background =
|
||||
"rgb(" +
|
||||
BG.map(function (c) {
|
||||
return Math.round(c * 255);
|
||||
}).join(",") +
|
||||
")";
|
||||
|
||||
var canvas = document.getElementById("hlf-canvas");
|
||||
var gl =
|
||||
canvas.getContext("webgl", {
|
||||
alpha: false,
|
||||
antialias: false,
|
||||
depth: false,
|
||||
stencil: false,
|
||||
preserveDrawingBuffer: true,
|
||||
powerPreference: "high-performance",
|
||||
}) ||
|
||||
canvas.getContext("experimental-webgl", {
|
||||
alpha: false,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
|
||||
var VERT = "attribute vec2 aPos;\nvoid main() { gl_Position = vec4(aPos, 0.0, 1.0); }";
|
||||
|
||||
// Single pass. The reference renders the colour field into a target and
|
||||
// samples it at each cell centre, but the field is analytic, so
|
||||
// evaluating it at the cell centre here is the same value without the
|
||||
// round trip through a texture.
|
||||
var FRAG = `
|
||||
precision highp float;
|
||||
uniform vec2 uRes;
|
||||
uniform float uTime;
|
||||
uniform float uFreq;
|
||||
uniform float uSpeed;
|
||||
uniform float uCell;
|
||||
uniform float uGamma;
|
||||
uniform float uBias;
|
||||
uniform vec3 uPal0;
|
||||
uniform vec3 uPal1;
|
||||
uniform vec3 uPal2;
|
||||
uniform vec3 uPal3;
|
||||
uniform vec3 uBg;
|
||||
|
||||
// 3D simplex noise — Ashima Arts / Stefan Gustavson (MIT).
|
||||
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
||||
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
||||
vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }
|
||||
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
|
||||
|
||||
float snoise(vec3 v) {
|
||||
const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
|
||||
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
||||
|
||||
vec3 i = floor(v + dot(v, C.yyy));
|
||||
vec3 x0 = v - i + dot(i, C.xxx);
|
||||
|
||||
vec3 g = step(x0.yzx, x0.xyz);
|
||||
vec3 l = 1.0 - g;
|
||||
vec3 i1 = min(g.xyz, l.zxy);
|
||||
vec3 i2 = max(g.xyz, l.zxy);
|
||||
|
||||
vec3 x1 = x0 - i1 + C.xxx;
|
||||
vec3 x2 = x0 - i2 + C.yyy;
|
||||
vec3 x3 = x0 - D.yyy;
|
||||
|
||||
i = mod289(i);
|
||||
vec4 p = permute(permute(permute(
|
||||
i.z + vec4(0.0, i1.z, i2.z, 1.0)) +
|
||||
i.y + vec4(0.0, i1.y, i2.y, 1.0)) +
|
||||
i.x + vec4(0.0, i1.x, i2.x, 1.0));
|
||||
|
||||
float n_ = 0.142857142857;
|
||||
vec3 ns = n_ * D.wyz - D.xzx;
|
||||
|
||||
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
||||
|
||||
vec4 x_ = floor(j * ns.z);
|
||||
vec4 y_ = floor(j - 7.0 * x_);
|
||||
|
||||
vec4 x = x_ * ns.x + ns.yyyy;
|
||||
vec4 y = y_ * ns.x + ns.yyyy;
|
||||
vec4 h = 1.0 - abs(x) - abs(y);
|
||||
|
||||
vec4 b0 = vec4(x.xy, y.xy);
|
||||
vec4 b1 = vec4(x.zw, y.zw);
|
||||
|
||||
vec4 s0 = floor(b0) * 2.0 + 1.0;
|
||||
vec4 s1 = floor(b1) * 2.0 + 1.0;
|
||||
vec4 sh = -step(h, vec4(0.0));
|
||||
|
||||
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
|
||||
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
|
||||
|
||||
vec3 p0 = vec3(a0.xy, h.x);
|
||||
vec3 p1 = vec3(a0.zw, h.y);
|
||||
vec3 p2 = vec3(a1.xy, h.z);
|
||||
vec3 p3 = vec3(a1.zw, h.w);
|
||||
|
||||
vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
|
||||
p0 *= norm.x;
|
||||
p1 *= norm.y;
|
||||
p2 *= norm.z;
|
||||
p3 *= norm.w;
|
||||
|
||||
vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
|
||||
m = m * m;
|
||||
return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
|
||||
}
|
||||
|
||||
// Full-saturation, full-value HSV: only the hue ramp survives.
|
||||
vec3 hue2rgb(float h) {
|
||||
return clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// Piecewise-linear ramp across the four palette stops.
|
||||
vec3 ramp(float v) {
|
||||
float x = clamp(v, 0.0, 1.0) * 3.0;
|
||||
vec3 c = mix(uPal0, uPal1, clamp(x, 0.0, 1.0));
|
||||
c = mix(c, uPal2, clamp(x - 1.0, 0.0, 1.0));
|
||||
c = mix(c, uPal3, clamp(x - 2.0, 0.0, 1.0));
|
||||
return c;
|
||||
}
|
||||
|
||||
void main() {
|
||||
// One sample per cell, taken at the cell centre.
|
||||
vec2 cell = floor(gl_FragCoord.xy / uCell);
|
||||
vec2 centre = (cell + 0.5) * uCell;
|
||||
|
||||
vec2 st = centre / uRes;
|
||||
st.x *= uRes.x / uRes.y;
|
||||
|
||||
float hue = abs(snoise(vec3(st * uFreq, uTime * uSpeed)));
|
||||
vec3 field = hue2rgb(hue);
|
||||
|
||||
// The gamma is the whole look: it crushes the midtones so most of the frame
|
||||
// stays background and only the crests of the field open up.
|
||||
float gray = pow(dot(field, vec3(0.2126, 0.7152, 0.0722)), uGamma);
|
||||
float v = clamp(gray + uBias, 0.0, 1.0);
|
||||
|
||||
float radius = v * 0.5 * uCell;
|
||||
float d = length(gl_FragCoord.xy - centre);
|
||||
float mask = 1.0 - smoothstep(radius - 1.0, radius + 1.0, d);
|
||||
|
||||
gl_FragColor = vec4(mix(uBg, ramp(v), mask), 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
var uni = {};
|
||||
var ready = false;
|
||||
|
||||
function compile(type, src) {
|
||||
var sh = gl.createShader(type);
|
||||
gl.shaderSource(sh, src);
|
||||
gl.compileShader(sh);
|
||||
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
||||
throw new Error("halftone-field shader: " + gl.getShaderInfoLog(sh));
|
||||
}
|
||||
return sh;
|
||||
}
|
||||
|
||||
if (gl) {
|
||||
var prog = gl.createProgram();
|
||||
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
|
||||
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
|
||||
gl.linkProgram(prog);
|
||||
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
||||
throw new Error("halftone-field link: " + gl.getProgramInfoLog(prog));
|
||||
}
|
||||
gl.useProgram(prog);
|
||||
|
||||
var buf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
||||
var loc = gl.getAttribLocation(prog, "aPos");
|
||||
gl.enableVertexAttribArray(loc);
|
||||
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
[
|
||||
"uRes",
|
||||
"uTime",
|
||||
"uFreq",
|
||||
"uSpeed",
|
||||
"uCell",
|
||||
"uGamma",
|
||||
"uBias",
|
||||
"uPal0",
|
||||
"uPal1",
|
||||
"uPal2",
|
||||
"uPal3",
|
||||
"uBg",
|
||||
].forEach(function (n) {
|
||||
uni[n] = gl.getUniformLocation(prog, n);
|
||||
});
|
||||
|
||||
gl.viewport(0, 0, W, H);
|
||||
gl.uniform2f(uni.uRes, W, H);
|
||||
gl.uniform1f(uni.uFreq, FREQ);
|
||||
gl.uniform1f(uni.uSpeed, SPEED);
|
||||
gl.uniform1f(uni.uCell, CELL);
|
||||
gl.uniform1f(uni.uGamma, GAMMA);
|
||||
gl.uniform1f(uni.uBias, BIAS);
|
||||
gl.uniform3f(uni.uPal0, PAL[0][0], PAL[0][1], PAL[0][2]);
|
||||
gl.uniform3f(uni.uPal1, PAL[1][0], PAL[1][1], PAL[1][2]);
|
||||
gl.uniform3f(uni.uPal2, PAL[2][0], PAL[2][1], PAL[2][2]);
|
||||
gl.uniform3f(uni.uPal3, PAL[3][0], PAL[3][1], PAL[3][2]);
|
||||
gl.uniform3f(uni.uBg, BG[0], BG[1], BG[2]);
|
||||
ready = true;
|
||||
}
|
||||
|
||||
// Every frame is computed from t alone — the field is a closed-form
|
||||
// function of position and time. No accumulator, no clock, no PRNG.
|
||||
// `quantize` snaps t to a 30fps grid for the stepped LED-wall look.
|
||||
function draw(t) {
|
||||
if (!ready) return;
|
||||
gl.uniform1f(uni.uTime, QUANTIZE ? Math.floor(t * 30) / 30 : t);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||
gl.flush();
|
||||
}
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
// The canvas is repainted from a property SETTER, not from onUpdate:
|
||||
// gsap's seek(t) suppresses events by default, so an onUpdate callback
|
||||
// silently never fires on a scrub and the canvas freezes on frame 0.
|
||||
// Tweened values are always written during render, suppressed or not,
|
||||
// so this fires on every seek — and hands us the frame time directly.
|
||||
var driver = { _t: 0 };
|
||||
Object.defineProperty(driver, "t", {
|
||||
get: function () {
|
||||
return this._t;
|
||||
},
|
||||
set: function (v) {
|
||||
this._t = v;
|
||||
draw(v);
|
||||
},
|
||||
});
|
||||
tl.to(driver, { t: DUR, duration: DUR, ease: "none" }, 0);
|
||||
window.__timelines["halftone-field"] = tl;
|
||||
|
||||
draw(0);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</VariablesExplorer>
|
||||
|
||||
## Install
|
||||
|
||||
<InstallCommand command="npx hyperframes add halftone-field" />
|
||||
|
||||
That writes one file: `compositions/halftone-field.html`.
|
||||
|
||||
## Add it to your video
|
||||
|
||||
It runs for 10 seconds at 1920×1080. Paste this into your composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="halftone-field"
|
||||
data-composition-src="compositions/halftone-field.html"
|
||||
data-start="0"
|
||||
data-duration="10"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Change how it looks
|
||||
|
||||
Set these CSS variables on the block:
|
||||
|
||||
- `--frequency` — Field frequency (1 = broad swells, 10 = fine mottle). Defaults to `2`.
|
||||
- `--speed` — Flow speed (0 freezes the field). Defaults to `5`.
|
||||
- `--cellsize` — Cell size (dot pitch, 1 = 6px, 100 = 60px). Defaults to `54`.
|
||||
- `--gamma` — Gamma: midtone crush, the whole look. Low values flatten it to an even grey dot field. Defaults to `12`.
|
||||
- `--palettebias` — Palette bias: floor under every dot's size and palette position. 0 lets the darks go fully black. Defaults to `2`.
|
||||
- `--palette1` — Palette stop 1 (lows). Defaults to `#00AAFF`.
|
||||
- `--palette2` — Palette stop 2. Defaults to `#C2FF67`.
|
||||
- `--palette3` — Palette stop 3. Defaults to `#6600FF`.
|
||||
- `--palette4` — Palette stop 4 (crests). Defaults to `#000000`.
|
||||
- `--background` — Background. Defaults to `#000000`.
|
||||
- `--quantize` — Quantise to 30fps for a stepped LED-wall look. Defaults to `false`. Options: `false`, `true`.
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `webgl` `shader` `background` `texture` `halftone`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
||||
- [Build a richer composition](/go-further)
|
||||
@@ -3,13 +3,13 @@ title: "Sketched Frame"
|
||||
description: "Media in a hand-drawn border with corner doodles and a handwritten caption; the unit tilts slightly and boils (images inside the block; keep live footage in the host)"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-frame.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-frame.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/hw-frame.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Sketched Frame preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add hw-frame
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add hw-frame" />
|
||||
|
||||
That writes `compositions/hw-frame.html`, plus 1 supporting file under `assets/fonts/`.
|
||||
|
||||
@@ -32,6 +32,317 @@ It runs for 8 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`hw-frame.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Caveat";
|
||||
src: url("assets/fonts/Caveat-700-latin.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-display: block;
|
||||
}
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
#hw-fr-root {
|
||||
--hw-font-print: "Caveat", cursive;
|
||||
--hw-font-script: "Caveat", cursive;
|
||||
--hw-ink: #f4f2ec;
|
||||
--hw-accent-warm: #ffb020;
|
||||
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #1b1a1e;
|
||||
font-family: var(--hw-font-print, "Caveat", cursive);
|
||||
}
|
||||
#hw-fr-media {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
}
|
||||
#hw-fr-media img,
|
||||
#hw-fr-media .hw-fr-fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.hw-fr-fill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 110px;
|
||||
color: rgba(244, 242, 236, 0.9);
|
||||
background: linear-gradient(150deg, #35604a 0%, #14231c 100%);
|
||||
}
|
||||
#hw-fr-svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
#hw-fr-svg path {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
#hw-fr-border {
|
||||
stroke: var(--hw-ink);
|
||||
stroke-width: 7;
|
||||
}
|
||||
.hw-fr-doodle {
|
||||
stroke: var(--hw-accent-warm);
|
||||
stroke-width: 5;
|
||||
}
|
||||
#hw-fr-caption {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 58px;
|
||||
color: var(--hw-ink);
|
||||
opacity: 0;
|
||||
text-shadow: 0 2px 14px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="hw-fr-root"
|
||||
data-composition-id="hw-frame"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="hw-fr-media"></div>
|
||||
<svg id="hw-fr-svg" viewBox="0 0 1920 1080"><path id="hw-fr-border"></path></svg>
|
||||
<div id="hw-fr-caption"></div>
|
||||
<div
|
||||
id="hw-fr-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
/* ---- published parameters ----
|
||||
A sketched frame: content inside a hand-drawn border with
|
||||
corner doodles + optional caption. IMAGES ONLY inside this block
|
||||
(learning: the runtime never seeks media elements inside
|
||||
sub-compositions) — for live footage keep the media element in
|
||||
the HOST root positioned over the documented media slot below;
|
||||
layering two framed clips = instant PiP. */
|
||||
var CONFIG = {
|
||||
src: null, // IMAGE path; null -> stand-in gradient (demo)
|
||||
slot: { x: 480, y: 180, w: 960, h: 620 }, // the media window (documented for host-side footage)
|
||||
caption: "our trip!",
|
||||
doodles: true, // corner sparks
|
||||
tilt: -1.2, // deg — the whole framed unit sits slightly rotated
|
||||
seed: 14,
|
||||
boil: { enabled: true, amp: 1.6, rot: 0.35, frameDrop: 3 },
|
||||
animationIn: true,
|
||||
animationOut: true,
|
||||
};
|
||||
|
||||
var DUR = 8;
|
||||
function clamp(v, lo, hi) {
|
||||
return Math.min(hi, Math.max(lo, v));
|
||||
}
|
||||
var IN = clamp(DUR * 0.08, 0.3, 0.8);
|
||||
var OUT = clamp(DUR * 0.06, 0.25, 0.6);
|
||||
|
||||
/* hw family runtime */
|
||||
window.hwOnUpdate = function (tl, fn) {
|
||||
if (!tl.__hwRenders) {
|
||||
tl.__hwRenders = [];
|
||||
tl.eventCallback("onUpdate", function () {
|
||||
for (var i = 0; i < tl.__hwRenders.length; i++) tl.__hwRenders[i]();
|
||||
});
|
||||
}
|
||||
tl.__hwRenders.push(fn);
|
||||
fn();
|
||||
};
|
||||
window.hwHash = function (n, seed) {
|
||||
var x = Math.sin(n * 127.1 + (seed || 1) * 311.7) * 43758.5453;
|
||||
return (x - Math.floor(x)) * 2 - 1;
|
||||
};
|
||||
window.hwBoil = function (tl, target, opts) {
|
||||
opts = opts || {};
|
||||
var amp = opts.amp !== undefined ? opts.amp : 1.6;
|
||||
var rot = opts.rot !== undefined ? opts.rot : 0.5;
|
||||
var fps = opts.fps || 30;
|
||||
var drop = opts.frameDrop || 3;
|
||||
var seed = opts.seed || 1;
|
||||
var els = gsap.utils.toArray(target);
|
||||
window.hwOnUpdate(tl, function () {
|
||||
var step = Math.floor((tl.time() * fps) / drop);
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
gsap.set(els[i], {
|
||||
x: window.hwHash(step * 3 + i * 97, seed) * amp,
|
||||
y: window.hwHash(step * 3 + 1 + i * 97, seed) * amp,
|
||||
rotation: (opts.baseRot || 0) + window.hwHash(step * 3 + 2 + i * 97, seed) * rot,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
window.hwWobbleRect = function (w, h, r, seed, amp) {
|
||||
amp = amp === undefined ? 3.5 : amp;
|
||||
function wob(n) {
|
||||
return window.hwHash(n, seed) * amp;
|
||||
}
|
||||
var d = "M " + (r + wob(1)) + " " + wob(2);
|
||||
d += " L " + (w / 2 + wob(3)) + " " + wob(4);
|
||||
d += " L " + (w - r + wob(5)) + " " + wob(6);
|
||||
d += " Q " + (w + wob(7)) + " " + wob(8) + " " + (w + wob(9)) + " " + (r + wob(10));
|
||||
d += " L " + (w + wob(11)) + " " + (h / 2 + wob(12));
|
||||
d += " L " + (w + wob(13)) + " " + (h - r + wob(14));
|
||||
d +=
|
||||
" Q " +
|
||||
(w + wob(15)) +
|
||||
" " +
|
||||
(h + wob(16)) +
|
||||
" " +
|
||||
(w - r + wob(17)) +
|
||||
" " +
|
||||
(h + wob(18));
|
||||
d += " L " + (w / 2 + wob(19)) + " " + (h + wob(20));
|
||||
d += " L " + (r + wob(21)) + " " + (h + wob(22));
|
||||
d += " Q " + wob(23) + " " + (h + wob(24)) + " " + wob(25) + " " + (h - r + wob(26));
|
||||
d += " L " + wob(27) + " " + (h / 2 + wob(28));
|
||||
d += " L " + wob(29) + " " + (r + wob(30));
|
||||
d += " Q " + wob(31) + " " + wob(32) + " " + (r + wob(33)) + " " + wob(34);
|
||||
return d;
|
||||
};
|
||||
|
||||
var S = CONFIG.slot;
|
||||
var media = document.getElementById("hw-fr-media");
|
||||
media.style.left = S.x + "px";
|
||||
media.style.top = S.y + "px";
|
||||
media.style.width = S.w + "px";
|
||||
media.style.height = S.h + "px";
|
||||
if (CONFIG.src) {
|
||||
var img = document.createElement("img");
|
||||
img.src = CONFIG.src;
|
||||
media.appendChild(img);
|
||||
} else {
|
||||
var d = document.createElement("div");
|
||||
d.className = "hw-fr-fill";
|
||||
d.textContent = "photo";
|
||||
media.appendChild(d);
|
||||
}
|
||||
|
||||
/* border drawn slightly outside the slot */
|
||||
var NS = "http://www.w3.org/2000/svg";
|
||||
var border = document.getElementById("hw-fr-border");
|
||||
var m = 16;
|
||||
border.setAttribute("d", window.hwWobbleRect(S.w + m * 2, S.h + m * 2, 18, CONFIG.seed, 5));
|
||||
border.setAttribute("transform", "translate(" + (S.x - m) + " " + (S.y - m) + ")");
|
||||
|
||||
/* corner doodles: little three-spark bursts */
|
||||
var svg = document.getElementById("hw-fr-svg");
|
||||
var doodles = [];
|
||||
if (CONFIG.doodles) {
|
||||
[
|
||||
[S.x - 40, S.y - 34],
|
||||
[S.x + S.w + 40, S.y - 26],
|
||||
[S.x + S.w + 36, S.y + S.h + 30],
|
||||
].forEach(function (pt, i) {
|
||||
var p = document.createElementNS(NS, "path");
|
||||
p.setAttribute("class", "hw-fr-doodle");
|
||||
p.setAttribute("id", "hw-fr-doodle-" + i);
|
||||
var dd = "";
|
||||
for (var k = 0; k < 3; k++) {
|
||||
var a = -0.6 - k * 0.5 + window.hwHash(i * 9 + k, CONFIG.seed) * 0.2;
|
||||
var l = 26 + window.hwHash(i * 9 + k + 4, CONFIG.seed) * 8;
|
||||
dd +=
|
||||
"M " +
|
||||
pt[0] +
|
||||
" " +
|
||||
pt[1] +
|
||||
" l " +
|
||||
(Math.cos(a) * l).toFixed(1) +
|
||||
" " +
|
||||
(Math.sin(a) * l).toFixed(1) +
|
||||
" ";
|
||||
}
|
||||
p.setAttribute("d", dd);
|
||||
svg.appendChild(p);
|
||||
doodles.push(p);
|
||||
});
|
||||
}
|
||||
|
||||
var caption = document.getElementById("hw-fr-caption");
|
||||
caption.textContent = CONFIG.caption || "";
|
||||
caption.style.top = S.y + S.h + 42 + "px";
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
if (CONFIG.animationIn) {
|
||||
gsap.set(media, { opacity: 0, scale: 0.96 });
|
||||
tl.to(media, { opacity: 1, scale: 1, duration: IN * 1.4, ease: "power2.out" }, IN * 0.8);
|
||||
}
|
||||
var blen = border.getTotalLength();
|
||||
gsap.set(border, { strokeDasharray: blen, strokeDashoffset: blen });
|
||||
tl.to(border, { strokeDashoffset: 0, duration: 0.9, ease: "power2.inOut" }, 0.2);
|
||||
doodles.forEach(function (p, i) {
|
||||
var l = p.getTotalLength();
|
||||
gsap.set(p, { strokeDasharray: l, strokeDashoffset: l });
|
||||
tl.to(p, { strokeDashoffset: 0, duration: 0.3, ease: "power2.out" }, 1.1 + i * 0.12);
|
||||
});
|
||||
if (CONFIG.caption) tl.to(caption, { opacity: 1, duration: 0.4, ease: "power2.out" }, 1.4);
|
||||
|
||||
/* whole framed unit tilts + boils together */
|
||||
var unit = [media, svg, caption];
|
||||
gsap.set(unit, { rotation: CONFIG.tilt, transformOrigin: "50% 50%" });
|
||||
if (CONFIG.boil.enabled) {
|
||||
window.hwBoil(tl, unit, {
|
||||
amp: CONFIG.boil.amp,
|
||||
rot: CONFIG.boil.rot,
|
||||
frameDrop: CONFIG.boil.frameDrop,
|
||||
seed: CONFIG.seed,
|
||||
baseRot: CONFIG.tilt,
|
||||
});
|
||||
}
|
||||
|
||||
if (CONFIG.animationOut) {
|
||||
tl.to(unit, { opacity: 0, duration: OUT, ease: "power2.in" }, DUR - OUT - 0.05);
|
||||
}
|
||||
tl.set("#hw-fr-root", { visibility: "hidden" }, DUR - 0.02);
|
||||
|
||||
window.__timelines["hw-frame"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `handwritten` `frame` `media`.
|
||||
|
||||
@@ -3,13 +3,13 @@ title: "Text Along a Path"
|
||||
description: "Handwriting flowing along any SVG path with a per-character reveal; the path is the only geometry input"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-path-text.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-path-text.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/hw-path-text.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Text Along a Path preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add hw-path-text
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add hw-path-text" />
|
||||
|
||||
That writes `compositions/hw-path-text.html`, plus 1 supporting file under `assets/fonts/`.
|
||||
|
||||
@@ -32,6 +32,208 @@ It runs for 6 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`hw-path-text.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Caveat";
|
||||
src: url("assets/fonts/Caveat-700-latin.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-display: block;
|
||||
}
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
#hw-pt-root {
|
||||
--hw-font-print: "Caveat", cursive;
|
||||
--hw-font-script: "Caveat", cursive;
|
||||
--hw-ink: #f4f2ec;
|
||||
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
font-family: var(--hw-font-print, "Caveat", cursive);
|
||||
}
|
||||
#hw-pt-svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
/* The textPath carrier is geometry only — without fill:none it inherits
|
||||
SVG's default black fill and renders as a solid blob under the text. */
|
||||
#hw-pt-path {
|
||||
fill: none;
|
||||
}
|
||||
#hw-pt-guide {
|
||||
fill: none;
|
||||
stroke: rgba(244, 242, 236, 0.22);
|
||||
stroke-width: 3;
|
||||
stroke-dasharray: 2 14;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
#hw-pt-svg text {
|
||||
font-family: var(--hw-font-print, "Caveat", cursive);
|
||||
font-weight: 700;
|
||||
fill: var(--hw-ink);
|
||||
}
|
||||
#hw-pt-svg tspan {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="hw-pt-root"
|
||||
data-composition-id="hw-path-text"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<svg id="hw-pt-svg" viewBox="0 0 1920 1080">
|
||||
<path id="hw-pt-path" d=""></path>
|
||||
<path id="hw-pt-guide" d=""></path>
|
||||
<text id="hw-pt-text" font-size="92">
|
||||
<textPath id="hw-pt-tp" href="#hw-pt-path" startOffset="4%"></textPath>
|
||||
</text>
|
||||
</svg>
|
||||
<div
|
||||
id="hw-pt-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
/* ---- published parameters ----
|
||||
Handwriting along any path — custom path text, natively via
|
||||
SVG textPath (no Fusion overlay). CONFIG.d is the whole design. */
|
||||
var CONFIG = {
|
||||
text: "come on this wild journey with me...",
|
||||
d: "M 220 700 C 520 480, 900 820, 1230 560 S 1700 380, 1780 420",
|
||||
fontSize: 92,
|
||||
showGuide: true, // faint dotted guide under the writing
|
||||
typeCharDelay: 0.05,
|
||||
seed: 12,
|
||||
boil: { enabled: true, amp: 1.4, rot: 0.3, frameDrop: 3 },
|
||||
animationOut: true,
|
||||
};
|
||||
|
||||
var DUR = 6;
|
||||
function clamp(v, lo, hi) {
|
||||
return Math.min(hi, Math.max(lo, v));
|
||||
}
|
||||
var OUT = clamp(DUR * 0.06, 0.25, 0.6);
|
||||
|
||||
/* hw family runtime */
|
||||
window.hwOnUpdate = function (tl, fn) {
|
||||
if (!tl.__hwRenders) {
|
||||
tl.__hwRenders = [];
|
||||
tl.eventCallback("onUpdate", function () {
|
||||
for (var i = 0; i < tl.__hwRenders.length; i++) tl.__hwRenders[i]();
|
||||
});
|
||||
}
|
||||
tl.__hwRenders.push(fn);
|
||||
fn();
|
||||
};
|
||||
window.hwHash = function (n, seed) {
|
||||
var x = Math.sin(n * 127.1 + (seed || 1) * 311.7) * 43758.5453;
|
||||
return (x - Math.floor(x)) * 2 - 1;
|
||||
};
|
||||
window.hwBoil = function (tl, target, opts) {
|
||||
opts = opts || {};
|
||||
var amp = opts.amp !== undefined ? opts.amp : 1.6;
|
||||
var rot = opts.rot !== undefined ? opts.rot : 0.5;
|
||||
var fps = opts.fps || 30;
|
||||
var drop = opts.frameDrop || 3;
|
||||
var seed = opts.seed || 1;
|
||||
var els = gsap.utils.toArray(target);
|
||||
window.hwOnUpdate(tl, function () {
|
||||
var step = Math.floor((tl.time() * fps) / drop);
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
gsap.set(els[i], {
|
||||
x: window.hwHash(step * 3 + i * 97, seed) * amp,
|
||||
y: window.hwHash(step * 3 + 1 + i * 97, seed) * amp,
|
||||
rotation: (opts.baseRot || 0) + window.hwHash(step * 3 + 2 + i * 97, seed) * rot,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById("hw-pt-path").setAttribute("d", CONFIG.d);
|
||||
if (CONFIG.showGuide) document.getElementById("hw-pt-guide").setAttribute("d", CONFIG.d);
|
||||
document.getElementById("hw-pt-text").setAttribute("font-size", CONFIG.fontSize);
|
||||
|
||||
/* per-char tspans along the path */
|
||||
var NS = "http://www.w3.org/2000/svg";
|
||||
var tp = document.getElementById("hw-pt-tp");
|
||||
Array.from(CONFIG.text).forEach(function (ch) {
|
||||
var t = document.createElementNS(NS, "tspan");
|
||||
t.textContent = ch;
|
||||
tp.appendChild(t);
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
/* guide draws first, then the handwriting types along it */
|
||||
var guide = document.getElementById("hw-pt-guide");
|
||||
if (CONFIG.showGuide) {
|
||||
var glen = guide.getTotalLength();
|
||||
gsap.set(guide, { strokeDasharray: "2 14", strokeDashoffset: glen });
|
||||
tl.to(guide, { strokeDashoffset: 0, duration: 0.9, ease: "power2.inOut" }, 0.2);
|
||||
}
|
||||
var chars = tp.querySelectorAll("tspan");
|
||||
var delay = Math.min(CONFIG.typeCharDelay, 2.6 / Math.max(1, chars.length));
|
||||
tl.to(chars, { opacity: 1, duration: 0.03, stagger: delay, ease: "none" }, 0.7);
|
||||
|
||||
if (CONFIG.boil.enabled) {
|
||||
window.hwBoil(tl, "#hw-pt-svg", {
|
||||
amp: CONFIG.boil.amp,
|
||||
rot: CONFIG.boil.rot,
|
||||
frameDrop: CONFIG.boil.frameDrop,
|
||||
seed: CONFIG.seed,
|
||||
});
|
||||
}
|
||||
|
||||
if (CONFIG.animationOut) {
|
||||
tl.to("#hw-pt-svg", { opacity: 0, duration: OUT, ease: "power2.in" }, DUR - OUT - 0.05);
|
||||
}
|
||||
tl.set("#hw-pt-root", { visibility: "hidden" }, DUR - 0.02);
|
||||
|
||||
window.__timelines["hw-path-text"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `handwritten` `typography` `path` `overlay`.
|
||||
|
||||
@@ -3,13 +3,13 @@ title: "Hand-Drawn Flowchart"
|
||||
description: "Wobbled boxes with handwritten labels joined by curved connectors, drawing on in sequence from a node list"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-pipeline.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-pipeline.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/hw-pipeline.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Hand-Drawn Flowchart preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add hw-pipeline
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add hw-pipeline" />
|
||||
|
||||
That writes `compositions/hw-pipeline.html`, plus 1 supporting file under `assets/fonts/`.
|
||||
|
||||
@@ -32,6 +32,332 @@ It runs for 7 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`hw-pipeline.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Caveat";
|
||||
src: url("assets/fonts/Caveat-700-latin.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-display: block;
|
||||
}
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
#hw-pl-root {
|
||||
--hw-font-print: "Caveat", cursive;
|
||||
--hw-font-script: "Caveat", cursive;
|
||||
--hw-ink: #f4f2ec;
|
||||
/* Warm marker accent, matching hw-frame / hw-title / hw-underline. The
|
||||
previous #6d6dff read as a default UI blue against the family's ink. */
|
||||
--hw-accent: #ffb020;
|
||||
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent; /* overlays footage or boards */
|
||||
font-family: var(--hw-font-print, "Caveat", cursive);
|
||||
}
|
||||
#hw-pl-svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
#hw-pl-svg path {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.hw-pl-box {
|
||||
stroke: var(--hw-ink);
|
||||
stroke-width: 6;
|
||||
}
|
||||
.hw-pl-conn {
|
||||
stroke: var(--hw-accent);
|
||||
stroke-width: 5;
|
||||
}
|
||||
.hw-pl-label {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
color: var(--hw-ink);
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
text-shadow: 0 2px 14px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="hw-pl-root"
|
||||
data-composition-id="hw-pipeline"
|
||||
data-start="0"
|
||||
data-duration="7"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<svg id="hw-pl-svg" viewBox="0 0 1920 1080"></svg>
|
||||
<div id="hw-pl-labels"></div>
|
||||
<div
|
||||
id="hw-pl-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="7"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
/* ---- published parameters ----
|
||||
A flowchart "pipeline": wobbled boxes joined by curved
|
||||
connectors, drawing on in sequence, all boiling. */
|
||||
var CONFIG = {
|
||||
nodes: [{ label: "Idea" }, { label: "Record" }, { label: "Shine!" }],
|
||||
boxW: 320,
|
||||
boxH: 170,
|
||||
gap: 190, // px between boxes
|
||||
y: 455, // top of the row; x centers automatically
|
||||
fontSize: 56,
|
||||
seed: 11,
|
||||
boil: { enabled: true, amp: 1.6, rot: 0.4, frameDrop: 3 },
|
||||
animationOut: true,
|
||||
};
|
||||
|
||||
var DUR = 7;
|
||||
function clamp(v, lo, hi) {
|
||||
return Math.min(hi, Math.max(lo, v));
|
||||
}
|
||||
var OUT = clamp(DUR * 0.06, 0.25, 0.6);
|
||||
|
||||
/* hw family runtime */
|
||||
window.hwOnUpdate = function (tl, fn) {
|
||||
if (!tl.__hwRenders) {
|
||||
tl.__hwRenders = [];
|
||||
tl.eventCallback("onUpdate", function () {
|
||||
for (var i = 0; i < tl.__hwRenders.length; i++) tl.__hwRenders[i]();
|
||||
});
|
||||
}
|
||||
tl.__hwRenders.push(fn);
|
||||
fn();
|
||||
};
|
||||
window.hwHash = function (n, seed) {
|
||||
var x = Math.sin(n * 127.1 + (seed || 1) * 311.7) * 43758.5453;
|
||||
return (x - Math.floor(x)) * 2 - 1;
|
||||
};
|
||||
window.hwBoil = function (tl, target, opts) {
|
||||
opts = opts || {};
|
||||
var amp = opts.amp !== undefined ? opts.amp : 1.6;
|
||||
var rot = opts.rot !== undefined ? opts.rot : 0.5;
|
||||
var fps = opts.fps || 30;
|
||||
var drop = opts.frameDrop || 3;
|
||||
var seed = opts.seed || 1;
|
||||
var els = gsap.utils.toArray(target);
|
||||
window.hwOnUpdate(tl, function () {
|
||||
var step = Math.floor((tl.time() * fps) / drop);
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
gsap.set(els[i], {
|
||||
x: window.hwHash(step * 3 + i * 97, seed) * amp,
|
||||
y: window.hwHash(step * 3 + 1 + i * 97, seed) * amp,
|
||||
rotation: (opts.baseRot || 0) + window.hwHash(step * 3 + 2 + i * 97, seed) * rot,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
window.hwWobbleRect = function (w, h, r, seed, amp) {
|
||||
amp = amp === undefined ? 3.5 : amp;
|
||||
function wob(n) {
|
||||
return window.hwHash(n, seed) * amp;
|
||||
}
|
||||
var d = "M " + (r + wob(1)) + " " + wob(2);
|
||||
d += " L " + (w / 2 + wob(3)) + " " + wob(4);
|
||||
d += " L " + (w - r + wob(5)) + " " + wob(6);
|
||||
d += " Q " + (w + wob(7)) + " " + wob(8) + " " + (w + wob(9)) + " " + (r + wob(10));
|
||||
d += " L " + (w + wob(11)) + " " + (h / 2 + wob(12));
|
||||
d += " L " + (w + wob(13)) + " " + (h - r + wob(14));
|
||||
d +=
|
||||
" Q " +
|
||||
(w + wob(15)) +
|
||||
" " +
|
||||
(h + wob(16)) +
|
||||
" " +
|
||||
(w - r + wob(17)) +
|
||||
" " +
|
||||
(h + wob(18));
|
||||
d += " L " + (w / 2 + wob(19)) + " " + (h + wob(20));
|
||||
d += " L " + (r + wob(21)) + " " + (h + wob(22));
|
||||
d += " Q " + wob(23) + " " + (h + wob(24)) + " " + wob(25) + " " + (h - r + wob(26));
|
||||
d += " L " + wob(27) + " " + (h / 2 + wob(28));
|
||||
d += " L " + wob(29) + " " + (r + wob(30));
|
||||
d += " Q " + wob(31) + " " + wob(32) + " " + (r + wob(33)) + " " + wob(34);
|
||||
return d;
|
||||
};
|
||||
|
||||
var NS = "http://www.w3.org/2000/svg";
|
||||
var svg = document.getElementById("hw-pl-svg");
|
||||
var labels = document.getElementById("hw-pl-labels");
|
||||
var n = CONFIG.nodes.length;
|
||||
var totalW = n * CONFIG.boxW + (n - 1) * CONFIG.gap;
|
||||
var x0 = (1920 - totalW) / 2;
|
||||
|
||||
var boxes = [],
|
||||
conns = [],
|
||||
labelEls = [];
|
||||
CONFIG.nodes.forEach(function (node, i) {
|
||||
var bx = x0 + i * (CONFIG.boxW + CONFIG.gap);
|
||||
/* outer g owns the translate (GSAP transforms REPLACE the SVG
|
||||
transform attribute — never boil the element that carries it);
|
||||
inner g is the boil target */
|
||||
var gOuter = document.createElementNS(NS, "g");
|
||||
gOuter.setAttribute("transform", "translate(" + bx + " " + CONFIG.y + ")");
|
||||
var g = document.createElementNS(NS, "g");
|
||||
g.setAttribute("id", "hw-pl-box-g-" + i);
|
||||
gOuter.appendChild(g);
|
||||
var p = document.createElementNS(NS, "path");
|
||||
p.setAttribute("class", "hw-pl-box");
|
||||
p.setAttribute("id", "hw-pl-box-" + i);
|
||||
p.setAttribute("d", window.hwWobbleRect(CONFIG.boxW, CONFIG.boxH, 24, CONFIG.seed + i));
|
||||
g.appendChild(p);
|
||||
svg.appendChild(gOuter);
|
||||
boxes.push(p);
|
||||
|
||||
var label = document.createElement("div");
|
||||
label.className = "hw-pl-label";
|
||||
label.id = "hw-pl-label-" + i;
|
||||
label.textContent = node.label;
|
||||
label.style.left = bx + "px";
|
||||
label.style.top = CONFIG.y + CONFIG.boxH / 2 - CONFIG.fontSize * 0.62 + "px";
|
||||
label.style.width = CONFIG.boxW + "px";
|
||||
label.style.fontSize = CONFIG.fontSize + "px";
|
||||
labels.appendChild(label);
|
||||
labelEls.push(label);
|
||||
|
||||
if (i < n - 1) {
|
||||
var cx = bx + CONFIG.boxW,
|
||||
cy = CONFIG.y + CONFIG.boxH / 2;
|
||||
var ex = cx + CONFIG.gap,
|
||||
ey = cy;
|
||||
var c = document.createElementNS(NS, "path");
|
||||
c.setAttribute("class", "hw-pl-conn");
|
||||
c.setAttribute("id", "hw-pl-conn-" + i);
|
||||
var sx = cx + 10,
|
||||
tx = ex - 10;
|
||||
var qx = cx + CONFIG.gap / 2 + window.hwHash(i * 9 + 2, CONFIG.seed) * 16,
|
||||
qy = cy - 46 - window.hwHash(i * 9 + 3, CONFIG.seed) * 12;
|
||||
/* Barbs follow the curve's incoming tangent. A quadratic's tangent
|
||||
at its end is (end - control); barbs fixed to horizontal left the
|
||||
head detached from a curve that arrives descending. Symmetric
|
||||
length and angle, so it reads as one arrowhead. */
|
||||
var ang = Math.atan2(ey - qy, tx - qx) + Math.PI;
|
||||
var BARB = 26,
|
||||
SPREAD = 0.42;
|
||||
var b1x = tx + BARB * Math.cos(ang - SPREAD),
|
||||
b1y = ey + BARB * Math.sin(ang - SPREAD);
|
||||
var b2x = tx + BARB * Math.cos(ang + SPREAD),
|
||||
b2y = ey + BARB * Math.sin(ang + SPREAD);
|
||||
c.setAttribute(
|
||||
"d",
|
||||
"M " +
|
||||
sx +
|
||||
" " +
|
||||
cy +
|
||||
" Q " +
|
||||
qx +
|
||||
" " +
|
||||
qy +
|
||||
" " +
|
||||
tx +
|
||||
" " +
|
||||
ey +
|
||||
" M " +
|
||||
tx +
|
||||
" " +
|
||||
ey +
|
||||
" L " +
|
||||
b1x +
|
||||
" " +
|
||||
b1y +
|
||||
" M " +
|
||||
tx +
|
||||
" " +
|
||||
ey +
|
||||
" L " +
|
||||
b2x +
|
||||
" " +
|
||||
b2y,
|
||||
);
|
||||
svg.appendChild(c);
|
||||
conns.push(c);
|
||||
}
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
/* sequence: box draws -> label -> connector -> next box... */
|
||||
var t = 0.3;
|
||||
CONFIG.nodes.forEach(function (_, i) {
|
||||
var p = boxes[i];
|
||||
var len = p.getTotalLength();
|
||||
gsap.set(p, { strokeDasharray: len, strokeDashoffset: len });
|
||||
tl.to(p, { strokeDashoffset: 0, duration: 0.65, ease: "power2.inOut" }, t);
|
||||
tl.to(labelEls[i], { opacity: 1, duration: 0.35, ease: "power2.out" }, t + 0.45);
|
||||
t += 0.7;
|
||||
if (i < conns.length) {
|
||||
var c = conns[i];
|
||||
var cl = c.getTotalLength();
|
||||
gsap.set(c, { strokeDasharray: cl, strokeDashoffset: cl });
|
||||
tl.to(c, { strokeDashoffset: 0, duration: 0.45, ease: "power2.inOut" }, t);
|
||||
t += 0.5;
|
||||
}
|
||||
});
|
||||
|
||||
if (CONFIG.boil.enabled) {
|
||||
var targets = boxes.map(function (_, i) {
|
||||
return "#hw-pl-box-g-" + i;
|
||||
});
|
||||
window.hwBoil(tl, targets.concat([".hw-pl-conn", ".hw-pl-label"]), {
|
||||
amp: CONFIG.boil.amp,
|
||||
rot: CONFIG.boil.rot,
|
||||
frameDrop: CONFIG.boil.frameDrop,
|
||||
seed: CONFIG.seed,
|
||||
});
|
||||
}
|
||||
|
||||
if (CONFIG.animationOut) {
|
||||
tl.to([svg, labels], { opacity: 0, duration: OUT, ease: "power2.in" }, DUR - OUT - 0.05);
|
||||
}
|
||||
tl.set("#hw-pl-root", { visibility: "hidden" }, DUR - 0.02);
|
||||
|
||||
window.__timelines["hw-pipeline"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `handwritten` `diagram` `data-viz` `overlay`.
|
||||
|
||||
@@ -3,13 +3,13 @@ title: "Scribble Transition"
|
||||
description: "Overlay transition: scribble bands accumulate to cover the frame with a solid backstop, the host cuts beneath, and the scribble clears"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-scribble-transition.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-scribble-transition.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/hw-scribble-transition.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Scribble Transition preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add hw-scribble-transition
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add hw-scribble-transition" />
|
||||
|
||||
That writes one file: `compositions/hw-scribble-transition.html`.
|
||||
|
||||
@@ -32,6 +32,167 @@ It runs for 2.2 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`hw-scribble-transition.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
#hw-st-root {
|
||||
/* Warm marker accent, consistent with the rest of the hw family.
|
||||
Override per use via CONFIG.color. */
|
||||
--hw-accent: #ffb020;
|
||||
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
#hw-st-svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#hw-st-svg path {
|
||||
fill: none;
|
||||
stroke: var(--hw-accent);
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
/* solid backstop that guarantees full coverage at the midpoint */
|
||||
#hw-st-solid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--hw-accent);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="hw-st-root"
|
||||
data-composition-id="hw-scribble-transition"
|
||||
data-start="0"
|
||||
data-duration="2.2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<svg id="hw-st-svg" viewBox="0 0 1920 1080" data-layout-allow-overflow></svg>
|
||||
<div id="hw-st-solid"></div>
|
||||
<div
|
||||
id="hw-st-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="2.2"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
/* ---- published parameters ----
|
||||
Overlay-transition (the mk-clone-wall model): fat scribble strokes
|
||||
sweep across and cover the frame, a solid backstop guarantees full
|
||||
coverage at the midpoint (the host hard-cuts beneath, ~0.9-1.4s
|
||||
local), then everything clears. Place spanning the host's cut. */
|
||||
var CONFIG = {
|
||||
color: null, // css color; null = --hw-accent (warm marker)
|
||||
bands: 7, // horizontal scribble bands
|
||||
strokeScale: 1.0,
|
||||
seed: 17,
|
||||
};
|
||||
|
||||
var W = 1920,
|
||||
H = 1080,
|
||||
DUR = 2.2;
|
||||
|
||||
window.hwHash = function (n, seed) {
|
||||
var x = Math.sin(n * 127.1 + (seed || 1) * 311.7) * 43758.5453;
|
||||
return (x - Math.floor(x)) * 2 - 1;
|
||||
};
|
||||
|
||||
var root = document.getElementById("hw-st-root");
|
||||
if (CONFIG.color) root.style.setProperty("--hw-accent", CONFIG.color);
|
||||
var NS = "http://www.w3.org/2000/svg";
|
||||
var svg = document.getElementById("hw-st-svg");
|
||||
|
||||
/* fat zigzag bands, each covering a horizontal slice edge-to-edge */
|
||||
var bandH = H / CONFIG.bands;
|
||||
var paths = [];
|
||||
for (var b = 0; b < CONFIG.bands; b++) {
|
||||
var y0 = b * bandH + bandH / 2;
|
||||
var d = "M -80 " + (y0 + window.hwHash(b * 17, CONFIG.seed) * 20).toFixed(1);
|
||||
var zigs = 7;
|
||||
for (var i = 1; i <= zigs; i++) {
|
||||
var x = (W + 160) * (i / zigs) - 80;
|
||||
var y =
|
||||
y0 + (i % 2 ? -1 : 1) * bandH * 0.32 + window.hwHash(b * 17 + i, CONFIG.seed) * 16;
|
||||
d += " L " + x.toFixed(1) + " " + y.toFixed(1);
|
||||
}
|
||||
var p = document.createElementNS(NS, "path");
|
||||
p.setAttribute("d", d);
|
||||
p.setAttribute("stroke-width", Math.ceil(bandH * 0.85 * CONFIG.strokeScale));
|
||||
p.setAttribute("id", "hw-st-band-" + b);
|
||||
svg.appendChild(p);
|
||||
paths.push(p);
|
||||
}
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
/* 0-0.85: bands scribble on (staggered, alternating direction) */
|
||||
paths.forEach(function (p, i) {
|
||||
var len = p.getTotalLength();
|
||||
gsap.set(p, { strokeDasharray: len, strokeDashoffset: i % 2 ? -len : len });
|
||||
tl.to(p, { strokeDashoffset: 0, duration: 0.55, ease: "power2.in" }, 0.04 * i);
|
||||
});
|
||||
/* 0.8: solid backstop snaps in beneath the finished scribble — full
|
||||
coverage guaranteed 0.85-1.35 (the host's cut window) */
|
||||
tl.to("#hw-st-solid", { opacity: 1, duration: 0.12, ease: "none" }, 0.8);
|
||||
|
||||
/* 1.35-1.95: clear — bands scribble off the other way, backstop lifts */
|
||||
tl.to("#hw-st-solid", { opacity: 0, duration: 0.15, ease: "none" }, 1.42);
|
||||
paths.forEach(function (p, i) {
|
||||
var len = p.getTotalLength();
|
||||
tl.to(
|
||||
p,
|
||||
{ strokeDashoffset: i % 2 ? len : -len, duration: 0.5, ease: "power2.out" },
|
||||
1.42 + 0.035 * i,
|
||||
);
|
||||
});
|
||||
|
||||
/* hard kill the moment the last band clears (learning: kill roots
|
||||
when the final fade completes, not at DUR) */
|
||||
tl.set(root, { visibility: "hidden" }, 2.17);
|
||||
|
||||
window.__timelines["hw-scribble-transition"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `handwritten` `transition` `wipe`.
|
||||
|
||||
@@ -3,13 +3,13 @@ title: "Speech Bubble"
|
||||
description: "Hand-drawn speech bubble with a positionable tail: outline draws on, paper fill fades in, handwritten text types per character; boils"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-text-cloud.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-text-cloud.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/hw-text-cloud.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Speech Bubble preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add hw-text-cloud
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add hw-text-cloud" />
|
||||
|
||||
That writes `compositions/hw-text-cloud.html`, plus 1 supporting file under `assets/fonts/`.
|
||||
|
||||
@@ -32,6 +32,300 @@ It runs for 6 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`hw-text-cloud.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Caveat";
|
||||
src: url("assets/fonts/Caveat-700-latin.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-display: block;
|
||||
}
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
#hw-tc-root {
|
||||
/* ---- hw tokens ---- */
|
||||
--hw-font-print: "Caveat", cursive;
|
||||
--hw-font-script: "Caveat", cursive;
|
||||
--hw-ink: #f4f2ec;
|
||||
--hw-ink-dark: #26241f;
|
||||
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent; /* overlays footage */
|
||||
font-family: var(--hw-font-print, "Caveat", cursive);
|
||||
}
|
||||
#hw-tc-wrap {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
#hw-tc-wrap svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
#hw-tc-fill {
|
||||
fill: var(--hw-ink);
|
||||
opacity: 0;
|
||||
}
|
||||
#hw-tc-stroke {
|
||||
fill: none;
|
||||
stroke: var(--hw-ink-dark);
|
||||
stroke-width: 4;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
#hw-tc-text {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
color: var(--hw-ink-dark);
|
||||
line-height: 1.05;
|
||||
}
|
||||
#hw-tc-text .ch {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="hw-tc-root"
|
||||
data-composition-id="hw-text-cloud"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="hw-tc-wrap">
|
||||
<svg id="hw-tc-svg">
|
||||
<path id="hw-tc-fill"></path>
|
||||
<path id="hw-tc-stroke"></path>
|
||||
</svg>
|
||||
<div id="hw-tc-text"></div>
|
||||
</div>
|
||||
<div
|
||||
id="hw-tc-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
/* ---- published parameters ----
|
||||
Speech bubble with a movable tip (pointable at any subject): wobbled
|
||||
cloud, draw-on outline, paper fill, per-char handwritten text. */
|
||||
var CONFIG = {
|
||||
text: "what are you looking at?",
|
||||
x: 620, // wrapper left
|
||||
y: 180, // wrapper top
|
||||
w: 560,
|
||||
h: 300,
|
||||
fontSize: 58,
|
||||
tip: { side: "bottom-left", len: 90 }, // "bottom-left"|"bottom-right"
|
||||
seed: 6,
|
||||
boil: { enabled: true, amp: 1.6, rot: 0.4, frameDrop: 3 },
|
||||
typeCharDelay: 0.03,
|
||||
animationIn: true,
|
||||
animationOut: true,
|
||||
};
|
||||
|
||||
var DUR = 6;
|
||||
function clamp(v, lo, hi) {
|
||||
return Math.min(hi, Math.max(lo, v));
|
||||
}
|
||||
var OUT = clamp(DUR * 0.06, 0.25, 0.6);
|
||||
|
||||
/* hw family runtime */
|
||||
window.hwOnUpdate = function (tl, fn) {
|
||||
if (!tl.__hwRenders) {
|
||||
tl.__hwRenders = [];
|
||||
tl.eventCallback("onUpdate", function () {
|
||||
for (var i = 0; i < tl.__hwRenders.length; i++) tl.__hwRenders[i]();
|
||||
});
|
||||
}
|
||||
tl.__hwRenders.push(fn);
|
||||
fn();
|
||||
};
|
||||
window.hwHash = function (n, seed) {
|
||||
var x = Math.sin(n * 127.1 + (seed || 1) * 311.7) * 43758.5453;
|
||||
return (x - Math.floor(x)) * 2 - 1;
|
||||
};
|
||||
window.hwBoil = function (tl, target, opts) {
|
||||
opts = opts || {};
|
||||
var amp = opts.amp !== undefined ? opts.amp : 1.6;
|
||||
var rot = opts.rot !== undefined ? opts.rot : 0.5;
|
||||
var fps = opts.fps || 30;
|
||||
var drop = opts.frameDrop || 3;
|
||||
var seed = opts.seed || 1;
|
||||
var els = gsap.utils.toArray(target);
|
||||
window.hwOnUpdate(tl, function () {
|
||||
var step = Math.floor((tl.time() * fps) / drop);
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
gsap.set(els[i], {
|
||||
x: window.hwHash(step * 3 + i * 97, seed) * amp,
|
||||
y: window.hwHash(step * 3 + 1 + i * 97, seed) * amp,
|
||||
rotation: (opts.baseRot || 0) + window.hwHash(step * 3 + 2 + i * 97, seed) * rot,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* wobbled cloud with a tail tip */
|
||||
function cloudPath(W, H, tip, seed) {
|
||||
var cx = W / 2,
|
||||
cy = H / 2 - 14,
|
||||
rx = W / 2 - 12,
|
||||
ry = H / 2 - 34;
|
||||
var N = 16,
|
||||
pts = [];
|
||||
for (var i = 0; i < N; i++) {
|
||||
var a = (i / N) * Math.PI * 2;
|
||||
var wr = 1 + window.hwHash(i * 11 + 3, seed) * 0.06;
|
||||
pts.push([cx + Math.cos(a) * rx * wr, cy + Math.sin(a) * ry * wr]);
|
||||
}
|
||||
var mx = (pts[0][0] + pts[N - 1][0]) / 2,
|
||||
my = (pts[0][1] + pts[N - 1][1]) / 2;
|
||||
var d = "M" + mx.toFixed(1) + " " + my.toFixed(1);
|
||||
for (var j = 0; j < N; j++) {
|
||||
var p = pts[j],
|
||||
q = pts[(j + 1) % N];
|
||||
d +=
|
||||
" Q" +
|
||||
p[0].toFixed(1) +
|
||||
" " +
|
||||
p[1].toFixed(1) +
|
||||
" " +
|
||||
((p[0] + q[0]) / 2).toFixed(1) +
|
||||
" " +
|
||||
((p[1] + q[1]) / 2).toFixed(1);
|
||||
}
|
||||
d += "Z";
|
||||
/* tail */
|
||||
var left = tip.side !== "bottom-right";
|
||||
var bx = cx + (left ? -rx * 0.45 : rx * 0.45),
|
||||
by = cy + ry * 0.92;
|
||||
var tx = bx + (left ? -tip.len * 0.5 : tip.len * 0.5),
|
||||
ty = by + tip.len;
|
||||
d +=
|
||||
" M " +
|
||||
(bx - 26).toFixed(1) +
|
||||
" " +
|
||||
(by - 6).toFixed(1) +
|
||||
" Q " +
|
||||
(bx - 8 + window.hwHash(31, seed) * 6).toFixed(1) +
|
||||
" " +
|
||||
((by + ty) / 2).toFixed(1) +
|
||||
" " +
|
||||
tx.toFixed(1) +
|
||||
" " +
|
||||
ty.toFixed(1) +
|
||||
" Q " +
|
||||
(bx + 18).toFixed(1) +
|
||||
" " +
|
||||
((by + ty) / 2).toFixed(1) +
|
||||
" " +
|
||||
(bx + 30).toFixed(1) +
|
||||
" " +
|
||||
(by - 2).toFixed(1);
|
||||
return d;
|
||||
}
|
||||
|
||||
var wrap = document.getElementById("hw-tc-wrap");
|
||||
wrap.style.left = CONFIG.x + "px";
|
||||
wrap.style.top = CONFIG.y + "px";
|
||||
wrap.style.width = CONFIG.w + "px";
|
||||
wrap.style.height = CONFIG.h + "px";
|
||||
var svg = document.getElementById("hw-tc-svg");
|
||||
svg.setAttribute("viewBox", "0 0 " + CONFIG.w + " " + CONFIG.h);
|
||||
var d = cloudPath(CONFIG.w, CONFIG.h, CONFIG.tip, CONFIG.seed);
|
||||
document.getElementById("hw-tc-fill").setAttribute("d", d);
|
||||
document.getElementById("hw-tc-stroke").setAttribute("d", d);
|
||||
|
||||
var text = document.getElementById("hw-tc-text");
|
||||
text.style.left = CONFIG.w * 0.12 + "px";
|
||||
text.style.top = CONFIG.h * 0.1 + "px";
|
||||
text.style.width = CONFIG.w * 0.76 + "px";
|
||||
text.style.height = CONFIG.h * 0.62 + "px";
|
||||
text.style.fontSize = CONFIG.fontSize + "px";
|
||||
var inner = document.createElement("div");
|
||||
Array.from(CONFIG.text).forEach(function (ch) {
|
||||
var s = document.createElement("span");
|
||||
s.className = "ch";
|
||||
s.textContent = ch;
|
||||
inner.appendChild(s);
|
||||
});
|
||||
text.appendChild(inner);
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
/* draw outline, fade fill, type text */
|
||||
var stroke = document.getElementById("hw-tc-stroke");
|
||||
var len = stroke.getTotalLength();
|
||||
gsap.set(stroke, { strokeDasharray: len, strokeDashoffset: len });
|
||||
var t0 = CONFIG.animationIn ? 0.3 : 0;
|
||||
if (CONFIG.animationIn) {
|
||||
tl.to(stroke, { strokeDashoffset: 0, duration: 0.8, ease: "power2.inOut" }, t0);
|
||||
tl.to("#hw-tc-fill", { opacity: 0.94, duration: 0.4, ease: "power2.out" }, t0 + 0.55);
|
||||
} else {
|
||||
gsap.set(stroke, { strokeDashoffset: 0 });
|
||||
gsap.set("#hw-tc-fill", { opacity: 0.94 });
|
||||
}
|
||||
var chars = text.querySelectorAll(".ch");
|
||||
var delay = Math.min(CONFIG.typeCharDelay, 1.8 / Math.max(1, chars.length));
|
||||
tl.to(chars, { opacity: 1, duration: 0.02, stagger: delay, ease: "none" }, t0 + 0.9);
|
||||
|
||||
if (CONFIG.boil.enabled) {
|
||||
window.hwBoil(tl, "#hw-tc-wrap", {
|
||||
amp: CONFIG.boil.amp,
|
||||
rot: CONFIG.boil.rot,
|
||||
frameDrop: CONFIG.boil.frameDrop,
|
||||
seed: CONFIG.seed,
|
||||
});
|
||||
}
|
||||
|
||||
if (CONFIG.animationOut) {
|
||||
tl.to(wrap, { opacity: 0, duration: OUT, ease: "power2.in" }, DUR - OUT - 0.05);
|
||||
}
|
||||
tl.set("#hw-tc-root", { visibility: "hidden" }, DUR - 0.02);
|
||||
|
||||
window.__timelines["hw-text-cloud"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `handwritten` `speech-bubble` `overlay`.
|
||||
|
||||
@@ -3,13 +3,13 @@ title: "Handwritten Title"
|
||||
description: "Handwritten display text with a scalar-driven word-by-word highlight sweep and a seeded squiggle underline sized to the text; boils"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-title.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-title.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/hw-title.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Handwritten Title preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add hw-title
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add hw-title" />
|
||||
|
||||
That writes `compositions/hw-title.html`, plus 1 supporting file under `assets/fonts/`.
|
||||
|
||||
@@ -32,6 +32,272 @@ It runs for 6 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`hw-title.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Caveat";
|
||||
src: url("assets/fonts/Caveat-700-latin.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-display: block;
|
||||
}
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
#hw-t-root {
|
||||
/* ---- hw tokens ---- */
|
||||
--hw-font-print: "Caveat", cursive;
|
||||
--hw-font-script: "Caveat", cursive;
|
||||
--hw-ink: #f4f2ec;
|
||||
--hw-ink-dark: #26241f;
|
||||
--hw-accent-warm: #ffb020;
|
||||
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent; /* overlays footage or a board */
|
||||
font-family: var(--hw-font-print, "Caveat", cursive);
|
||||
}
|
||||
#hw-t-wrap {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
#hw-t-line {
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 2px 18px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
#hw-t-line .w {
|
||||
display: inline;
|
||||
}
|
||||
#hw-t-under {
|
||||
overflow: visible;
|
||||
display: block;
|
||||
}
|
||||
#hw-t-under path {
|
||||
fill: none;
|
||||
stroke: var(--hw-accent-warm);
|
||||
stroke-width: 7;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="hw-t-root"
|
||||
data-composition-id="hw-title"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="hw-t-wrap">
|
||||
<div id="hw-t-line"></div>
|
||||
<svg id="hw-t-under" width="10" height="34"><path id="hw-t-under-path"></path></svg>
|
||||
</div>
|
||||
<div
|
||||
id="hw-t-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
/* ---- published parameters ----
|
||||
Handwritten title with the highlight-sweep scalar (the shared
|
||||
mk-callout-highlight mechanic) + squiggle underline + the boil.
|
||||
Demonstrates the dispatcher composing sweep + boil on one
|
||||
timeline (learning: one onUpdate — everything through hwOnUpdate). */
|
||||
var CONFIG = {
|
||||
text: "shine like the star you are",
|
||||
fontSize: 120,
|
||||
scheme: "chalk", // "chalk" (white, over footage) | "ink" (dark, boards)
|
||||
sweep: { enabled: true, start: 0.8, end: 3.2 },
|
||||
underline: { enabled: true, wobble: 6 },
|
||||
boil: { enabled: true, amp: 1.4, rot: 0.3, frameDrop: 3 },
|
||||
seed: 8,
|
||||
x: null, // null = centered
|
||||
y: null,
|
||||
animationIn: true,
|
||||
animationOut: true,
|
||||
};
|
||||
|
||||
var DUR = 6;
|
||||
function clamp(v, lo, hi) {
|
||||
return Math.min(hi, Math.max(lo, v));
|
||||
}
|
||||
var IN = clamp(DUR * 0.08, 0.3, 0.8);
|
||||
var OUT = clamp(DUR * 0.06, 0.25, 0.6);
|
||||
|
||||
/* hw family runtime */
|
||||
window.hwOnUpdate = function (tl, fn) {
|
||||
if (!tl.__hwRenders) {
|
||||
tl.__hwRenders = [];
|
||||
tl.eventCallback("onUpdate", function () {
|
||||
for (var i = 0; i < tl.__hwRenders.length; i++) tl.__hwRenders[i]();
|
||||
});
|
||||
}
|
||||
tl.__hwRenders.push(fn);
|
||||
fn();
|
||||
};
|
||||
window.hwHash = function (n, seed) {
|
||||
var x = Math.sin(n * 127.1 + (seed || 1) * 311.7) * 43758.5453;
|
||||
return (x - Math.floor(x)) * 2 - 1;
|
||||
};
|
||||
window.hwBoil = function (tl, target, opts) {
|
||||
opts = opts || {};
|
||||
var amp = opts.amp !== undefined ? opts.amp : 1.6;
|
||||
var rot = opts.rot !== undefined ? opts.rot : 0.5;
|
||||
var fps = opts.fps || 30;
|
||||
var drop = opts.frameDrop || 3;
|
||||
var seed = opts.seed || 1;
|
||||
var els = gsap.utils.toArray(target);
|
||||
window.hwOnUpdate(tl, function () {
|
||||
var step = Math.floor((tl.time() * fps) / drop);
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
gsap.set(els[i], {
|
||||
x: window.hwHash(step * 3 + i * 97, seed) * amp,
|
||||
y: window.hwHash(step * 3 + 1 + i * 97, seed) * amp,
|
||||
rotation: (opts.baseRot || 0) + window.hwHash(step * 3 + 2 + i * 97, seed) * rot,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var FULL = CONFIG.scheme === "ink" ? "#26241f" : "#f4f2ec";
|
||||
var FAINT = CONFIG.scheme === "ink" ? "rgba(38,36,31,0.3)" : "rgba(244,242,236,0.32)";
|
||||
|
||||
var wrap = document.getElementById("hw-t-wrap");
|
||||
var line = document.getElementById("hw-t-line");
|
||||
line.style.fontSize = CONFIG.fontSize + "px";
|
||||
|
||||
/* word spans */
|
||||
var wordEls = [];
|
||||
CONFIG.text.split(/\s+/).forEach(function (w, i) {
|
||||
if (i > 0) line.appendChild(document.createTextNode(" "));
|
||||
var span = document.createElement("span");
|
||||
span.className = "w";
|
||||
span.textContent = w;
|
||||
span.style.color = CONFIG.sweep.enabled ? FAINT : FULL;
|
||||
line.appendChild(span);
|
||||
wordEls.push(span);
|
||||
});
|
||||
|
||||
/* measure + position; build squiggle underline to match width */
|
||||
var rect = line.getBoundingClientRect();
|
||||
wrap.style.left =
|
||||
(CONFIG.x === null ? Math.round((1920 - rect.width) / 2) : CONFIG.x) + "px";
|
||||
wrap.style.top =
|
||||
(CONFIG.y === null ? Math.round((1080 - rect.height) / 2) - 20 : CONFIG.y) + "px";
|
||||
|
||||
var underPath = document.getElementById("hw-t-under-path");
|
||||
if (CONFIG.underline.enabled) {
|
||||
var uw = Math.round(rect.width);
|
||||
var svg = document.getElementById("hw-t-under");
|
||||
svg.setAttribute("width", uw);
|
||||
svg.setAttribute("viewBox", "0 0 " + uw + " 34");
|
||||
var segs = Math.max(6, Math.round(uw / 90));
|
||||
var d = "M 2 " + (16 + window.hwHash(0, CONFIG.seed) * 4).toFixed(1);
|
||||
for (var i = 1; i <= segs; i++) {
|
||||
var x = (uw / segs) * i;
|
||||
var y = 16 + window.hwHash(i * 5 + 1, CONFIG.seed) * CONFIG.underline.wobble;
|
||||
var cxp = x - uw / segs / 2 + window.hwHash(i * 5 + 2, CONFIG.seed) * 10;
|
||||
var cyp = 16 + (i % 2 ? -1 : 1) * (8 + window.hwHash(i * 5 + 3, CONFIG.seed) * 4);
|
||||
d +=
|
||||
" Q " +
|
||||
cxp.toFixed(1) +
|
||||
" " +
|
||||
cyp.toFixed(1) +
|
||||
" " +
|
||||
x.toFixed(1) +
|
||||
" " +
|
||||
y.toFixed(1);
|
||||
}
|
||||
underPath.setAttribute("d", d);
|
||||
} else {
|
||||
document.getElementById("hw-t-under").style.display = "none";
|
||||
}
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
if (CONFIG.animationIn) {
|
||||
gsap.set(wrap, { opacity: 0 });
|
||||
tl.to(wrap, { opacity: 1, duration: IN, ease: "power2.out" }, 0.1);
|
||||
}
|
||||
|
||||
/* the sweep scalar — registered through the dispatcher so it
|
||||
coexists with the boil on the same timeline */
|
||||
if (CONFIG.sweep.enabled) {
|
||||
var mix = gsap.utils.interpolate(FAINT, FULL);
|
||||
var st = { p: 0 };
|
||||
window.hwOnUpdate(tl, function () {
|
||||
var k = st.p * wordEls.length;
|
||||
for (var i = 0; i < wordEls.length; i++) {
|
||||
var f = Math.max(0, Math.min(1, k - i));
|
||||
wordEls[i].style.color = mix(f);
|
||||
}
|
||||
});
|
||||
tl.to(
|
||||
st,
|
||||
{ p: 1, duration: CONFIG.sweep.end - CONFIG.sweep.start, ease: "none" },
|
||||
CONFIG.sweep.start,
|
||||
);
|
||||
}
|
||||
|
||||
/* underline draws as the sweep lands */
|
||||
if (CONFIG.underline.enabled) {
|
||||
var len = underPath.getTotalLength();
|
||||
gsap.set(underPath, { strokeDasharray: len, strokeDashoffset: len });
|
||||
var uAt = CONFIG.sweep.enabled ? CONFIG.sweep.end - 0.2 : 0.8;
|
||||
tl.to(underPath, { strokeDashoffset: 0, duration: 0.7, ease: "power2.inOut" }, uAt);
|
||||
}
|
||||
|
||||
if (CONFIG.boil.enabled) {
|
||||
window.hwBoil(tl, "#hw-t-wrap", {
|
||||
amp: CONFIG.boil.amp,
|
||||
rot: CONFIG.boil.rot,
|
||||
frameDrop: CONFIG.boil.frameDrop,
|
||||
seed: CONFIG.seed,
|
||||
});
|
||||
}
|
||||
|
||||
if (CONFIG.animationOut) {
|
||||
tl.to(wrap, { opacity: 0, duration: OUT, ease: "power2.in" }, DUR - OUT - 0.05);
|
||||
}
|
||||
tl.set("#hw-t-root", { visibility: "hidden" }, DUR - 0.02);
|
||||
|
||||
window.__timelines["hw-title"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `handwritten` `typography` `title-card` `overlay`.
|
||||
|
||||
@@ -3,34 +3,320 @@ title: "Instagram Follow"
|
||||
description: "Animated Instagram follow overlay with profile card and follow button"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/instagram-follow.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/instagram-follow.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/instagram-follow.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Instagram Follow preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add instagram-follow
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add instagram-follow" />
|
||||
|
||||
That writes `compositions/instagram-follow.html`, plus 1 supporting file under `assets/`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4.5 seconds at 1080×1920. Paste this into your composition:
|
||||
<Accordion title={`instagram-follow.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;700&display=block"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
width: 1080px;
|
||||
height: 1920px;
|
||||
font-family: "DM Sans", sans-serif;
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .lower-third {
|
||||
position: absolute;
|
||||
bottom: 160px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
background: #1a1a1a;
|
||||
border-radius: 75px;
|
||||
padding: 25px 40px 25px 25px;
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 3px solid #333;
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .profile-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .display-name {
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.3;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .verified-badge {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .verified-badge svg {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .handle {
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
color: #a0a0a0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .follower-count {
|
||||
font-size: 25px;
|
||||
font-weight: 400;
|
||||
color: #737373;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .follow-btn {
|
||||
position: relative;
|
||||
width: 250px;
|
||||
height: 80px;
|
||||
border-radius: 40px;
|
||||
background: #0095f6;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .btn-text {
|
||||
position: absolute;
|
||||
font-family: "DM Sans", sans-serif;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .btn-text-follow {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .btn-text-following {
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .chevron-icon {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
[data-composition-id="instagram-follow"] .chevron-icon svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="instagram-follow"
|
||||
data-composition-src="compositions/instagram-follow.html"
|
||||
data-start="0"
|
||||
data-duration="4.5"
|
||||
data-track-index="1"
|
||||
data-width="1080"
|
||||
data-height="1920"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="4.5"
|
||||
>
|
||||
<div
|
||||
id="card"
|
||||
class="clip lower-third"
|
||||
data-start="0"
|
||||
data-duration="4.5"
|
||||
data-track-index="0"
|
||||
>
|
||||
<img class="avatar" src="assets/avatar.jpg" alt="HeyGen" crossorigin="anonymous" />
|
||||
<div class="profile-info">
|
||||
<div class="name-row">
|
||||
<div class="display-name">HeyGen</div>
|
||||
<span class="verified-badge"
|
||||
><svg viewBox="0 0 40 40" fill="none">
|
||||
<circle cx="20" cy="20" r="20" fill="#0095F6" />
|
||||
<path d="M17.5 27.5L10 20l2.5-2.5 5 5 10-10L30 15 17.5 27.5z" fill="#fff" /></svg
|
||||
></span>
|
||||
</div>
|
||||
<div class="handle">@heygen_official</div>
|
||||
<div class="follower-count">47.5K followers</div>
|
||||
</div>
|
||||
<div id="follow-btn" class="follow-btn">
|
||||
<span id="btn-follow" class="btn-text btn-text-follow">Follow</span>
|
||||
<span id="btn-following" class="btn-text btn-text-following">
|
||||
<span>Following</span>
|
||||
<span class="chevron-icon"
|
||||
><svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9"></polyline></svg
|
||||
></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var card = document.getElementById("card");
|
||||
var btn = document.getElementById("follow-btn");
|
||||
var textFollow = document.getElementById("btn-follow");
|
||||
var textFollowing = document.getElementById("btn-following");
|
||||
|
||||
// Start off-screen
|
||||
gsap.set(card, { y: 300, opacity: 0 });
|
||||
|
||||
// Slide in from bottom
|
||||
tl.to(
|
||||
card,
|
||||
{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
duration: 0.5,
|
||||
ease: "power3.out",
|
||||
},
|
||||
0.1,
|
||||
);
|
||||
|
||||
// Hold, then button press-in
|
||||
tl.to(
|
||||
btn,
|
||||
{
|
||||
scale: 0.92,
|
||||
duration: 0.15,
|
||||
ease: "power2.out",
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
|
||||
// Button release with spring bounce
|
||||
tl.to(
|
||||
btn,
|
||||
{
|
||||
scale: 1,
|
||||
duration: 0.4,
|
||||
ease: "elastic.out(1, 0.4)",
|
||||
},
|
||||
1.15,
|
||||
);
|
||||
|
||||
// Button background: IG blue → dark gray
|
||||
tl.to(
|
||||
btn,
|
||||
{
|
||||
backgroundColor: "#2f2f2f",
|
||||
duration: 0.12,
|
||||
ease: "none",
|
||||
},
|
||||
1.15,
|
||||
);
|
||||
|
||||
// Fade out "Follow", fade in "Following"
|
||||
tl.to(
|
||||
textFollow,
|
||||
{
|
||||
opacity: 0,
|
||||
duration: 0.08,
|
||||
ease: "none",
|
||||
},
|
||||
1.15,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
textFollowing,
|
||||
{
|
||||
opacity: 1,
|
||||
duration: 0.08,
|
||||
ease: "none",
|
||||
},
|
||||
1.18,
|
||||
);
|
||||
|
||||
// Slide out to bottom
|
||||
tl.to(
|
||||
card,
|
||||
{
|
||||
y: 300,
|
||||
opacity: 0,
|
||||
duration: 0.25,
|
||||
ease: "power3.in",
|
||||
},
|
||||
3.8,
|
||||
);
|
||||
|
||||
window.__timelines["instagram-follow"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="instagram-follow" data-composition-src="compositions/instagram-follow.html" data-start="0" data-duration="4.5" data-track-index="1" data-width="1080" data-height="1920"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,34 +3,397 @@ title: "Light Leak"
|
||||
description: "Shader transition with cinematic light leak overlay"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/light-leak.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/light-leak.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/light-leak.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Light Leak preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add light-leak
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add light-leak" />
|
||||
|
||||
That writes one file: `compositions/light-leak.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`light-leak.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="light-leak"
|
||||
data-composition-src="compositions/light-leak.html"
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<style>
|
||||
[data-composition-id="main"] {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #001524;
|
||||
}
|
||||
|
||||
/* Blueprint grid */
|
||||
.bp-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.bp-left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.bp-right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 720px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 60px 60px 60px 40px;
|
||||
}
|
||||
.bp-divider {
|
||||
position: absolute;
|
||||
left: 1200px;
|
||||
top: 60px;
|
||||
bottom: 60px;
|
||||
width: 1px;
|
||||
background: #fb8b24;
|
||||
opacity: 0.15;
|
||||
}
|
||||
.bp-number {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4em;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 60px;
|
||||
color: #fb8b24;
|
||||
}
|
||||
|
||||
/* Right panel typography */
|
||||
.bp-name {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 38px;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 20px;
|
||||
color: #fb8b24;
|
||||
}
|
||||
.bp-plabel {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
color: #fb8b24;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.bp-prompt {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
padding: 16px 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
color: #fb8b24;
|
||||
background: rgba(251, 139, 36, 0.06);
|
||||
border: 1px solid rgba(251, 139, 36, 0.12);
|
||||
}
|
||||
.bp-desc {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
color: #708d81;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* GL scenes (hidden, just for texture capture) */
|
||||
.scene {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.scene-label {
|
||||
font-family: "Bebas Neue", sans-serif;
|
||||
font-size: 140px;
|
||||
letter-spacing: 0.06em;
|
||||
opacity: 0.12;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="bp-grid"></div>
|
||||
<div class="bp-divider"></div>
|
||||
<div class="bp-number">14 / 14</div>
|
||||
|
||||
<!-- GL demo scenes (captured as textures, then hidden) -->
|
||||
<div id="s1" class="scene" style="background: #001524">
|
||||
<div class="scene-label" style="color: #fb8b24">SCENE A</div>
|
||||
</div>
|
||||
<div id="s2" class="scene" style="background: #fb8b24; opacity: 0">
|
||||
<div class="scene-label" style="color: #001524">SCENE B</div>
|
||||
</div>
|
||||
|
||||
<!-- GL canvas (left panel only) -->
|
||||
<canvas
|
||||
id="gl-canvas"
|
||||
width="1200"
|
||||
height="1080"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1200px;
|
||||
height: 1080px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
"
|
||||
>
|
||||
</canvas>
|
||||
|
||||
<!-- Right panel metadata -->
|
||||
<div class="bp-right">
|
||||
<div class="bp-name">Light Leak</div>
|
||||
<div class="bp-plabel">Prompt</div>
|
||||
<div class="bp-prompt">"use light leak shader transition"</div>
|
||||
<div class="bp-desc">
|
||||
Beer-Lambert exponential falloff with ACES tone mapping and directional flare streak.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var glCanvas = document.getElementById("gl-canvas");
|
||||
var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
|
||||
if (!gl) {
|
||||
console.warn("WebGL not available");
|
||||
window.__timelines["main"] = gsap.timeline({ paused: true });
|
||||
} else {
|
||||
gl.viewport(0, 0, 1200, 1080);
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
||||
var sceneTextures = {};
|
||||
|
||||
function captureScene(sceneId) {
|
||||
var scene = document.getElementById(sceneId);
|
||||
var origOpacity = scene.style.opacity;
|
||||
var origZ = scene.style.zIndex;
|
||||
scene.style.opacity = "1";
|
||||
scene.style.zIndex = "999";
|
||||
scene.offsetHeight;
|
||||
var c = document.createElement("canvas");
|
||||
c.width = 1200;
|
||||
c.height = 1080;
|
||||
var ctx = c.getContext("2d");
|
||||
ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
|
||||
ctx.fillRect(0, 0, 1200, 1080);
|
||||
var sr = scene.getBoundingClientRect();
|
||||
var els = scene.querySelectorAll("*");
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
var el = els[i],
|
||||
cs = window.getComputedStyle(el);
|
||||
if (cs.display === "none" || cs.visibility === "hidden") continue;
|
||||
var r = el.getBoundingClientRect();
|
||||
if (r.width < 1 || r.height < 1) continue;
|
||||
var x = r.left - sr.left,
|
||||
y = r.top - sr.top,
|
||||
w = r.width,
|
||||
h = r.height;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = parseFloat(cs.opacity) || 1;
|
||||
var bg = cs.backgroundColor;
|
||||
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
}
|
||||
var hasChildEls = el.querySelector("div,span,img,video");
|
||||
var text = "";
|
||||
for (var j = 0; j < el.childNodes.length; j++)
|
||||
if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
|
||||
text = text.trim();
|
||||
if (text && !hasChildEls) {
|
||||
ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
|
||||
ctx.fillStyle = cs.color;
|
||||
if (cs.letterSpacing && cs.letterSpacing !== "normal")
|
||||
ctx.letterSpacing = cs.letterSpacing;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(text, x + w / 2, y + h / 2);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
scene.style.opacity = origOpacity;
|
||||
scene.style.zIndex = origZ;
|
||||
var tex = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
|
||||
sceneTextures[sceneId] = tex;
|
||||
}
|
||||
|
||||
var vertSrc =
|
||||
"attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
|
||||
"v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
|
||||
var quadBuf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.bufferData(
|
||||
gl.ARRAY_BUFFER,
|
||||
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
||||
gl.STATIC_DRAW,
|
||||
);
|
||||
function compileShader(src, type) {
|
||||
var s = gl.createShader(type);
|
||||
gl.shaderSource(s, src);
|
||||
gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
|
||||
console.error("Shader:", gl.getShaderInfoLog(s));
|
||||
return s;
|
||||
}
|
||||
function mkProg(fragSrc) {
|
||||
var p = gl.createProgram();
|
||||
gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
|
||||
gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
|
||||
gl.linkProgram(p);
|
||||
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
|
||||
console.error("Link:", gl.getProgramInfoLog(p));
|
||||
return p;
|
||||
}
|
||||
|
||||
var H =
|
||||
"precision mediump float;" +
|
||||
"varying vec2 v_uv;" +
|
||||
"uniform sampler2D u_from, u_to;" +
|
||||
"uniform float u_progress;" +
|
||||
"uniform vec2 u_resolution;\n";
|
||||
|
||||
var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
|
||||
var progTrans = mkProg(
|
||||
H +
|
||||
"vec3 aces(vec3 x){return clamp((x*(2.51*x+.03))/(x*(2.43*x+.59)+.14),0.,1.);}void main(){vec4 A=texture2D(u_from,v_uv),B=texture2D(u_to,v_uv);vec2 lp=vec2(1.3,-.2);float dist=length(v_uv-lp);float leak=clamp(exp(-dist*1.8)*u_progress*4.,0.,1.);vec3 warmColor=mix(vec3(1.,.5,.15),vec3(1.,.9,.75),dist*.7);float flare=exp(-abs(v_uv.y-(-.2+v_uv.x*.3))*15.)*leak*.3;vec3 overexposed=A.rgb+warmColor*leak*3.+vec3(1.,.8,.5)*flare;overexposed=aces(overexposed);gl_FragColor=vec4(mix(overexposed,B.rgb,smoothstep(.15,.85,u_progress)),1.);}",
|
||||
);
|
||||
|
||||
function renderShader(prog, texFrom, texTo, progress) {
|
||||
gl.useProgram(prog);
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texFrom);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texTo);
|
||||
gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
|
||||
gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
|
||||
gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
|
||||
var pos = gl.getAttribLocation(prog, "a_pos");
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
|
||||
gl.enableVertexAttribArray(pos);
|
||||
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
function easeInOut(p) {
|
||||
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
|
||||
}
|
||||
|
||||
captureScene("s1");
|
||||
captureScene("s2");
|
||||
glCanvas.style.display = "block";
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
document.querySelectorAll(".scene").forEach(function (s) {
|
||||
s.style.opacity = "0";
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({
|
||||
paused: true,
|
||||
onUpdate: function () {
|
||||
var t = tl.time();
|
||||
if (t >= 1.0 && t < 3.0) {
|
||||
renderShader(
|
||||
progTrans,
|
||||
sceneTextures["s1"],
|
||||
sceneTextures["s2"],
|
||||
easeInOut((t - 1.0) / 2.0),
|
||||
);
|
||||
} else if (t >= 3.0) {
|
||||
renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
|
||||
} else {
|
||||
renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
|
||||
window.__timelines["main"] = tl;
|
||||
} // end if (gl)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="light-leak" data-composition-src="compositions/light-leak.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,40 +3,661 @@ title: "Liquid Glass Context Menu"
|
||||
description: "Frosted glass context menu panel drifting over an aurora shader background"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-context-menu.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-context-menu.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/liquid-glass-context-menu.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Liquid Glass Context Menu preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add liquid-glass-context-menu
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add liquid-glass-context-menu" />
|
||||
|
||||
That writes `compositions/liquid-glass-context-menu.html`, plus 1 supporting file under `compositions/lib/`.
|
||||
|
||||
<Warning>
|
||||
<Danger>
|
||||
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
|
||||
Rendering from the CLI switches it on for you. [How it
|
||||
works](/guides/html-in-canvas)
|
||||
</Warning>
|
||||
Without it this item's screen renders black. Rendering from the CLI switches
|
||||
it on for you. [How it works](/guides/html-in-canvas)
|
||||
</Danger>
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`liquid-glass-context-menu.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Liquid Glass Context Menu</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script src="lib/liquid-glass.iife.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0218;
|
||||
overflow: hidden;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#three-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
z-index: 0;
|
||||
}
|
||||
#glass-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ── Glass Panel ── */
|
||||
.menu-panel {
|
||||
position: absolute;
|
||||
width: 390px;
|
||||
height: 506px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
--lg-blur: 0.56;
|
||||
--lg-refraction: 0.52;
|
||||
--lg-corner-radius: 30;
|
||||
--lg-z-radius: 48;
|
||||
--lg-specular: 0.26;
|
||||
--lg-fresnel: 1;
|
||||
--lg-edge-highlight: 0.2;
|
||||
--lg-chrom-aberration: 0.035;
|
||||
--lg-tint: 0.88;
|
||||
--lg-brightness: 0.54;
|
||||
--lg-shadow-opacity: 0;
|
||||
--lg-shadow-spread: 0;
|
||||
--lg-shadow-offset-y: 0;
|
||||
--lg-saturation: -0.24;
|
||||
}
|
||||
#glass-menu {
|
||||
top: 270px;
|
||||
left: 1980px;
|
||||
}
|
||||
.file-panel {
|
||||
position: absolute;
|
||||
top: 372px;
|
||||
left: 500px;
|
||||
width: 360px;
|
||||
height: 104px;
|
||||
background: rgba(255, 255, 255, 0.11);
|
||||
--lg-blur: 0.52;
|
||||
--lg-refraction: 0.48;
|
||||
--lg-corner-radius: 28;
|
||||
--lg-z-radius: 36;
|
||||
--lg-specular: 0.28;
|
||||
--lg-fresnel: 0.94;
|
||||
--lg-edge-highlight: 0.18;
|
||||
--lg-chrom-aberration: 0.03;
|
||||
--lg-tint: 0.74;
|
||||
--lg-brightness: 0.34;
|
||||
--lg-shadow-opacity: 0;
|
||||
--lg-shadow-spread: 0;
|
||||
--lg-shadow-offset-y: 0;
|
||||
--lg-saturation: -0.18;
|
||||
}
|
||||
.submenu-panel {
|
||||
position: absolute;
|
||||
top: 456px;
|
||||
left: 1980px;
|
||||
width: 230px;
|
||||
height: 58px;
|
||||
background: rgba(255, 255, 255, 0.11);
|
||||
--lg-blur: 0.52;
|
||||
--lg-refraction: 0.5;
|
||||
--lg-corner-radius: 20;
|
||||
--lg-z-radius: 28;
|
||||
--lg-specular: 0.3;
|
||||
--lg-fresnel: 0.96;
|
||||
--lg-edge-highlight: 0.18;
|
||||
--lg-chrom-aberration: 0.03;
|
||||
--lg-tint: 0.74;
|
||||
--lg-brightness: 0.36;
|
||||
--lg-shadow-opacity: 0;
|
||||
--lg-shadow-spread: 0;
|
||||
--lg-shadow-offset-y: 0;
|
||||
--lg-saturation: -0.2;
|
||||
}
|
||||
|
||||
/* ── Text Overlay ── */
|
||||
.text-overlay {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
.context-file {
|
||||
position: absolute;
|
||||
top: 340px;
|
||||
left: 500px;
|
||||
z-index: 2;
|
||||
width: 360px;
|
||||
height: 104px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 18px 22px;
|
||||
border-radius: 26px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.095) 0%,
|
||||
rgba(255, 255, 255, 0.036) 38%,
|
||||
rgba(255, 255, 255, 0.006) 100%
|
||||
);
|
||||
box-shadow: none;
|
||||
}
|
||||
.file-icon {
|
||||
width: 58px;
|
||||
height: 68px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 16px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(6, 227, 250, 0.92), rgba(79, 219, 94, 0.85)),
|
||||
rgba(255, 255, 255, 0.12);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.34);
|
||||
}
|
||||
.file-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.file-title {
|
||||
color: rgba(5, 17, 28, 0.9);
|
||||
font-size: 21px;
|
||||
font-weight: 780;
|
||||
letter-spacing: 0;
|
||||
text-shadow:
|
||||
0 1px 0 rgba(255, 255, 255, 0.24),
|
||||
0 10px 20px rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
.file-subtitle {
|
||||
color: rgba(5, 17, 28, 0.72);
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
text-shadow:
|
||||
0 1px 0 rgba(255, 255, 255, 0.2),
|
||||
0 8px 18px rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
.pointer {
|
||||
position: absolute;
|
||||
top: 395px;
|
||||
left: 778px;
|
||||
z-index: 4;
|
||||
width: 34px;
|
||||
height: 44px;
|
||||
border-radius: 2px;
|
||||
background: #fff;
|
||||
clip-path: polygon(0 0, 0 100%, 31% 73%, 47% 100%, 62% 93%, 46% 67%, 82% 67%);
|
||||
filter: drop-shadow(0 8px 14px rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
.click-ring {
|
||||
position: absolute;
|
||||
top: 390px;
|
||||
left: 770px;
|
||||
z-index: 3;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.75);
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
}
|
||||
.menu-text {
|
||||
width: 390px;
|
||||
height: 506px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 18px 0;
|
||||
background: transparent;
|
||||
border-radius: 30px;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
#text-menu {
|
||||
top: 270px;
|
||||
left: 1980px;
|
||||
}
|
||||
|
||||
.menu-highlight {
|
||||
position: absolute;
|
||||
top: 17px;
|
||||
left: 14px;
|
||||
z-index: 0;
|
||||
width: 362px;
|
||||
height: 52px;
|
||||
border-radius: 17px;
|
||||
background:
|
||||
radial-gradient(circle at 24% 22%, rgba(255, 255, 255, 0.48), transparent 42%),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.29), rgba(255, 255, 255, 0.12));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.38),
|
||||
inset 0 -1px 0 rgba(255, 255, 255, 0.1);
|
||||
opacity: 0;
|
||||
}
|
||||
.menu-item {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 14px 28px;
|
||||
font-size: 18px;
|
||||
font-weight: 650;
|
||||
color: rgb(0, 7, 14);
|
||||
cursor: default;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.26);
|
||||
}
|
||||
.menu-item svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
stroke: rgb(0, 7, 14);
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.menu-sep {
|
||||
height: 1px;
|
||||
margin: 10px 28px;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.24), transparent);
|
||||
}
|
||||
.menu-footer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 26px;
|
||||
margin-top: auto;
|
||||
}
|
||||
.menu-footer-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
color: rgba(0, 7, 14, 0.9);
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.24);
|
||||
}
|
||||
.menu-footer-item svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
stroke: rgba(0, 7, 14, 0.84);
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.menu-kbd {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
color: rgba(0, 7, 14, 0.82);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.submenu-preview {
|
||||
position: absolute;
|
||||
top: 456px;
|
||||
left: 1980px;
|
||||
z-index: 2;
|
||||
width: 230px;
|
||||
height: 58px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 20px;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: rgba(0, 7, 14, 0.92);
|
||||
font-size: 15px;
|
||||
font-weight: 760;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="liquid-glass-context-menu"
|
||||
data-composition-src="compositions/liquid-glass-context-menu.html"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-root="true"
|
||||
>
|
||||
<canvas id="three-canvas" width="1920" height="1080"></canvas>
|
||||
|
||||
<canvas id="glass-canvas" layoutsubtree width="1920" height="1080">
|
||||
<div id="glass-file" class="file-panel liquid-glass"></div>
|
||||
<div id="glass-menu" class="menu-panel liquid-glass"></div>
|
||||
<div id="glass-submenu" class="submenu-panel liquid-glass"></div>
|
||||
</canvas>
|
||||
|
||||
<div id="context-file" class="context-file">
|
||||
<div class="file-icon"></div>
|
||||
<div class="file-meta">
|
||||
<div class="file-title">liquid-glass.mov</div>
|
||||
<div class="file-subtitle">Preview render - 08s</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="click-ring" class="click-ring"></div>
|
||||
<div id="pointer" class="pointer"></div>
|
||||
|
||||
<div id="text-menu" class="text-overlay menu-text">
|
||||
<div id="menu-highlight" class="menu-highlight"></div>
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
|
||||
<polyline points="16 6 12 2 8 6" />
|
||||
<line x1="12" y1="2" x2="12" y2="15" />
|
||||
</svg>
|
||||
<span>Share</span>
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
<span>Add to Bookmarks</span>
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14" /></svg>
|
||||
<span>Add Bookmark to...</span>
|
||||
</div>
|
||||
<div class="menu-sep"></div>
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<line x1="9" y1="3" x2="9" y2="21" />
|
||||
</svg>
|
||||
<span>New Tab</span>
|
||||
<span class="menu-kbd">⌘T</span>
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<path d="M9 3v18" />
|
||||
<path d="M14 9l3 3-3 3" />
|
||||
</svg>
|
||||
<span>New Private Tab</span>
|
||||
<span class="menu-kbd">⇧⌘N</span>
|
||||
</div>
|
||||
<div class="menu-sep"></div>
|
||||
<div class="menu-footer">
|
||||
<div class="menu-footer-item">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
<span>Bookmarks</span>
|
||||
</div>
|
||||
<div class="menu-footer-item">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
<span>All Tabs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="submenu-preview" class="submenu-preview">Open in New Tab</div>
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var W = 1920,
|
||||
H = 1080,
|
||||
DURATION = 8;
|
||||
|
||||
/* ── Three.js Aurora Shader ─────────────────────────────────────── */
|
||||
var vs = "varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 1.0); }";
|
||||
var fs = [
|
||||
"precision highp float;",
|
||||
"varying vec2 vUv;",
|
||||
"uniform float uTime;",
|
||||
"vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}",
|
||||
"vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}",
|
||||
"vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}",
|
||||
"vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}",
|
||||
"float snoise(vec3 v){",
|
||||
" const vec2 C=vec2(1.0/6.0,1.0/3.0);const vec4 D=vec4(0.0,0.5,1.0,2.0);",
|
||||
" vec3 i=floor(v+dot(v,C.yyy));vec3 x0=v-i+dot(i,C.xxx);",
|
||||
" vec3 g=step(x0.yzx,x0.xyz);vec3 l=1.0-g;",
|
||||
" vec3 i1=min(g.xyz,l.zxy);vec3 i2=max(g.xyz,l.zxy);",
|
||||
" vec3 x1=x0-i1+C.xxx;vec3 x2=x0-i2+C.yyy;vec3 x3=x0-D.yyy;",
|
||||
" i=mod289(i);",
|
||||
" vec4 p=permute(permute(permute(i.z+vec4(0.0,i1.z,i2.z,1.0))+i.y+vec4(0.0,i1.y,i2.y,1.0))+i.x+vec4(0.0,i1.x,i2.x,1.0));",
|
||||
" float n_=0.142857142857;vec3 ns=n_*D.wyz-D.xzx;",
|
||||
" vec4 j=p-49.0*floor(p*ns.z*ns.z);vec4 x_=floor(j*ns.z);vec4 y_=floor(j-7.0*x_);",
|
||||
" vec4 x=x_*ns.x+ns.yyyy;vec4 y=y_*ns.x+ns.yyyy;vec4 h=1.0-abs(x)-abs(y);",
|
||||
" vec4 b0=vec4(x.xy,y.xy);vec4 b1=vec4(x.zw,y.zw);",
|
||||
" vec4 s0=floor(b0)*2.0+1.0;vec4 s1=floor(b1)*2.0+1.0;",
|
||||
" vec4 sh=-step(h,vec4(0.0));",
|
||||
" vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;",
|
||||
" vec3 p0=vec3(a0.xy,h.x);vec3 p1=vec3(a0.zw,h.y);vec3 p2=vec3(a1.xy,h.z);vec3 p3=vec3(a1.zw,h.w);",
|
||||
" vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));",
|
||||
" p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;",
|
||||
" vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0);m=m*m;",
|
||||
" return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));",
|
||||
"}",
|
||||
"void main(){",
|
||||
" vec2 uv=vUv;float ar=1920.0/1080.0;vec2 p=vec2(uv.x*ar,uv.y);float t=uTime*0.06;",
|
||||
" float wx=snoise(vec3(p*1.4,t*0.5))*0.2;float wy=snoise(vec3(p*1.4+100.0,t*0.5))*0.2;",
|
||||
" vec2 wp=p+vec2(wx,wy);",
|
||||
" vec3 base=mix(vec3(0.10,0.02,0.22),vec3(0.04,0.10,0.25),smoothstep(0.0,1.0,uv.x*0.6+uv.y*0.4));",
|
||||
" float angle=0.65;float ridgeUv=dot(wp,vec2(cos(angle),sin(angle)));",
|
||||
" float ridge1=sin(ridgeUv*8.0+t*2.0+snoise(vec3(wp*0.8,t))*1.5);",
|
||||
" float ridge2=sin(ridgeUv*5.5-t*1.4+snoise(vec3(wp*1.2+50.0,t*0.7))*2.0);",
|
||||
" float ridge3=sin(ridgeUv*12.0+t*3.0+snoise(vec3(wp*0.6+80.0,t*0.4))*1.0);",
|
||||
" float crease1=pow(abs(ridge1),0.35)*sign(ridge1)*0.5+0.5;",
|
||||
" float crease2=pow(abs(ridge2),0.4)*sign(ridge2)*0.5+0.5;",
|
||||
" float crease3=pow(abs(ridge3),0.5)*sign(ridge3)*0.5+0.5;",
|
||||
" vec3 warm=mix(vec3(0.9,0.4,0.08),vec3(0.95,0.65,0.15),crease1);",
|
||||
" float warmZone=smoothstep(0.15,0.75,uv.x+uv.y*0.4+snoise(vec3(uv*2.0,t))*0.3);",
|
||||
" vec3 cool=mix(vec3(0.02,0.4,0.55),vec3(0.25,0.7,0.82),crease2);",
|
||||
" float coolZone=smoothstep(0.25,0.85,1.2-uv.x+uv.y*0.5+snoise(vec3(uv*2.0+40.0,t*0.8))*0.25);",
|
||||
" vec3 hot=mix(vec3(0.8,0.1,0.4),vec3(0.6,0.08,0.65),crease3);",
|
||||
" float hotZone=smoothstep(0.5,0.9,snoise(vec3(uv*2.5+20.0,t*0.6))*0.5+0.5);",
|
||||
" vec3 col=base;",
|
||||
" col=mix(col,warm,warmZone*0.75*smoothstep(0.3,0.7,crease1));",
|
||||
" col=mix(col,cool,coolZone*0.65*smoothstep(0.2,0.65,crease2));",
|
||||
" col=mix(col,hot,hotZone*0.5*smoothstep(0.35,0.8,crease3));",
|
||||
" float edgeGlow=pow(1.0-abs(ridge1),6.0)*0.4+pow(1.0-abs(ridge2),5.0)*0.25;",
|
||||
" col+=edgeGlow*mix(warm,cool,uv.x)*0.8;",
|
||||
" col*=1.0-pow(abs(ridge1),4.0)*0.15;",
|
||||
" float vig=1.0-smoothstep(0.5,1.5,length((uv-0.5)*vec2(1.3,1.0)));",
|
||||
" col*=0.65+vig*0.5;",
|
||||
" float luma=dot(col,vec3(0.299,0.587,0.114));col=mix(vec3(luma),col,1.5);",
|
||||
" col=clamp(col,0.0,1.0);col=pow(col,vec3(0.9));",
|
||||
" gl_FragColor=vec4(col,1.0);",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
/* ── Three.js Setup ─────────────────────────────────────────────── */
|
||||
var threeCanvas = document.getElementById("three-canvas");
|
||||
var renderer = new THREE.WebGLRenderer({ canvas: threeCanvas, alpha: false });
|
||||
renderer.setSize(W, H, false);
|
||||
renderer.setPixelRatio(1);
|
||||
|
||||
var scene = new THREE.Scene();
|
||||
var camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
var uniforms = { uTime: { value: 0 } };
|
||||
var mat = new THREE.ShaderMaterial({
|
||||
vertexShader: vs,
|
||||
fragmentShader: fs,
|
||||
uniforms: uniforms,
|
||||
});
|
||||
var quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), mat);
|
||||
scene.add(quad);
|
||||
|
||||
/* ── Glass Canvas ───────────────────────────────────────────────── */
|
||||
var glassCanvas = document.getElementById("glass-canvas");
|
||||
var ctx = glassCanvas.getContext("2d");
|
||||
var lg = new LiquidGlass.LiquidGlassCanvas(glassCanvas);
|
||||
var lgReady = false;
|
||||
|
||||
function renderGlass(time) {
|
||||
if (!lgReady) return;
|
||||
uniforms.uTime.value = time;
|
||||
renderer.render(scene, camera);
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
ctx.drawImage(threeCanvas, 0, 0, W, H);
|
||||
lg.renderGlassElements();
|
||||
}
|
||||
|
||||
function requestGlassRender(time) {
|
||||
if (glassCanvas.requestPaint) {
|
||||
glassCanvas.onpaint = function () {
|
||||
renderGlass(time);
|
||||
};
|
||||
glassCanvas.requestPaint();
|
||||
return;
|
||||
}
|
||||
renderGlass(time);
|
||||
}
|
||||
|
||||
lg.waitForInit().then(function (ok) {
|
||||
if (!ok) return;
|
||||
lgReady = true;
|
||||
requestGlassRender(0);
|
||||
});
|
||||
|
||||
/* ── GSAP Timeline ──────────────────────────────────────────────── */
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var gm = document.getElementById("glass-menu");
|
||||
var gf = document.getElementById("glass-file");
|
||||
var gsm = document.getElementById("glass-submenu");
|
||||
var tm = document.getElementById("text-menu");
|
||||
var pointer = document.getElementById("pointer");
|
||||
var fileCard = document.getElementById("context-file");
|
||||
|
||||
gsap.set([gm, tm], {
|
||||
left: 860,
|
||||
top: 270,
|
||||
opacity: 0,
|
||||
scale: 0.82,
|
||||
y: -8,
|
||||
transformOrigin: "0% 18%",
|
||||
});
|
||||
// glass (#glass-file) is html-in-canvas — stays on left/top (exempt, no stutter)
|
||||
gsap.set(gf, { top: 372, opacity: 1, scale: 1, transformOrigin: "78% 54%" });
|
||||
// plain DOM (#context-file) — CSS resting top is 340px, so top:372 becomes y:32
|
||||
gsap.set(fileCard, { y: 32, opacity: 1, scale: 1, transformOrigin: "78% 54%" });
|
||||
gsap.set([gsm, "#submenu-preview"], {
|
||||
left: 1980,
|
||||
opacity: 0,
|
||||
scale: 0.86,
|
||||
x: -12,
|
||||
transformOrigin: "0% 50%",
|
||||
});
|
||||
gsap.set(".menu-item", { y: -4, opacity: 0 });
|
||||
// #menu-highlight plain DOM — CSS resting top:17; top:17 seed is y:0, tweens use y
|
||||
gsap.set("#menu-highlight", { y: 0, opacity: 0 });
|
||||
|
||||
tl.to(gf, { top: 340, duration: 0.38, ease: "power3.out" }, 0.1);
|
||||
tl.to(fileCard, { y: 0, duration: 0.38, ease: "power3.out" }, 0.1);
|
||||
// #pointer plain DOM — CSS resting left:778 top:395; convert left/top → x/y offsets
|
||||
tl.fromTo(
|
||||
pointer,
|
||||
{ x: -158, y: 125 },
|
||||
{ x: 0, y: 0, duration: 0.55, ease: "power3.out" },
|
||||
0.35,
|
||||
);
|
||||
tl.set("#click-ring", { left: 756, top: 372, scale: 0.72 }, 0.84);
|
||||
tl.to("#click-ring", { opacity: 0.7, scale: 1.18, duration: 0.16, ease: "power2.out" }, 0.86);
|
||||
tl.to("#click-ring", { opacity: 0, scale: 1.8, duration: 0.28, ease: "power2.out" }, 1.02);
|
||||
tl.to(
|
||||
[gf, fileCard],
|
||||
{ scale: 0.985, duration: 0.08, ease: "power2.out", yoyo: true, repeat: 1 },
|
||||
0.86,
|
||||
);
|
||||
|
||||
tl.to([gm, tm], { opacity: 1, scale: 1, y: 0, duration: 0.36, ease: "back.out(1.08)" }, 0.98);
|
||||
tl.to(
|
||||
".menu-item",
|
||||
{ y: 0, opacity: 1, duration: 0.24, ease: "power3.out", stagger: 0.026 },
|
||||
1.07,
|
||||
);
|
||||
|
||||
tl.to("#menu-highlight", { opacity: 1, duration: 0.12, ease: "none" }, 1.35);
|
||||
tl.to("#menu-highlight", { y: 52, duration: 0.22, ease: "power2.out" }, 2.0);
|
||||
tl.to(pointer, { x: 350, y: 73, duration: 0.46, ease: "power3.inOut" }, 2.48);
|
||||
tl.to("#menu-highlight", { y: 170, duration: 0.22, ease: "power2.out" }, 2.58);
|
||||
tl.set([gsm, "#submenu-preview"], { left: 1236 }, 3.04);
|
||||
tl.fromTo(
|
||||
[gsm, "#submenu-preview"],
|
||||
{ scale: 0.86, opacity: 0, x: -12 },
|
||||
{ scale: 1, opacity: 1, x: 0, duration: 0.24, ease: "back.out(1.04)" },
|
||||
3.08,
|
||||
);
|
||||
tl.to(pointer, { x: 540, y: 89, duration: 0.5, ease: "power3.inOut" }, 3.32);
|
||||
tl.set("#click-ring", { left: 1295, top: 461, scale: 0.72 }, 3.86);
|
||||
tl.to(
|
||||
"#click-ring",
|
||||
{ opacity: 0.72, scale: 1.08, duration: 0.12, ease: "power2.out" },
|
||||
3.88,
|
||||
);
|
||||
tl.to("#click-ring", { opacity: 0, scale: 1.58, duration: 0.24, ease: "power2.out" }, 4.0);
|
||||
tl.to(
|
||||
[gsm, "#submenu-preview"],
|
||||
{ scale: 0.965, duration: 0.09, ease: "power2.out", yoyo: true, repeat: 1 },
|
||||
3.92,
|
||||
);
|
||||
|
||||
tl.to(pointer, { opacity: 0, duration: 0.35, ease: "power2.out" }, 5.85);
|
||||
|
||||
/* Duration driver */
|
||||
tl.to({ v: 0 }, { v: 1, duration: DURATION, ease: "none" }, 0);
|
||||
|
||||
tl.eventCallback("onUpdate", function () {
|
||||
requestGlassRender(tl.time());
|
||||
});
|
||||
|
||||
window.__timelines["liquid-glass-context-menu"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="liquid-glass-context-menu" data-composition-src="compositions/liquid-glass-context-menu.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,40 +3,722 @@ title: "Liquid Glass Media Controls"
|
||||
description: "Frosted glass media control panels spreading over an aurora shader background"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-media-controls.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-media-controls.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/liquid-glass-media-controls.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Liquid Glass Media Controls preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add liquid-glass-media-controls
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add liquid-glass-media-controls" />
|
||||
|
||||
That writes `compositions/liquid-glass-media-controls.html`, plus 1 supporting file under `compositions/lib/`.
|
||||
|
||||
<Warning>
|
||||
<Danger>
|
||||
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
|
||||
Rendering from the CLI switches it on for you. [How it
|
||||
works](/guides/html-in-canvas)
|
||||
</Warning>
|
||||
Without it this item's screen renders black. Rendering from the CLI switches
|
||||
it on for you. [How it works](/guides/html-in-canvas)
|
||||
</Danger>
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`liquid-glass-media-controls.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Liquid Glass Media Controls</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script src="lib/liquid-glass.iife.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0218;
|
||||
overflow: hidden;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#three-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
z-index: 0;
|
||||
}
|
||||
#glass-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ── Glass Panels ── */
|
||||
.glass-panel {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.055);
|
||||
--lg-blur: 0.42;
|
||||
--lg-refraction: 0.92;
|
||||
--lg-corner-radius: 36;
|
||||
--lg-z-radius: 58;
|
||||
--lg-specular: 0.46;
|
||||
--lg-fresnel: 1.15;
|
||||
--lg-edge-highlight: 0.34;
|
||||
--lg-chrom-aberration: 0.08;
|
||||
--lg-shadow-opacity: 0;
|
||||
--lg-shadow-spread: 0;
|
||||
--lg-shadow-offset-y: 0;
|
||||
--lg-saturation: 1.1;
|
||||
}
|
||||
.search-panel {
|
||||
width: 640px;
|
||||
height: 72px;
|
||||
--lg-corner-radius: 24;
|
||||
}
|
||||
.toggle-panel {
|
||||
width: 640px;
|
||||
height: 64px;
|
||||
--lg-corner-radius: 44;
|
||||
}
|
||||
.media-panel {
|
||||
width: 640px;
|
||||
height: 116px;
|
||||
--lg-corner-radius: 44;
|
||||
}
|
||||
.slider-panel {
|
||||
width: 640px;
|
||||
height: 76px;
|
||||
--lg-corner-radius: 28;
|
||||
}
|
||||
|
||||
#glass-search {
|
||||
top: 300px;
|
||||
left: 640px;
|
||||
}
|
||||
#glass-toggle {
|
||||
top: 390px;
|
||||
left: 640px;
|
||||
}
|
||||
#glass-media {
|
||||
top: 476px;
|
||||
left: 640px;
|
||||
}
|
||||
#glass-slider {
|
||||
top: 616px;
|
||||
left: 640px;
|
||||
}
|
||||
|
||||
/* ── Text Overlays ── */
|
||||
.text-overlay {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Search bar */
|
||||
.search-text {
|
||||
width: 640px;
|
||||
height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 0 28px;
|
||||
background: transparent;
|
||||
border-radius: 24px;
|
||||
box-shadow: none;
|
||||
}
|
||||
.search-text svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
stroke: rgba(255, 255, 255, 0.86);
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.search-label {
|
||||
font-size: 19px;
|
||||
font-weight: 650;
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
.search-caret {
|
||||
width: 2px;
|
||||
height: 25px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
opacity: 0;
|
||||
}
|
||||
#text-search {
|
||||
top: 300px;
|
||||
left: 640px;
|
||||
}
|
||||
|
||||
/* Toggle row */
|
||||
.toggle-text {
|
||||
width: 640px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
position: relative;
|
||||
padding: 6px;
|
||||
background: transparent;
|
||||
border-radius: 44px;
|
||||
box-shadow: none;
|
||||
}
|
||||
.toggle-active-pill {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
z-index: 0;
|
||||
width: 209px;
|
||||
height: 52px;
|
||||
border-radius: 999px;
|
||||
background:
|
||||
radial-gradient(circle at 28% 18%, rgba(255, 255, 255, 0.32), transparent 42%),
|
||||
rgba(255, 255, 255, 0.14);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.32),
|
||||
inset 0 -1px 0 rgba(255, 255, 255, 0.09);
|
||||
}
|
||||
.toggle-item {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.96);
|
||||
line-height: 52px;
|
||||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
.toggle-item.active {
|
||||
color: #fff;
|
||||
}
|
||||
#text-toggle {
|
||||
top: 390px;
|
||||
left: 640px;
|
||||
}
|
||||
|
||||
/* Media pill */
|
||||
.media-text {
|
||||
width: 640px;
|
||||
height: 116px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22px;
|
||||
padding: 0 24px;
|
||||
background: transparent;
|
||||
border-radius: 44px;
|
||||
box-shadow: none;
|
||||
}
|
||||
.album-art {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 78px;
|
||||
height: 78px;
|
||||
border-radius: 20px;
|
||||
background:
|
||||
radial-gradient(circle at 70% 28%, rgba(255, 255, 255, 0.78), transparent 16%),
|
||||
conic-gradient(from 210deg, #06e3fa, #4fdb5e, #f59e0b, #ec4899, #06e3fa);
|
||||
flex-shrink: 0;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.28),
|
||||
0 12px 26px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
.album-sheen {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -70px;
|
||||
width: 38px;
|
||||
height: 110px;
|
||||
background: rgba(255, 255, 255, 0.34);
|
||||
filter: blur(8px);
|
||||
transform: rotate(24deg);
|
||||
}
|
||||
.media-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.media-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
.media-artist {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.84);
|
||||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
.media-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
.live-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #4ade80;
|
||||
box-shadow: 0 0 14px rgba(74, 222, 128, 0.76);
|
||||
}
|
||||
.sound-wave {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
gap: 3px;
|
||||
height: 16px;
|
||||
}
|
||||
.sound-wave span {
|
||||
width: 3px;
|
||||
height: 7px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.74);
|
||||
}
|
||||
.transport {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
margin-left: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.transport svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
fill: #fff;
|
||||
stroke: none;
|
||||
}
|
||||
.transport .play-btn svg {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
#text-media {
|
||||
top: 476px;
|
||||
left: 640px;
|
||||
}
|
||||
|
||||
/* Brightness slider */
|
||||
.slider-text {
|
||||
width: 640px;
|
||||
height: 76px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 0 28px;
|
||||
background: transparent;
|
||||
border-radius: 28px;
|
||||
box-shadow: none;
|
||||
}
|
||||
.slider-text svg {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
stroke: rgba(255, 255, 255, 0.88);
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.slider-track {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.24);
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
}
|
||||
.slider-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 34%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 14px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.slider-pct {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-variant-numeric: tabular-nums;
|
||||
width: 42px;
|
||||
text-align: right;
|
||||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
#text-slider {
|
||||
top: 616px;
|
||||
left: 640px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="liquid-glass-media-controls"
|
||||
data-composition-src="compositions/liquid-glass-media-controls.html"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-root="true"
|
||||
>
|
||||
<canvas id="three-canvas" width="1920" height="1080"></canvas>
|
||||
|
||||
<canvas id="glass-canvas" layoutsubtree width="1920" height="1080">
|
||||
<div id="glass-search" class="glass-panel search-panel liquid-glass"></div>
|
||||
<div id="glass-toggle" class="glass-panel toggle-panel liquid-glass"></div>
|
||||
<div id="glass-media" class="glass-panel media-panel liquid-glass"></div>
|
||||
<div id="glass-slider" class="glass-panel slider-panel liquid-glass"></div>
|
||||
</canvas>
|
||||
|
||||
<!-- Search bar -->
|
||||
<div id="text-search" class="text-overlay search-text">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
<span id="search-label" class="search-label">Search</span>
|
||||
<span id="search-caret" class="search-caret"></span>
|
||||
</div>
|
||||
|
||||
<!-- Toggle row -->
|
||||
<div id="text-toggle" class="text-overlay toggle-text">
|
||||
<div id="toggle-pill" class="toggle-active-pill"></div>
|
||||
<div class="toggle-item active">All</div>
|
||||
<div class="toggle-item">Favorites</div>
|
||||
<div class="toggle-item">Recent</div>
|
||||
</div>
|
||||
|
||||
<!-- Media pill -->
|
||||
<div id="text-media" class="text-overlay media-text">
|
||||
<div class="album-art"><span id="album-sheen" class="album-sheen"></span></div>
|
||||
<div class="media-info">
|
||||
<div class="media-title">Midnight Drive</div>
|
||||
<div class="media-meta-row">
|
||||
<span id="live-dot" class="live-dot"></span>
|
||||
<div class="media-artist">Glass Resonance</div>
|
||||
<div id="sound-wave" class="sound-wave">
|
||||
<span></span><span></span><span></span><span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="transport">
|
||||
<span>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<polygon points="19 20 9 12 19 4" />
|
||||
<line x1="5" y1="4" x2="5" y2="20" stroke="#fff" stroke-width="2" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="play-btn">
|
||||
<svg viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21" /></svg>
|
||||
</span>
|
||||
<span>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<polygon points="5 4 15 12 5 20" />
|
||||
<line x1="19" y1="4" x2="19" y2="20" stroke="#fff" stroke-width="2" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brightness slider -->
|
||||
<div id="text-slider" class="text-overlay slider-text">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<line x1="12" y1="1" x2="12" y2="3" />
|
||||
<line x1="12" y1="21" x2="12" y2="23" />
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
||||
<line x1="1" y1="12" x2="3" y2="12" />
|
||||
<line x1="21" y1="12" x2="23" y2="12" />
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
||||
</svg>
|
||||
<div class="slider-track"><div id="slider-fill" class="slider-fill"></div></div>
|
||||
<span id="slider-pct" class="slider-pct">34%</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var W = 1920,
|
||||
H = 1080,
|
||||
DURATION = 8;
|
||||
|
||||
/* ── Three.js Aurora Shader ─────────────────────────────────────── */
|
||||
var vs = "varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 1.0); }";
|
||||
var fs = [
|
||||
"precision highp float;",
|
||||
"varying vec2 vUv;",
|
||||
"uniform float uTime;",
|
||||
"vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}",
|
||||
"vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}",
|
||||
"vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}",
|
||||
"vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}",
|
||||
"float snoise(vec3 v){",
|
||||
" const vec2 C=vec2(1.0/6.0,1.0/3.0);const vec4 D=vec4(0.0,0.5,1.0,2.0);",
|
||||
" vec3 i=floor(v+dot(v,C.yyy));vec3 x0=v-i+dot(i,C.xxx);",
|
||||
" vec3 g=step(x0.yzx,x0.xyz);vec3 l=1.0-g;",
|
||||
" vec3 i1=min(g.xyz,l.zxy);vec3 i2=max(g.xyz,l.zxy);",
|
||||
" vec3 x1=x0-i1+C.xxx;vec3 x2=x0-i2+C.yyy;vec3 x3=x0-D.yyy;",
|
||||
" i=mod289(i);",
|
||||
" vec4 p=permute(permute(permute(i.z+vec4(0.0,i1.z,i2.z,1.0))+i.y+vec4(0.0,i1.y,i2.y,1.0))+i.x+vec4(0.0,i1.x,i2.x,1.0));",
|
||||
" float n_=0.142857142857;vec3 ns=n_*D.wyz-D.xzx;",
|
||||
" vec4 j=p-49.0*floor(p*ns.z*ns.z);vec4 x_=floor(j*ns.z);vec4 y_=floor(j-7.0*x_);",
|
||||
" vec4 x=x_*ns.x+ns.yyyy;vec4 y=y_*ns.x+ns.yyyy;vec4 h=1.0-abs(x)-abs(y);",
|
||||
" vec4 b0=vec4(x.xy,y.xy);vec4 b1=vec4(x.zw,y.zw);",
|
||||
" vec4 s0=floor(b0)*2.0+1.0;vec4 s1=floor(b1)*2.0+1.0;",
|
||||
" vec4 sh=-step(h,vec4(0.0));",
|
||||
" vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;",
|
||||
" vec3 p0=vec3(a0.xy,h.x);vec3 p1=vec3(a0.zw,h.y);vec3 p2=vec3(a1.xy,h.z);vec3 p3=vec3(a1.zw,h.w);",
|
||||
" vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));",
|
||||
" p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;",
|
||||
" vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0);m=m*m;",
|
||||
" return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));",
|
||||
"}",
|
||||
"void main(){",
|
||||
" vec2 uv=vUv;float ar=1920.0/1080.0;vec2 p=vec2(uv.x*ar,uv.y);float t=uTime*0.06;",
|
||||
" float wx=snoise(vec3(p*1.4,t*0.5))*0.2;float wy=snoise(vec3(p*1.4+100.0,t*0.5))*0.2;",
|
||||
" vec2 wp=p+vec2(wx,wy);",
|
||||
" vec3 base=mix(vec3(0.10,0.02,0.22),vec3(0.04,0.10,0.25),smoothstep(0.0,1.0,uv.x*0.6+uv.y*0.4));",
|
||||
" float angle=0.65;float ridgeUv=dot(wp,vec2(cos(angle),sin(angle)));",
|
||||
" float ridge1=sin(ridgeUv*8.0+t*2.0+snoise(vec3(wp*0.8,t))*1.5);",
|
||||
" float ridge2=sin(ridgeUv*5.5-t*1.4+snoise(vec3(wp*1.2+50.0,t*0.7))*2.0);",
|
||||
" float ridge3=sin(ridgeUv*12.0+t*3.0+snoise(vec3(wp*0.6+80.0,t*0.4))*1.0);",
|
||||
" float crease1=pow(abs(ridge1),0.35)*sign(ridge1)*0.5+0.5;",
|
||||
" float crease2=pow(abs(ridge2),0.4)*sign(ridge2)*0.5+0.5;",
|
||||
" float crease3=pow(abs(ridge3),0.5)*sign(ridge3)*0.5+0.5;",
|
||||
" vec3 warm=mix(vec3(0.9,0.4,0.08),vec3(0.95,0.65,0.15),crease1);",
|
||||
" float warmZone=smoothstep(0.15,0.75,uv.x+uv.y*0.4+snoise(vec3(uv*2.0,t))*0.3);",
|
||||
" vec3 cool=mix(vec3(0.02,0.4,0.55),vec3(0.25,0.7,0.82),crease2);",
|
||||
" float coolZone=smoothstep(0.25,0.85,1.2-uv.x+uv.y*0.5+snoise(vec3(uv*2.0+40.0,t*0.8))*0.25);",
|
||||
" vec3 hot=mix(vec3(0.8,0.1,0.4),vec3(0.6,0.08,0.65),crease3);",
|
||||
" float hotZone=smoothstep(0.5,0.9,snoise(vec3(uv*2.5+20.0,t*0.6))*0.5+0.5);",
|
||||
" vec3 col=base;",
|
||||
" col=mix(col,warm,warmZone*0.75*smoothstep(0.3,0.7,crease1));",
|
||||
" col=mix(col,cool,coolZone*0.65*smoothstep(0.2,0.65,crease2));",
|
||||
" col=mix(col,hot,hotZone*0.5*smoothstep(0.35,0.8,crease3));",
|
||||
" float edgeGlow=pow(1.0-abs(ridge1),6.0)*0.4+pow(1.0-abs(ridge2),5.0)*0.25;",
|
||||
" col+=edgeGlow*mix(warm,cool,uv.x)*0.8;",
|
||||
" col*=1.0-pow(abs(ridge1),4.0)*0.15;",
|
||||
" float vig=1.0-smoothstep(0.5,1.5,length((uv-0.5)*vec2(1.3,1.0)));",
|
||||
" col*=0.65+vig*0.5;",
|
||||
" float luma=dot(col,vec3(0.299,0.587,0.114));col=mix(vec3(luma),col,1.5);",
|
||||
" col=clamp(col,0.0,1.0);col=pow(col,vec3(0.9));",
|
||||
" gl_FragColor=vec4(col,1.0);",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
/* ── Three.js Setup ─────────────────────────────────────────────── */
|
||||
var threeCanvas = document.getElementById("three-canvas");
|
||||
var renderer = new THREE.WebGLRenderer({ canvas: threeCanvas, alpha: false });
|
||||
renderer.setSize(W, H, false);
|
||||
renderer.setPixelRatio(1);
|
||||
|
||||
var scene = new THREE.Scene();
|
||||
var camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
var uniforms = { uTime: { value: 0 } };
|
||||
var shaderMat = new THREE.ShaderMaterial({
|
||||
vertexShader: vs,
|
||||
fragmentShader: fs,
|
||||
uniforms: uniforms,
|
||||
});
|
||||
var quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), shaderMat);
|
||||
scene.add(quad);
|
||||
|
||||
/* ── Glass Canvas ───────────────────────────────────────────────── */
|
||||
var glassCanvas = document.getElementById("glass-canvas");
|
||||
var ctx = glassCanvas.getContext("2d");
|
||||
var lg = new LiquidGlass.LiquidGlassCanvas(glassCanvas);
|
||||
var lgReady = false;
|
||||
|
||||
function renderGlass(time) {
|
||||
if (!lgReady) return;
|
||||
uniforms.uTime.value = time;
|
||||
renderer.render(scene, camera);
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
ctx.drawImage(threeCanvas, 0, 0, W, H);
|
||||
lg.renderGlassElements();
|
||||
}
|
||||
|
||||
function requestGlassRender(time) {
|
||||
if (glassCanvas.requestPaint) {
|
||||
glassCanvas.onpaint = function () {
|
||||
renderGlass(time);
|
||||
};
|
||||
glassCanvas.requestPaint();
|
||||
return;
|
||||
}
|
||||
renderGlass(time);
|
||||
}
|
||||
|
||||
lg.waitForInit().then(function (ok) {
|
||||
if (!ok) return;
|
||||
lgReady = true;
|
||||
requestGlassRender(0);
|
||||
});
|
||||
|
||||
/* ── GSAP Timeline ──────────────────────────────────────────────── */
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var gs = document.getElementById("glass-search");
|
||||
var gt = document.getElementById("glass-toggle");
|
||||
var gm = document.getElementById("glass-media");
|
||||
var gsl = document.getElementById("glass-slider");
|
||||
var ts = document.getElementById("text-search");
|
||||
var tt = document.getElementById("text-toggle");
|
||||
var tm = document.getElementById("text-media");
|
||||
var tsl = document.getElementById("text-slider");
|
||||
var searchLabel = document.getElementById("search-label");
|
||||
var sliderPct = document.getElementById("slider-pct");
|
||||
var sliderValue = { value: 34 };
|
||||
|
||||
// Glass panels live inside <canvas layoutsubtree> (html-in-canvas): the canvas lib
|
||||
// reads their sub-pixel computed top/left, so animating layout props on them does
|
||||
// NOT stutter and is exempt from gsap_non_transform_motion. They keep top/left.
|
||||
// Plain-DOM text overlays DO stutter on layout props, so they use transforms (x/y).
|
||||
// CSS resting tops — search 300, toggle 390, media 476, slider 616 — are the
|
||||
// transform base; the original set seeded top:1160, i.e. y = 1160 - restingTop.
|
||||
gsap.set([gs, gt, gm, gsl], { top: 1160, opacity: 1 });
|
||||
gsap.set(ts, { y: 860, opacity: 1 });
|
||||
gsap.set(tt, { y: 770, opacity: 1 });
|
||||
gsap.set(tm, { y: 684, opacity: 1 });
|
||||
gsap.set(tsl, { y: 544, opacity: 1 });
|
||||
gsap.set("#sound-wave span", { height: 7 });
|
||||
|
||||
tl.to(gs, { top: 300, duration: 0.48, ease: "power3.out" }, 0.12);
|
||||
tl.to(ts, { y: 0, duration: 0.48, ease: "power3.out" }, 0.12);
|
||||
tl.to(gt, { top: 390, duration: 0.44, ease: "power3.out" }, 0.22);
|
||||
tl.to(tt, { y: 0, duration: 0.44, ease: "power3.out" }, 0.22);
|
||||
tl.to(gm, { top: 476, duration: 0.44, ease: "power3.out" }, 0.32);
|
||||
tl.to(tm, { y: 0, duration: 0.44, ease: "power3.out" }, 0.32);
|
||||
tl.to(gsl, { top: 616, duration: 0.46, ease: "power3.out" }, 0.42);
|
||||
tl.to(tsl, { y: 0, duration: 0.46, ease: "power3.out" }, 0.42);
|
||||
|
||||
tl.call(
|
||||
function () {
|
||||
searchLabel.textContent = "Search Music";
|
||||
},
|
||||
null,
|
||||
0.9,
|
||||
);
|
||||
tl.to(
|
||||
"#search-caret",
|
||||
{ opacity: 1, duration: 0.1, yoyo: true, repeat: 5, ease: "none" },
|
||||
0.94,
|
||||
);
|
||||
tl.call(
|
||||
function () {
|
||||
searchLabel.textContent = "Midnight Drive";
|
||||
},
|
||||
null,
|
||||
1.55,
|
||||
);
|
||||
tl.to("#toggle-pill", { x: 209, duration: 0.24, ease: "power2.out" }, 1.82);
|
||||
tl.to(
|
||||
".play-btn",
|
||||
{ scale: 0.86, duration: 0.12, ease: "power2.out", yoyo: true, repeat: 1 },
|
||||
2.18,
|
||||
);
|
||||
tl.to("#album-sheen", { x: 182, duration: 0.75, ease: "power2.out" }, 2.2);
|
||||
tl.to(
|
||||
"#sound-wave span",
|
||||
{ height: 15, duration: 0.22, ease: "power2.inOut", stagger: 0.05, yoyo: true, repeat: 12 },
|
||||
2.2,
|
||||
);
|
||||
tl.to(
|
||||
sliderValue,
|
||||
{
|
||||
value: 82,
|
||||
duration: 4.4,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
sliderPct.textContent = Math.round(sliderValue.value) + "%";
|
||||
},
|
||||
},
|
||||
1.2,
|
||||
);
|
||||
tl.to("#slider-fill", { width: "82%", duration: 4.4, ease: "none" }, 1.2);
|
||||
tl.to(
|
||||
"#live-dot",
|
||||
{ scale: 1.45, duration: 0.24, ease: "power2.out", yoyo: true, repeat: 4 },
|
||||
2.6,
|
||||
);
|
||||
tl.to("#toggle-pill", { x: 419, duration: 0.24, ease: "power2.out" }, 4.15);
|
||||
|
||||
// Settle + exit. Split so glass panels keep relative top (exempt, html-in-canvas)
|
||||
// and plain-DOM text overlays use the relative transform equivalent (y). Inline
|
||||
// array literals so the linter can resolve each group's targets; a pre-declared
|
||||
// array variable resolves to __unresolved__ and would lose the canvas exemption.
|
||||
tl.to(
|
||||
[gs, gt, gm, gsl],
|
||||
{ top: "-=8", duration: 0.22, ease: "power2.out", yoyo: true, repeat: 1 },
|
||||
5.55,
|
||||
);
|
||||
tl.to(
|
||||
[ts, tt, tm, tsl],
|
||||
{ y: "-=8", duration: 0.22, ease: "power2.out", yoyo: true, repeat: 1 },
|
||||
5.55,
|
||||
);
|
||||
tl.to([gs, gt, gm, gsl], { top: "+=760", duration: 0.42, ease: "power2.in" }, 6.72);
|
||||
tl.to([ts, tt, tm, tsl], { y: "+=760", duration: 0.42, ease: "power2.in" }, 6.72);
|
||||
|
||||
/* Duration driver */
|
||||
tl.to({ v: 0 }, { v: 1, duration: DURATION, ease: "none" }, 0);
|
||||
|
||||
tl.eventCallback("onUpdate", function () {
|
||||
requestGlassRender(tl.time());
|
||||
});
|
||||
|
||||
window.__timelines["liquid-glass-media-controls"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="liquid-glass-media-controls" data-composition-src="compositions/liquid-glass-media-controls.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,40 +3,519 @@ title: "Liquid Glass Notification"
|
||||
description: "Frosted glass notification cards floating over an aurora shader background"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-notification.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-notification.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/liquid-glass-notification.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Liquid Glass Notification preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add liquid-glass-notification
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add liquid-glass-notification" />
|
||||
|
||||
That writes `compositions/liquid-glass-notification.html`, plus 2 supporting files under `compositions/lib/` and `compositions/assets/`.
|
||||
That writes `compositions/liquid-glass-notification.html`, plus 2 supporting files under `compositions/assets/` and `compositions/lib/`.
|
||||
|
||||
<Warning>
|
||||
<Danger>
|
||||
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
|
||||
Rendering from the CLI switches it on for you. [How it
|
||||
works](/guides/html-in-canvas)
|
||||
</Warning>
|
||||
Without it this item's screen renders black. Rendering from the CLI switches
|
||||
it on for you. [How it works](/guides/html-in-canvas)
|
||||
</Danger>
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`liquid-glass-notification.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script src="lib/liquid-glass.iife.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
body {
|
||||
font-family: "Inter", sans-serif;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#three-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
display: block;
|
||||
}
|
||||
#glass-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
.text-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
position: absolute;
|
||||
width: 520px;
|
||||
height: 118px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
--lg-blur: 0.22;
|
||||
--lg-refraction: 0.6;
|
||||
--lg-corner-radius: 36;
|
||||
--lg-z-radius: 38;
|
||||
--lg-specular: 0.2;
|
||||
--lg-fresnel: 0.9;
|
||||
--lg-edge-highlight: 0.1;
|
||||
--lg-chrom-aberration: 0.05;
|
||||
--lg-shadow-opacity: 0.35;
|
||||
--lg-shadow-spread: 14;
|
||||
--lg-shadow-offset-y: 4;
|
||||
--lg-saturation: 0.25;
|
||||
}
|
||||
.glass-panel.p1 {
|
||||
top: 60px;
|
||||
left: 1340px;
|
||||
}
|
||||
.glass-panel.p2 {
|
||||
top: 196px;
|
||||
left: 1340px;
|
||||
}
|
||||
|
||||
.notif-card {
|
||||
position: absolute;
|
||||
width: 520px;
|
||||
height: 118px;
|
||||
padding: 18px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
.notif-card.c1 {
|
||||
top: 60px;
|
||||
left: 1340px;
|
||||
}
|
||||
.notif-card.c2 {
|
||||
top: 196px;
|
||||
left: 1340px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 19px;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.35),
|
||||
0 10px 24px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
.avatar-ja {
|
||||
width: 128px;
|
||||
border-radius: 20px;
|
||||
background: rgba(2, 4, 10, 0.72);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.18),
|
||||
0 10px 24px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
.hyperframes-logo {
|
||||
width: 102px;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
.avatar-kl {
|
||||
background: linear-gradient(135deg, #ff7ab6, #8b5cf6);
|
||||
}
|
||||
.notif-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.notif-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.notif-name {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.notif-time {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
.notif-text {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
line-height: 1.32;
|
||||
}
|
||||
.notif-progress {
|
||||
position: absolute;
|
||||
right: 26px;
|
||||
bottom: 15px;
|
||||
left: 168px;
|
||||
height: 3px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
.notif-progress-fill {
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #06e3fa, #4fdb5e);
|
||||
box-shadow: 0 0 16px rgba(79, 219, 94, 0.55);
|
||||
}
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #06e3fa;
|
||||
box-shadow: 0 0 16px rgba(6, 227, 250, 0.65);
|
||||
}
|
||||
.notif-actions {
|
||||
position: absolute;
|
||||
right: 22px;
|
||||
bottom: 13px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
opacity: 0;
|
||||
}
|
||||
.action-chip {
|
||||
padding: 5px 11px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
color: rgba(255, 255, 255, 0.94);
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
.typing-dots {
|
||||
display: inline-flex;
|
||||
gap: 3px;
|
||||
margin-left: 5px;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.typing-dots span {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="liquid-glass-notification"
|
||||
data-composition-src="compositions/liquid-glass-notification.html"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<canvas id="three-canvas"></canvas>
|
||||
|
||||
<canvas id="glass-canvas" layoutsubtree width="1920" height="1080">
|
||||
<div class="glass-panel liquid-glass p1" id="gp1"></div>
|
||||
<div class="glass-panel liquid-glass p2" id="gp2"></div>
|
||||
</canvas>
|
||||
|
||||
<div class="text-overlay">
|
||||
<div class="notif-card c1" id="txt1">
|
||||
<div class="avatar avatar-ja">
|
||||
<img
|
||||
class="hyperframes-logo"
|
||||
src="assets/hyperframes-logo-dark.svg"
|
||||
alt="HyperFrames"
|
||||
/>
|
||||
</div>
|
||||
<div class="notif-body">
|
||||
<div class="notif-header">
|
||||
<span class="notif-name">Render complete</span>
|
||||
<span class="status-dot" id="status-dot"></span>
|
||||
<span class="notif-time" id="render-status">rendering</span>
|
||||
</div>
|
||||
<div class="notif-text" id="render-copy">
|
||||
Liquid glass preview is encoding with GPU capture.
|
||||
</div>
|
||||
<div class="notif-progress">
|
||||
<div class="notif-progress-fill" id="render-progress"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notif-card c2" id="txt2">
|
||||
<div class="avatar avatar-kl">M</div>
|
||||
<div class="notif-body">
|
||||
<div class="notif-header">
|
||||
<span class="notif-name">Messages</span>
|
||||
<span class="notif-time">now</span>
|
||||
</div>
|
||||
<div class="notif-text">
|
||||
Miguel: can it feel alive but still native?
|
||||
<span class="typing-dots" id="typing-dots"
|
||||
><span></span><span></span><span></span
|
||||
></span>
|
||||
</div>
|
||||
<div class="notif-actions" id="reply-actions">
|
||||
<span class="action-chip">Reply</span>
|
||||
<span class="action-chip">Mark Read</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var threeCanvas = document.getElementById("three-canvas");
|
||||
var threeRenderer = new THREE.WebGLRenderer({
|
||||
canvas: threeCanvas,
|
||||
antialias: false,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
threeRenderer.setSize(1920, 1080);
|
||||
threeRenderer.setPixelRatio(1);
|
||||
var threeScene = new THREE.Scene();
|
||||
var threeCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
|
||||
var fs = [
|
||||
"precision highp float;",
|
||||
"varying vec2 vUv;",
|
||||
"uniform float uTime;",
|
||||
"vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}",
|
||||
"vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}",
|
||||
"vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}",
|
||||
"vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}",
|
||||
"float snoise(vec3 v){",
|
||||
" const vec2 C=vec2(1.0/6.0,1.0/3.0);const vec4 D=vec4(0.0,0.5,1.0,2.0);",
|
||||
" vec3 i=floor(v+dot(v,C.yyy));vec3 x0=v-i+dot(i,C.xxx);",
|
||||
" vec3 g=step(x0.yzx,x0.xyz);vec3 l=1.0-g;",
|
||||
" vec3 i1=min(g.xyz,l.zxy);vec3 i2=max(g.xyz,l.zxy);",
|
||||
" vec3 x1=x0-i1+C.xxx;vec3 x2=x0-i2+C.yyy;vec3 x3=x0-D.yyy;",
|
||||
" i=mod289(i);",
|
||||
" vec4 p=permute(permute(permute(i.z+vec4(0.0,i1.z,i2.z,1.0))+i.y+vec4(0.0,i1.y,i2.y,1.0))+i.x+vec4(0.0,i1.x,i2.x,1.0));",
|
||||
" float n_=0.142857142857;vec3 ns=n_*D.wyz-D.xzx;",
|
||||
" vec4 j=p-49.0*floor(p*ns.z*ns.z);vec4 x_=floor(j*ns.z);vec4 y_=floor(j-7.0*x_);",
|
||||
" vec4 x=x_*ns.x+ns.yyyy;vec4 y=y_*ns.x+ns.yyyy;vec4 h=1.0-abs(x)-abs(y);",
|
||||
" vec4 b0=vec4(x.xy,y.xy);vec4 b1=vec4(x.zw,y.zw);",
|
||||
" vec4 s0=floor(b0)*2.0+1.0;vec4 s1=floor(b1)*2.0+1.0;",
|
||||
" vec4 sh=-step(h,vec4(0.0));",
|
||||
" vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;",
|
||||
" vec3 p0=vec3(a0.xy,h.x);vec3 p1=vec3(a0.zw,h.y);vec3 p2=vec3(a1.xy,h.z);vec3 p3=vec3(a1.zw,h.w);",
|
||||
" vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));",
|
||||
" p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;",
|
||||
" vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0);m=m*m;",
|
||||
" return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));",
|
||||
"}",
|
||||
"void main(){",
|
||||
" vec2 uv=vUv;float ar=1920.0/1080.0;vec2 p=vec2(uv.x*ar,uv.y);float t=uTime*0.06;",
|
||||
" float wx=snoise(vec3(p*1.4,t*0.5))*0.2;float wy=snoise(vec3(p*1.4+100.0,t*0.5))*0.2;",
|
||||
" vec2 wp=p+vec2(wx,wy);",
|
||||
" vec3 base=mix(vec3(0.10,0.02,0.22),vec3(0.04,0.10,0.25),smoothstep(0.0,1.0,uv.x*0.6+uv.y*0.4));",
|
||||
" float angle=0.65;float ridgeUv=dot(wp,vec2(cos(angle),sin(angle)));",
|
||||
" float ridge1=sin(ridgeUv*8.0+t*2.0+snoise(vec3(wp*0.8,t))*1.5);",
|
||||
" float ridge2=sin(ridgeUv*5.5-t*1.4+snoise(vec3(wp*1.2+50.0,t*0.7))*2.0);",
|
||||
" float ridge3=sin(ridgeUv*12.0+t*3.0+snoise(vec3(wp*0.6+80.0,t*0.4))*1.0);",
|
||||
" float crease1=pow(abs(ridge1),0.35)*sign(ridge1)*0.5+0.5;",
|
||||
" float crease2=pow(abs(ridge2),0.4)*sign(ridge2)*0.5+0.5;",
|
||||
" float crease3=pow(abs(ridge3),0.5)*sign(ridge3)*0.5+0.5;",
|
||||
" vec3 warm=mix(vec3(0.9,0.4,0.08),vec3(0.95,0.65,0.15),crease1);",
|
||||
" float warmZone=smoothstep(0.15,0.75,uv.x+uv.y*0.4+snoise(vec3(uv*2.0,t))*0.3);",
|
||||
" vec3 cool=mix(vec3(0.02,0.4,0.55),vec3(0.25,0.7,0.82),crease2);",
|
||||
" float coolZone=smoothstep(0.25,0.85,1.2-uv.x+uv.y*0.5+snoise(vec3(uv*2.0+40.0,t*0.8))*0.25);",
|
||||
" vec3 hot=mix(vec3(0.8,0.1,0.4),vec3(0.6,0.08,0.65),crease3);",
|
||||
" float hotZone=smoothstep(0.5,0.9,snoise(vec3(uv*2.5+20.0,t*0.6))*0.5+0.5);",
|
||||
" vec3 col=base;",
|
||||
" col=mix(col,warm,warmZone*0.75*smoothstep(0.3,0.7,crease1));",
|
||||
" col=mix(col,cool,coolZone*0.65*smoothstep(0.2,0.65,crease2));",
|
||||
" col=mix(col,hot,hotZone*0.5*smoothstep(0.35,0.8,crease3));",
|
||||
" float edgeGlow=pow(1.0-abs(ridge1),6.0)*0.4+pow(1.0-abs(ridge2),5.0)*0.25;",
|
||||
" col+=edgeGlow*mix(warm,cool,uv.x)*0.8;",
|
||||
" col*=1.0-pow(abs(ridge1),4.0)*0.15;",
|
||||
" float vig=1.0-smoothstep(0.5,1.5,length((uv-0.5)*vec2(1.3,1.0)));",
|
||||
" col*=0.65+vig*0.5;",
|
||||
" float luma=dot(col,vec3(0.299,0.587,0.114));col=mix(vec3(luma),col,1.5);",
|
||||
" col=clamp(col,0.0,1.0);col=pow(col,vec3(0.9));",
|
||||
" gl_FragColor=vec4(col,1.0);",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
var uniforms = { uTime: { value: 0.0 } };
|
||||
threeScene.add(
|
||||
new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(2, 2),
|
||||
new THREE.ShaderMaterial({
|
||||
vertexShader:
|
||||
"varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 1.0); }",
|
||||
fragmentShader: fs,
|
||||
uniforms: uniforms,
|
||||
}),
|
||||
),
|
||||
);
|
||||
threeRenderer.render(threeScene, threeCamera);
|
||||
|
||||
var glassCanvas = document.getElementById("glass-canvas");
|
||||
var glassCtx = glassCanvas.getContext("2d");
|
||||
var lg = new LiquidGlass.LiquidGlassCanvas(glassCanvas);
|
||||
|
||||
function renderFrame(time) {
|
||||
uniforms.uTime.value = time;
|
||||
threeRenderer.render(threeScene, threeCamera);
|
||||
if (!lg.isReady) return;
|
||||
glassCtx.clearRect(0, 0, 1920, 1080);
|
||||
glassCtx.drawImage(threeCanvas, 0, 0, 1920, 1080);
|
||||
lg.renderGlassElements();
|
||||
}
|
||||
|
||||
var lgReady = false;
|
||||
lg.waitForInit().then(function (ok) {
|
||||
if (!ok) return;
|
||||
lgReady = true;
|
||||
glassCanvas.onpaint = function () {
|
||||
renderFrame(0);
|
||||
};
|
||||
if (glassCanvas.requestPaint) glassCanvas.requestPaint();
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.to(
|
||||
{},
|
||||
{
|
||||
duration: 8,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
var t = tl.time();
|
||||
uniforms.uTime.value = t;
|
||||
threeRenderer.render(threeScene, threeCamera);
|
||||
if (lgReady) {
|
||||
glassCanvas.onpaint = function () {
|
||||
glassCtx.clearRect(0, 0, 1920, 1080);
|
||||
glassCtx.drawImage(threeCanvas, 0, 0, 1920, 1080);
|
||||
lg.renderGlassElements();
|
||||
};
|
||||
if (glassCanvas.requestPaint) glassCanvas.requestPaint();
|
||||
}
|
||||
},
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
var renderStatus = document.getElementById("render-status");
|
||||
var renderCopy = document.getElementById("render-copy");
|
||||
|
||||
gsap.set(["#gp1", "#gp2"], { left: "1980px", opacity: 0 });
|
||||
gsap.set(["#txt1", "#txt2"], { x: 640, opacity: 0 });
|
||||
gsap.set("#reply-actions", { y: 8, opacity: 0 });
|
||||
gsap.set("#typing-dots span", { opacity: 0.35 });
|
||||
|
||||
tl.to("#gp1", { left: "1326px", opacity: 1, duration: 0.38, ease: "power3.out" }, 0.18);
|
||||
tl.to("#txt1", { x: -14, opacity: 1, duration: 0.38, ease: "power3.out" }, 0.18);
|
||||
tl.to("#gp1", { left: "1340px", duration: 0.16, ease: "power2.out" }, 0.56);
|
||||
tl.to("#txt1", { x: 0, duration: 0.16, ease: "power2.out" }, 0.56);
|
||||
|
||||
tl.to("#gp2", { left: "1326px", opacity: 1, duration: 0.36, ease: "power3.out" }, 0.82);
|
||||
tl.to("#txt2", { x: -14, opacity: 1, duration: 0.36, ease: "power3.out" }, 0.82);
|
||||
tl.to("#gp2", { left: "1340px", duration: 0.16, ease: "power2.out" }, 1.18);
|
||||
tl.to("#txt2", { x: 0, duration: 0.16, ease: "power2.out" }, 1.18);
|
||||
|
||||
tl.to("#render-progress", { width: "100%", duration: 3.4, ease: "none" }, 0.82);
|
||||
tl.to(
|
||||
"#status-dot",
|
||||
{ scale: 1.45, duration: 0.18, ease: "power2.out", yoyo: true, repeat: 1 },
|
||||
1.1,
|
||||
);
|
||||
tl.to(
|
||||
"#typing-dots span",
|
||||
{
|
||||
opacity: 1,
|
||||
y: -2,
|
||||
duration: 0.22,
|
||||
ease: "power2.out",
|
||||
stagger: 0.08,
|
||||
yoyo: true,
|
||||
repeat: 5,
|
||||
},
|
||||
1.2,
|
||||
);
|
||||
tl.to("#gp2", { top: "+=10", duration: 0.18, ease: "power2.out" }, 2.55);
|
||||
tl.to("#txt2", { y: "+=10", duration: 0.18, ease: "power2.out" }, 2.55);
|
||||
tl.to("#gp2", { top: "-=10", duration: 0.22, ease: "power2.out" }, 2.73);
|
||||
tl.to("#txt2", { y: "-=10", duration: 0.22, ease: "power2.out" }, 2.73);
|
||||
tl.to("#reply-actions", { y: 0, opacity: 1, duration: 0.24, ease: "power2.out" }, 2.72);
|
||||
tl.to(
|
||||
"#status-dot",
|
||||
{
|
||||
backgroundColor: "#4ade80",
|
||||
boxShadow: "0 0 18px rgba(74, 222, 128, 0.8)",
|
||||
duration: 0.15,
|
||||
},
|
||||
4.25,
|
||||
);
|
||||
tl.call(
|
||||
function () {
|
||||
renderStatus.textContent = "complete";
|
||||
renderCopy.textContent = "liquid-glass-notification.mp4 is ready.";
|
||||
},
|
||||
null,
|
||||
4.25,
|
||||
);
|
||||
tl.to(
|
||||
"#gp1",
|
||||
{ top: "-=8", duration: 0.2, ease: "power2.out", yoyo: true, repeat: 1 },
|
||||
4.25,
|
||||
);
|
||||
tl.to(
|
||||
"#txt1",
|
||||
{ y: "-=8", duration: 0.2, ease: "power2.out", yoyo: true, repeat: 1 },
|
||||
4.25,
|
||||
);
|
||||
|
||||
tl.to("#gp1", { left: "1980px", opacity: 0, duration: 0.42, ease: "power2.in" }, 6.72);
|
||||
tl.to("#txt1", { x: 640, opacity: 0, duration: 0.42, ease: "power2.in" }, 6.72);
|
||||
tl.to("#gp2", { left: "1980px", opacity: 0, duration: 0.42, ease: "power2.in" }, 6.9);
|
||||
tl.to("#txt2", { x: 640, opacity: 0, duration: 0.42, ease: "power2.in" }, 6.9);
|
||||
|
||||
window.__timelines["liquid-glass-notification"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="liquid-glass-notification" data-composition-src="compositions/liquid-glass-notification.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,40 +3,797 @@ title: "Liquid Glass Widgets"
|
||||
description: "Frosted glass stat cards, showcase panel and pill chips over an aurora shader background"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-widgets.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-widgets.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/liquid-glass-widgets.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Liquid Glass Widgets preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add liquid-glass-widgets
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add liquid-glass-widgets" />
|
||||
|
||||
That writes `compositions/liquid-glass-widgets.html`, plus 2 supporting files under `compositions/assets/` and `compositions/lib/`.
|
||||
|
||||
<Warning>
|
||||
<Danger>
|
||||
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
|
||||
Rendering from the CLI switches it on for you. [How it
|
||||
works](/guides/html-in-canvas)
|
||||
</Warning>
|
||||
Without it this item's screen renders black. Rendering from the CLI switches
|
||||
it on for you. [How it works](/guides/html-in-canvas)
|
||||
</Danger>
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`liquid-glass-widgets.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Liquid Glass Widgets</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script src="lib/liquid-glass.iife.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #0a0218;
|
||||
overflow: hidden;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#three-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
z-index: 0;
|
||||
}
|
||||
#glass-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ── Glass Panels ── */
|
||||
.glass-panel {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
--lg-blur: 0.42;
|
||||
--lg-refraction: 0.82;
|
||||
--lg-corner-radius: 28;
|
||||
--lg-z-radius: 46;
|
||||
--lg-specular: 0.34;
|
||||
--lg-fresnel: 1.25;
|
||||
--lg-edge-highlight: 0.26;
|
||||
--lg-chrom-aberration: 0.08;
|
||||
--lg-shadow-opacity: 0;
|
||||
--lg-shadow-spread: 0;
|
||||
--lg-shadow-offset-y: 0;
|
||||
--lg-saturation: 0.38;
|
||||
}
|
||||
.stat-card {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
}
|
||||
.showcase-card {
|
||||
width: 500px;
|
||||
height: 320px;
|
||||
}
|
||||
.pill-chip {
|
||||
width: 130px;
|
||||
height: 36px;
|
||||
--lg-corner-radius: 40;
|
||||
}
|
||||
|
||||
#glass-stat1 {
|
||||
top: 300px;
|
||||
left: 200px;
|
||||
}
|
||||
#glass-stat2 {
|
||||
top: 300px;
|
||||
left: 460px;
|
||||
}
|
||||
#glass-stat3 {
|
||||
top: 300px;
|
||||
left: 720px;
|
||||
}
|
||||
#glass-showcase {
|
||||
top: 380px;
|
||||
left: 1100px;
|
||||
}
|
||||
#glass-pill1 {
|
||||
top: 720px;
|
||||
left: 300px;
|
||||
}
|
||||
#glass-pill2 {
|
||||
top: 720px;
|
||||
left: 460px;
|
||||
}
|
||||
#glass-pill3 {
|
||||
top: 720px;
|
||||
left: 620px;
|
||||
}
|
||||
|
||||
/* ── Text Overlays ── */
|
||||
.text-overlay {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Stat cards */
|
||||
.stat-text {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 22px;
|
||||
border-radius: 28px;
|
||||
background:
|
||||
linear-gradient(
|
||||
150deg,
|
||||
rgba(255, 255, 255, 0.28),
|
||||
rgba(255, 255, 255, 0.06) 46%,
|
||||
rgba(255, 255, 255, 0.16)
|
||||
),
|
||||
linear-gradient(180deg, rgba(9, 18, 31, 0.2), rgba(255, 255, 255, 0.04));
|
||||
border: 1px solid rgba(255, 255, 255, 0.36);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.5),
|
||||
inset 0 -18px 38px rgba(255, 255, 255, 0.08);
|
||||
overflow: hidden;
|
||||
text-shadow: 0 2px 16px rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
.stat-text::before,
|
||||
.showcase-text::before,
|
||||
.pill-text::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 1px;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
120deg,
|
||||
rgba(255, 255, 255, 0.44),
|
||||
transparent 28%,
|
||||
transparent 66%,
|
||||
rgba(255, 255, 255, 0.2)
|
||||
);
|
||||
opacity: 0.32;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
.stat-label {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
.stat-value {
|
||||
position: relative;
|
||||
font-size: 52px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
}
|
||||
.stat-unit {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
}
|
||||
.mini-meter {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 6px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
.mini-meter-fill {
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #06e3fa, #4fdb5e);
|
||||
box-shadow: 0 0 12px rgba(79, 219, 94, 0.55);
|
||||
}
|
||||
.battery-shell {
|
||||
position: relative;
|
||||
width: 118px;
|
||||
height: 40px;
|
||||
padding: 4px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.66);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.battery-shell::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: -9px;
|
||||
width: 5px;
|
||||
height: 14px;
|
||||
border-radius: 0 5px 5px 0;
|
||||
background: rgba(255, 255, 255, 0.66);
|
||||
}
|
||||
.battery-fill {
|
||||
width: 72%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(90deg, #4ade80, #06e3fa);
|
||||
box-shadow: 0 0 18px rgba(74, 222, 128, 0.45);
|
||||
}
|
||||
.steps-ring {
|
||||
width: 106px;
|
||||
height: 106px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(#a78bfa 0deg, #06e3fa 180deg, rgba(255, 255, 255, 0.18) 180deg);
|
||||
}
|
||||
.steps-ring-inner {
|
||||
width: 78px;
|
||||
height: 78px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: rgba(7, 10, 18, 0.36);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.22),
|
||||
inset 0 -8px 20px rgba(0, 0, 0, 0.14);
|
||||
color: #fff;
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
#text-stat1 {
|
||||
top: 300px;
|
||||
left: 200px;
|
||||
}
|
||||
#text-stat2 {
|
||||
top: 300px;
|
||||
left: 460px;
|
||||
}
|
||||
#text-stat3 {
|
||||
top: 300px;
|
||||
left: 720px;
|
||||
}
|
||||
|
||||
/* Showcase card */
|
||||
.showcase-text {
|
||||
width: 500px;
|
||||
height: 320px;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
padding: 32px;
|
||||
border-radius: 28px;
|
||||
background:
|
||||
radial-gradient(circle at 18% 12%, rgba(255, 255, 255, 0.38), transparent 24%),
|
||||
linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.24),
|
||||
rgba(255, 255, 255, 0.07) 52%,
|
||||
rgba(255, 255, 255, 0.18)
|
||||
);
|
||||
border: 1px solid rgba(255, 255, 255, 0.38);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.5),
|
||||
inset 0 -24px 48px rgba(255, 255, 255, 0.08);
|
||||
overflow: hidden;
|
||||
text-shadow: 0 2px 18px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.showcase-icon {
|
||||
position: relative;
|
||||
width: 168px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.showcase-logo {
|
||||
width: 150px;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
.showcase-title {
|
||||
position: relative;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.showcase-desc {
|
||||
position: relative;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
max-width: 380px;
|
||||
}
|
||||
.showcase-status {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.showcase-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #4ade80;
|
||||
box-shadow: 0 0 14px rgba(74, 222, 128, 0.7);
|
||||
}
|
||||
.showcase-progress {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
height: 7px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
.showcase-progress-fill {
|
||||
width: 18%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #06e3fa, #4fdb5e);
|
||||
}
|
||||
#text-showcase {
|
||||
top: 380px;
|
||||
left: 1100px;
|
||||
}
|
||||
|
||||
/* Pill chips */
|
||||
.pill-text {
|
||||
width: 130px;
|
||||
height: 36px;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border-radius: 40px;
|
||||
background: linear-gradient(130deg, rgba(255, 255, 255, 0.28), rgba(255, 255, 255, 0.1));
|
||||
border: 1px solid rgba(255, 255, 255, 0.34);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.48);
|
||||
overflow: hidden;
|
||||
}
|
||||
.pill-dot {
|
||||
position: relative;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dot-green {
|
||||
background: #4ade80;
|
||||
}
|
||||
.dot-blue {
|
||||
background: #60a5fa;
|
||||
}
|
||||
.dot-violet {
|
||||
background: #a78bfa;
|
||||
}
|
||||
.pill-label {
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
#text-pill1 {
|
||||
top: 720px;
|
||||
left: 300px;
|
||||
}
|
||||
#text-pill2 {
|
||||
top: 720px;
|
||||
left: 460px;
|
||||
}
|
||||
#text-pill3 {
|
||||
top: 720px;
|
||||
left: 620px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="liquid-glass-widgets"
|
||||
data-composition-src="compositions/liquid-glass-widgets.html"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-root="true"
|
||||
>
|
||||
<canvas id="three-canvas" width="1920" height="1080"></canvas>
|
||||
|
||||
<canvas id="glass-canvas" layoutsubtree width="1920" height="1080">
|
||||
<div id="glass-stat1" class="glass-panel stat-card liquid-glass"></div>
|
||||
<div id="glass-stat2" class="glass-panel stat-card liquid-glass"></div>
|
||||
<div id="glass-stat3" class="glass-panel stat-card liquid-glass"></div>
|
||||
<div id="glass-showcase" class="glass-panel showcase-card liquid-glass"></div>
|
||||
<div id="glass-pill1" class="glass-panel pill-chip liquid-glass"></div>
|
||||
<div id="glass-pill2" class="glass-panel pill-chip liquid-glass"></div>
|
||||
<div id="glass-pill3" class="glass-panel pill-chip liquid-glass"></div>
|
||||
</canvas>
|
||||
|
||||
<!-- Stat cards -->
|
||||
<div id="text-stat1" class="text-overlay stat-text">
|
||||
<span class="stat-label">Temperature</span>
|
||||
<span class="stat-value"
|
||||
><span id="temp-value">21</span><span class="stat-unit">°</span></span
|
||||
>
|
||||
<div class="mini-meter"><div id="temp-meter" class="mini-meter-fill"></div></div>
|
||||
</div>
|
||||
<div id="text-stat2" class="text-overlay stat-text">
|
||||
<span class="stat-label">Battery</span>
|
||||
<div class="battery-shell"><div id="battery-fill" class="battery-fill"></div></div>
|
||||
<span class="stat-value"
|
||||
><span id="battery-value">87</span><span class="stat-unit">%</span></span
|
||||
>
|
||||
</div>
|
||||
<div id="text-stat3" class="text-overlay stat-text">
|
||||
<span class="stat-label">Steps</span>
|
||||
<div id="steps-ring" class="steps-ring">
|
||||
<div id="steps-value" class="steps-ring-inner">8.2K</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Showcase card -->
|
||||
<div id="text-showcase" class="text-overlay showcase-text">
|
||||
<div class="showcase-icon">
|
||||
<img class="showcase-logo" src="assets/hyperframes-logo-dark.svg" alt="HyperFrames" />
|
||||
</div>
|
||||
<div class="showcase-title">Preview pipeline</div>
|
||||
<div class="showcase-desc">
|
||||
Write HTML, render video. Ship compositions at the speed of code.
|
||||
</div>
|
||||
<div class="showcase-status">
|
||||
<span class="showcase-dot" id="showcase-dot"></span
|
||||
><span id="showcase-status">Encoding frames</span>
|
||||
</div>
|
||||
<div class="showcase-progress">
|
||||
<div id="showcase-progress" class="showcase-progress-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pill chips -->
|
||||
<div id="text-pill1" class="text-overlay pill-text">
|
||||
<span class="pill-dot dot-green"></span>
|
||||
<span class="pill-label">Connected</span>
|
||||
</div>
|
||||
<div id="text-pill2" class="text-overlay pill-text">
|
||||
<span class="pill-dot dot-blue"></span>
|
||||
<span class="pill-label">Bluetooth</span>
|
||||
</div>
|
||||
<div id="text-pill3" class="text-overlay pill-text">
|
||||
<span class="pill-dot dot-violet"></span>
|
||||
<span class="pill-label">Focus</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="driver"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var W = 1920,
|
||||
H = 1080,
|
||||
DURATION = 8;
|
||||
|
||||
/* ── Three.js Aurora Shader ─────────────────────────────────────── */
|
||||
var vs = "varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 1.0); }";
|
||||
var fs = [
|
||||
"precision highp float;",
|
||||
"varying vec2 vUv;",
|
||||
"uniform float uTime;",
|
||||
"vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}",
|
||||
"vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}",
|
||||
"vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}",
|
||||
"vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}",
|
||||
"float snoise(vec3 v){",
|
||||
" const vec2 C=vec2(1.0/6.0,1.0/3.0);const vec4 D=vec4(0.0,0.5,1.0,2.0);",
|
||||
" vec3 i=floor(v+dot(v,C.yyy));vec3 x0=v-i+dot(i,C.xxx);",
|
||||
" vec3 g=step(x0.yzx,x0.xyz);vec3 l=1.0-g;",
|
||||
" vec3 i1=min(g.xyz,l.zxy);vec3 i2=max(g.xyz,l.zxy);",
|
||||
" vec3 x1=x0-i1+C.xxx;vec3 x2=x0-i2+C.yyy;vec3 x3=x0-D.yyy;",
|
||||
" i=mod289(i);",
|
||||
" vec4 p=permute(permute(permute(i.z+vec4(0.0,i1.z,i2.z,1.0))+i.y+vec4(0.0,i1.y,i2.y,1.0))+i.x+vec4(0.0,i1.x,i2.x,1.0));",
|
||||
" float n_=0.142857142857;vec3 ns=n_*D.wyz-D.xzx;",
|
||||
" vec4 j=p-49.0*floor(p*ns.z*ns.z);vec4 x_=floor(j*ns.z);vec4 y_=floor(j-7.0*x_);",
|
||||
" vec4 x=x_*ns.x+ns.yyyy;vec4 y=y_*ns.x+ns.yyyy;vec4 h=1.0-abs(x)-abs(y);",
|
||||
" vec4 b0=vec4(x.xy,y.xy);vec4 b1=vec4(x.zw,y.zw);",
|
||||
" vec4 s0=floor(b0)*2.0+1.0;vec4 s1=floor(b1)*2.0+1.0;",
|
||||
" vec4 sh=-step(h,vec4(0.0));",
|
||||
" vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;",
|
||||
" vec3 p0=vec3(a0.xy,h.x);vec3 p1=vec3(a0.zw,h.y);vec3 p2=vec3(a1.xy,h.z);vec3 p3=vec3(a1.zw,h.w);",
|
||||
" vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));",
|
||||
" p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;",
|
||||
" vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0);m=m*m;",
|
||||
" return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));",
|
||||
"}",
|
||||
"void main(){",
|
||||
" vec2 uv=vUv;float ar=1920.0/1080.0;vec2 p=vec2(uv.x*ar,uv.y);float t=uTime*0.06;",
|
||||
" float wx=snoise(vec3(p*1.4,t*0.5))*0.2;float wy=snoise(vec3(p*1.4+100.0,t*0.5))*0.2;",
|
||||
" vec2 wp=p+vec2(wx,wy);",
|
||||
" vec3 base=mix(vec3(0.10,0.02,0.22),vec3(0.04,0.10,0.25),smoothstep(0.0,1.0,uv.x*0.6+uv.y*0.4));",
|
||||
" float angle=0.65;float ridgeUv=dot(wp,vec2(cos(angle),sin(angle)));",
|
||||
" float ridge1=sin(ridgeUv*8.0+t*2.0+snoise(vec3(wp*0.8,t))*1.5);",
|
||||
" float ridge2=sin(ridgeUv*5.5-t*1.4+snoise(vec3(wp*1.2+50.0,t*0.7))*2.0);",
|
||||
" float ridge3=sin(ridgeUv*12.0+t*3.0+snoise(vec3(wp*0.6+80.0,t*0.4))*1.0);",
|
||||
" float crease1=pow(abs(ridge1),0.35)*sign(ridge1)*0.5+0.5;",
|
||||
" float crease2=pow(abs(ridge2),0.4)*sign(ridge2)*0.5+0.5;",
|
||||
" float crease3=pow(abs(ridge3),0.5)*sign(ridge3)*0.5+0.5;",
|
||||
" vec3 warm=mix(vec3(0.9,0.4,0.08),vec3(0.95,0.65,0.15),crease1);",
|
||||
" float warmZone=smoothstep(0.15,0.75,uv.x+uv.y*0.4+snoise(vec3(uv*2.0,t))*0.3);",
|
||||
" vec3 cool=mix(vec3(0.02,0.4,0.55),vec3(0.25,0.7,0.82),crease2);",
|
||||
" float coolZone=smoothstep(0.25,0.85,1.2-uv.x+uv.y*0.5+snoise(vec3(uv*2.0+40.0,t*0.8))*0.25);",
|
||||
" vec3 hot=mix(vec3(0.8,0.1,0.4),vec3(0.6,0.08,0.65),crease3);",
|
||||
" float hotZone=smoothstep(0.5,0.9,snoise(vec3(uv*2.5+20.0,t*0.6))*0.5+0.5);",
|
||||
" vec3 col=base;",
|
||||
" col=mix(col,warm,warmZone*0.75*smoothstep(0.3,0.7,crease1));",
|
||||
" col=mix(col,cool,coolZone*0.65*smoothstep(0.2,0.65,crease2));",
|
||||
" col=mix(col,hot,hotZone*0.5*smoothstep(0.35,0.8,crease3));",
|
||||
" float edgeGlow=pow(1.0-abs(ridge1),6.0)*0.4+pow(1.0-abs(ridge2),5.0)*0.25;",
|
||||
" col+=edgeGlow*mix(warm,cool,uv.x)*0.8;",
|
||||
" col*=1.0-pow(abs(ridge1),4.0)*0.15;",
|
||||
" float vig=1.0-smoothstep(0.5,1.5,length((uv-0.5)*vec2(1.3,1.0)));",
|
||||
" col*=0.65+vig*0.5;",
|
||||
" float luma=dot(col,vec3(0.299,0.587,0.114));col=mix(vec3(luma),col,1.5);",
|
||||
" col=clamp(col,0.0,1.0);col=pow(col,vec3(0.9));",
|
||||
" gl_FragColor=vec4(col,1.0);",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
/* ── Three.js Setup ─────────────────────────────────────────────── */
|
||||
var threeCanvas = document.getElementById("three-canvas");
|
||||
var renderer = new THREE.WebGLRenderer({ canvas: threeCanvas, alpha: false });
|
||||
renderer.setSize(W, H, false);
|
||||
renderer.setPixelRatio(1);
|
||||
|
||||
var scene = new THREE.Scene();
|
||||
var camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
var uniforms = { uTime: { value: 0 } };
|
||||
var shaderMat = new THREE.ShaderMaterial({
|
||||
vertexShader: vs,
|
||||
fragmentShader: fs,
|
||||
uniforms: uniforms,
|
||||
});
|
||||
var quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), shaderMat);
|
||||
scene.add(quad);
|
||||
|
||||
/* ── Glass Canvas ───────────────────────────────────────────────── */
|
||||
var glassCanvas = document.getElementById("glass-canvas");
|
||||
var ctx = glassCanvas.getContext("2d");
|
||||
var lg = new LiquidGlass.LiquidGlassCanvas(glassCanvas);
|
||||
var lgReady = false;
|
||||
|
||||
function renderGlass(time) {
|
||||
if (!lgReady) return;
|
||||
uniforms.uTime.value = time;
|
||||
renderer.render(scene, camera);
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
ctx.drawImage(threeCanvas, 0, 0, W, H);
|
||||
lg.renderGlassElements();
|
||||
}
|
||||
|
||||
function requestGlassRender(time) {
|
||||
if (glassCanvas.requestPaint) {
|
||||
glassCanvas.onpaint = function () {
|
||||
renderGlass(time);
|
||||
};
|
||||
glassCanvas.requestPaint();
|
||||
return;
|
||||
}
|
||||
renderGlass(time);
|
||||
}
|
||||
|
||||
lg.waitForInit().then(function (ok) {
|
||||
if (!ok) return;
|
||||
lgReady = true;
|
||||
requestGlassRender(0);
|
||||
});
|
||||
|
||||
/* ── GSAP Timeline ──────────────────────────────────────────────── */
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
/* Glass + text element pairs */
|
||||
var s1g = document.getElementById("glass-stat1");
|
||||
var s2g = document.getElementById("glass-stat2");
|
||||
var s3g = document.getElementById("glass-stat3");
|
||||
var scg = document.getElementById("glass-showcase");
|
||||
var p1g = document.getElementById("glass-pill1");
|
||||
var p2g = document.getElementById("glass-pill2");
|
||||
var p3g = document.getElementById("glass-pill3");
|
||||
var s1t = document.getElementById("text-stat1");
|
||||
var s2t = document.getElementById("text-stat2");
|
||||
var s3t = document.getElementById("text-stat3");
|
||||
var sct = document.getElementById("text-showcase");
|
||||
var p1t = document.getElementById("text-pill1");
|
||||
var p2t = document.getElementById("text-pill2");
|
||||
var p3t = document.getElementById("text-pill3");
|
||||
|
||||
var tempValue = document.getElementById("temp-value");
|
||||
var batteryValue = document.getElementById("battery-value");
|
||||
var stepsValue = document.getElementById("steps-value");
|
||||
var stepsRing = document.getElementById("steps-ring");
|
||||
var showcaseStatus = document.getElementById("showcase-status");
|
||||
var tempCounter = { value: 21 };
|
||||
var batteryCounter = { value: 87 };
|
||||
var stepsCounter = { value: 8.2 };
|
||||
|
||||
// Glass panels (rasterized by the html-in-canvas API) animate top — the API
|
||||
// reads sub-pixel getComputedStyle().top, so layout-prop motion does not
|
||||
// stutter and the lint rule exempts them. Plain-DOM text overlays DO stutter on
|
||||
// layout props, so they animate the transform equivalent (y). y is relative to
|
||||
// each text element's CSS resting top (stats 300, showcase 380, pills 720), so the
|
||||
// initial seed to visual 1160 is y = 1160 - restingTop.
|
||||
var glassPanels = [s1g, s2g, s3g, scg, p1g, p2g, p3g];
|
||||
var statText = [s1t, s2t, s3t];
|
||||
var showcaseText = sct;
|
||||
var pillText = [p1t, p2t, p3t];
|
||||
gsap.set(glassPanels, { opacity: 1, top: 1160 });
|
||||
gsap.set(statText, { opacity: 1, y: 860 }); // 1160 - 300
|
||||
gsap.set(showcaseText, { opacity: 1, y: 780 }); // 1160 - 380
|
||||
gsap.set(pillText, { opacity: 1, y: 440 }); // 1160 - 720
|
||||
|
||||
tl.to(s1g, { top: 300, duration: 0.46, ease: "power3.out" }, 0.12);
|
||||
tl.to(s1t, { y: 0, duration: 0.46, ease: "power3.out" }, 0.12); // 300 - 300
|
||||
tl.to(s2g, { top: 300, duration: 0.46, ease: "power3.out" }, 0.24);
|
||||
tl.to(s2t, { y: 0, duration: 0.46, ease: "power3.out" }, 0.24); // 300 - 300
|
||||
tl.to(s3g, { top: 300, duration: 0.46, ease: "power3.out" }, 0.36);
|
||||
tl.to(s3t, { y: 0, duration: 0.46, ease: "power3.out" }, 0.36); // 300 - 300
|
||||
tl.to(scg, { top: 380, duration: 0.5, ease: "power3.out" }, 0.5);
|
||||
tl.to(sct, { y: 0, duration: 0.5, ease: "power3.out" }, 0.5); // 380 - 380
|
||||
tl.to(p1g, { top: 720, duration: 0.32, ease: "power3.out" }, 0.9);
|
||||
tl.to(p1t, { y: 0, duration: 0.32, ease: "power3.out" }, 0.9); // 720 - 720
|
||||
tl.to(p2g, { top: 720, duration: 0.32, ease: "power3.out" }, 1.0);
|
||||
tl.to(p2t, { y: 0, duration: 0.32, ease: "power3.out" }, 1.0); // 720 - 720
|
||||
tl.to(p3g, { top: 720, duration: 0.32, ease: "power3.out" }, 1.1);
|
||||
tl.to(p3t, { y: 0, duration: 0.32, ease: "power3.out" }, 1.1); // 720 - 720
|
||||
|
||||
tl.to("#temp-meter", { width: "76%", duration: 1.7, ease: "power2.out" }, 0.9);
|
||||
tl.to(
|
||||
tempCounter,
|
||||
{
|
||||
value: 25,
|
||||
duration: 2.2,
|
||||
ease: "power2.out",
|
||||
onUpdate: function () {
|
||||
tempValue.textContent = Math.round(tempCounter.value);
|
||||
},
|
||||
},
|
||||
0.9,
|
||||
);
|
||||
tl.to("#battery-fill", { width: "91%", duration: 2.4, ease: "power2.out" }, 1.05);
|
||||
tl.to(
|
||||
batteryCounter,
|
||||
{
|
||||
value: 91,
|
||||
duration: 2.4,
|
||||
ease: "power2.out",
|
||||
onUpdate: function () {
|
||||
batteryValue.textContent = Math.round(batteryCounter.value);
|
||||
},
|
||||
},
|
||||
1.05,
|
||||
);
|
||||
tl.to(
|
||||
stepsCounter,
|
||||
{
|
||||
value: 9.1,
|
||||
duration: 2.6,
|
||||
ease: "power2.out",
|
||||
onUpdate: function () {
|
||||
var degrees = Math.round(stepsCounter.value * 31.4);
|
||||
stepsValue.textContent = stepsCounter.value.toFixed(1) + "K";
|
||||
stepsRing.style.background =
|
||||
"conic-gradient(#a78bfa 0deg, #06e3fa " +
|
||||
degrees +
|
||||
"deg, rgba(255, 255, 255, 0.18) " +
|
||||
degrees +
|
||||
"deg)";
|
||||
},
|
||||
},
|
||||
1.2,
|
||||
);
|
||||
tl.to("#showcase-progress", { width: "92%", duration: 4.1, ease: "none" }, 1.2);
|
||||
tl.to(
|
||||
"#showcase-dot",
|
||||
{ scale: 1.5, duration: 0.25, ease: "power2.out", yoyo: true, repeat: 7 },
|
||||
1.4,
|
||||
);
|
||||
tl.call(
|
||||
function () {
|
||||
showcaseStatus.textContent = "Preview ready";
|
||||
},
|
||||
null,
|
||||
5.2,
|
||||
);
|
||||
tl.to(scg, { top: "-=10", duration: 0.2, ease: "power2.out", yoyo: true, repeat: 1 }, 5.2);
|
||||
tl.to(sct, { y: "-=10", duration: 0.2, ease: "power2.out", yoyo: true, repeat: 1 }, 5.2);
|
||||
tl.to(
|
||||
".pill-dot",
|
||||
{ scale: 1.55, duration: 0.2, ease: "power2.out", stagger: 0.12, yoyo: true, repeat: 3 },
|
||||
2.2,
|
||||
);
|
||||
|
||||
tl.to(
|
||||
[s1g, s2g, s3g],
|
||||
{ top: "-=12", duration: 0.2, ease: "power2.out", yoyo: true, repeat: 1 },
|
||||
5.7,
|
||||
);
|
||||
tl.to(
|
||||
[s1t, s2t, s3t],
|
||||
{ y: "-=12", duration: 0.2, ease: "power2.out", yoyo: true, repeat: 1 },
|
||||
5.7,
|
||||
);
|
||||
tl.to(
|
||||
[s1g, s2g, s3g, scg, p1g, p2g, p3g],
|
||||
{ top: "+=760", duration: 0.42, ease: "power2.in" },
|
||||
6.74,
|
||||
);
|
||||
tl.to(
|
||||
[s1t, s2t, s3t, sct, p1t, p2t, p3t],
|
||||
{ y: "+=760", duration: 0.42, ease: "power2.in" },
|
||||
6.74,
|
||||
);
|
||||
|
||||
/* Duration driver */
|
||||
tl.to({ v: 0 }, { v: 1, duration: DURATION, ease: "none" }, 0);
|
||||
|
||||
tl.eventCallback("onUpdate", function () {
|
||||
requestGlassRender(tl.time());
|
||||
});
|
||||
|
||||
window.__timelines["liquid-glass-widgets"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="liquid-glass-widgets" data-composition-src="compositions/liquid-glass-widgets.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,35 +3,16 @@ title: "Logo Outro"
|
||||
description: "Cinematic logo reveal with piece-by-piece assembly, glow bloom, tagline fade-in, and URL pill"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/logo-outro.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/logo-outro.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/logo-outro.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Logo Outro preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add logo-outro
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add logo-outro" />
|
||||
|
||||
That writes one file: `compositions/logo-outro.html`.
|
||||
|
||||
## Add it to your video
|
||||
|
||||
It runs for 6 seconds at 1920×1080. Paste this into your composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="logo-outro"
|
||||
data-composition-src="compositions/logo-outro.html"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Change the colors
|
||||
|
||||
Set these CSS variables on the block:
|
||||
@@ -39,6 +20,249 @@ Set these CSS variables on the block:
|
||||
- `--bg-color` — Background. Defaults to `#0a0a0f`.
|
||||
- `--accent-color` — Accent. Defaults to `#1a1a1f`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`logo-outro.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Logo Outro</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=block"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<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 {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #0a0a0f;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="logo-outro"
|
||||
data-composition-id="logo-outro"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
>
|
||||
<div class="canvas">
|
||||
<!-- Glow effect behind the logo -->
|
||||
<div class="logo-glow"></div>
|
||||
|
||||
<!-- Logo Container -->
|
||||
<div class="logo-container">
|
||||
<svg viewBox="0 0 160 240" width="160" height="240" class="logo-svg">
|
||||
<!-- Replace these paths with your own logo SVG -->
|
||||
<path
|
||||
class="logo-piece logo-tl"
|
||||
d="M 0 40 C 0 17.9 17.9 0 40 0 L 80 0 L 80 80 L 40 80 C 17.9 80 0 62.1 0 40 Z"
|
||||
fill="#F24E1E"
|
||||
/>
|
||||
<path
|
||||
class="logo-piece logo-tr"
|
||||
d="M 80 0 L 120 0 C 142.1 0 160 17.9 160 40 C 160 62.1 142.1 80 120 80 L 80 80 L 80 0 Z"
|
||||
fill="#A259FF"
|
||||
/>
|
||||
<path
|
||||
class="logo-piece logo-ml"
|
||||
d="M 0 120 C 0 97.9 17.9 80 40 80 L 80 80 L 80 160 L 40 160 C 17.9 160 0 142.1 0 120 Z"
|
||||
fill="#FF7262"
|
||||
/>
|
||||
<circle class="logo-piece logo-mr" cx="120" cy="120" r="40" fill="#1ABCFE" />
|
||||
<path
|
||||
class="logo-piece logo-bl"
|
||||
d="M 0 200 C 0 177.9 17.9 160 40 160 L 80 160 L 80 200 C 80 222.1 62.1 240 40 240 C 17.9 240 0 222.1 0 200 Z"
|
||||
fill="#0ACF83"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="text-container">
|
||||
<div class="tagline">Nothing great is made alone.</div>
|
||||
<div class="url-pill">
|
||||
<span class="url-text">figma.com</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
[data-composition-id="logo-outro"] .canvas {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: #0a0a0f;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
[data-composition-id="logo-outro"] .logo-container {
|
||||
position: absolute;
|
||||
left: 880px;
|
||||
top: 380px;
|
||||
width: 160px;
|
||||
height: 240px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
[data-composition-id="logo-outro"] .logo-piece {
|
||||
transform-origin: center center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-composition-id="logo-outro"] .logo-glow {
|
||||
position: absolute;
|
||||
left: 760px;
|
||||
top: 300px;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: radial-gradient(circle, rgba(162, 89, 255, 0.25) 0%, rgba(10, 10, 15, 0) 70%);
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
[data-composition-id="logo-outro"] .text-container {
|
||||
position: absolute;
|
||||
top: 720px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
[data-composition-id="logo-outro"] .tagline {
|
||||
font-size: 44px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
opacity: 0;
|
||||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-composition-id="logo-outro"] .url-pill {
|
||||
background: #1a1a1f;
|
||||
border: 1px solid #2c2c2c;
|
||||
border-radius: 100px;
|
||||
padding: 8px 24px;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-composition-id="logo-outro"] .url-text {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const pieces = {
|
||||
tl: '[data-composition-id="logo-outro"] .logo-tl',
|
||||
tr: '[data-composition-id="logo-outro"] .logo-tr',
|
||||
ml: '[data-composition-id="logo-outro"] .logo-ml',
|
||||
mr: '[data-composition-id="logo-outro"] .logo-mr',
|
||||
bl: '[data-composition-id="logo-outro"] .logo-bl',
|
||||
};
|
||||
|
||||
const pieceArray = [pieces.tl, pieces.tr, pieces.ml, pieces.mr, pieces.bl];
|
||||
const logoGlow = '[data-composition-id="logo-outro"] .logo-glow';
|
||||
const tagline = '[data-composition-id="logo-outro"] .tagline';
|
||||
const urlPill = '[data-composition-id="logo-outro"] .url-pill';
|
||||
|
||||
// 1. Pieces slide in from the right with ripple stagger
|
||||
pieceArray.forEach((piece, i) => {
|
||||
tl.fromTo(
|
||||
piece,
|
||||
{ x: 1920, opacity: 0 },
|
||||
{ x: 0, opacity: 1, duration: 1.2, ease: "power4.out" },
|
||||
i * 0.08,
|
||||
);
|
||||
});
|
||||
|
||||
// 2. Expand and close
|
||||
const expandTime = 1.2;
|
||||
|
||||
tl.to(
|
||||
pieceArray,
|
||||
{ scale: 1.15, duration: 0.5, ease: "back.out(1.7)", stagger: 0.02 },
|
||||
expandTime,
|
||||
);
|
||||
|
||||
tl.to(pieceArray, { scale: 1, duration: 0.4, ease: "power2.inOut" }, expandTime + 0.3);
|
||||
|
||||
// 3. Glow bloom
|
||||
tl.fromTo(
|
||||
logoGlow,
|
||||
{ opacity: 0, scale: 0.5 },
|
||||
{ opacity: 1, scale: 1.2, duration: 0.4, ease: "power2.out" },
|
||||
expandTime,
|
||||
);
|
||||
tl.to(
|
||||
logoGlow,
|
||||
{ opacity: 0, scale: 1.8, duration: 1.2, ease: "power2.out" },
|
||||
expandTime + 0.4,
|
||||
);
|
||||
|
||||
// 4. Tagline and URL pill fade in
|
||||
const textStartTime = expandTime + 0.8;
|
||||
tl.fromTo(
|
||||
tagline,
|
||||
{ opacity: 0, y: 20 },
|
||||
{ opacity: 1, y: 0, duration: 1, ease: "power3.out" },
|
||||
textStartTime,
|
||||
);
|
||||
|
||||
tl.fromTo(
|
||||
urlPill,
|
||||
{ opacity: 0, y: 15 },
|
||||
{ opacity: 1, y: 0, duration: 0.8, ease: "power3.out" },
|
||||
textStartTime + 0.4,
|
||||
);
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["logo-outro"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="logo-outro" data-composition-src="compositions/logo-outro.html" data-start="0" data-duration="6" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `branding` `outro` `logo`.
|
||||
|
||||
@@ -3,35 +3,186 @@ title: "Lower Third — BILD Style"
|
||||
description: "News-style lower third with tight-fit text boxes: white headline bar with red drop-shadow, red sub-line with white drop-shadow."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lower-third-bild.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lower-third-bild.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
|
||||
|
||||
<VariablesExplorer
|
||||
previewSrc="/public/catalog/preview/blocks/lower-third-bild.html"
|
||||
compositionId="lower-third-bild"
|
||||
compositionSrc="compositions/lower-third-bild.html"
|
||||
variables={[{"id":"TXT_MAIN_1_Line","type":"string","default":"BILD EXKLUSIV"},{"id":"TXT_SUB","type":"string","default":"Sonderbericht"}]}
|
||||
>
|
||||
|
||||
```html lower-third-bild.html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Barlow Condensed";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: block;
|
||||
src: url("assets/fonts/BarlowCondensed-Bold.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Barlow Condensed";
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: block;
|
||||
src: url("assets/fonts/BarlowCondensed-ExtraBold.woff2") format("woff2");
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
|
||||
#lb-root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#lb-main-outer {
|
||||
position: absolute;
|
||||
bottom: 215px;
|
||||
left: 80px;
|
||||
padding-right: 14px;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
#lb-main-text {
|
||||
font-family: "Barlow Condensed", sans-serif;
|
||||
font-size: 88px;
|
||||
font-weight: 800;
|
||||
line-height: 1.05;
|
||||
color: #212529;
|
||||
background: #ffffff;
|
||||
padding: 10px 22px 8px;
|
||||
white-space: nowrap;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
box-shadow: 12px 12px 0 0 #dd0000;
|
||||
}
|
||||
|
||||
#lb-sub-outer {
|
||||
position: absolute;
|
||||
bottom: 135px;
|
||||
left: 80px;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
#lb-sub-text {
|
||||
font-family: "Barlow Condensed", sans-serif;
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
color: #ffffff;
|
||||
background: #dd0000;
|
||||
padding: 6px 18px 5px;
|
||||
white-space: nowrap;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
box-shadow: 8px 8px 0 0 #ffffff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="lb-root"
|
||||
data-composition-id="lower-third-bild"
|
||||
data-composition-variables='{"TXT_MAIN_1_Line":{"type":"string","default":"BILD EXKLUSIV"},"TXT_SUB":{"type":"string","default":"Sonderbericht"}}'
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div
|
||||
id="lb-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
|
||||
<div id="lb-main-outer">
|
||||
<div id="lb-main-text"></div>
|
||||
</div>
|
||||
|
||||
<div id="lb-sub-outer">
|
||||
<div id="lb-sub-text"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var vars =
|
||||
window.__hyperframes && window.__hyperframes.getVariables
|
||||
? window.__hyperframes.getVariables()
|
||||
: {};
|
||||
|
||||
var mainText = (vars.TXT_MAIN_1_Line || "BILD EXKLUSIV").trim();
|
||||
var subText = (vars.TXT_SUB || "Sonderbericht").trim();
|
||||
|
||||
var dur = parseFloat(document.getElementById("lb-drv").dataset.duration) || 8;
|
||||
|
||||
document.getElementById("lb-main-text").textContent = mainText;
|
||||
document.getElementById("lb-sub-text").textContent = subText || " ";
|
||||
|
||||
var mainOuter = document.getElementById("lb-main-outer");
|
||||
var subOuter = document.getElementById("lb-sub-outer");
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
gsap.set([mainOuter, subOuter], { opacity: 1 });
|
||||
|
||||
tl.set([mainOuter, subOuter], { opacity: 0, visibility: "hidden" }, dur);
|
||||
|
||||
window.__timelines["lower-third-bild"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</VariablesExplorer>
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add lower-third-bild
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add lower-third-bild" />
|
||||
|
||||
That writes `compositions/lower-third-bild.html`, plus 2 supporting files under `compositions/assets/fonts/`.
|
||||
|
||||
## Add it to your video
|
||||
## Usage
|
||||
|
||||
It runs for 8 seconds at 1920×1080. Paste this into your composition:
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="lower-third-bild"
|
||||
data-composition-src="compositions/lower-third-bild.html"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```html
|
||||
<div data-composition-id="lower-third-bild" data-composition-src="compositions/lower-third-bild.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `broadcast` `lower-third` `news` `overlay`.
|
||||
|
||||
@@ -3,34 +3,138 @@ title: "Lower Third — Accent Underline"
|
||||
description: "Cardless lower third for footage overlay: name rises, an accent rule draws left-to-right, role fades in; text-shadowed for legibility"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-accent-underline.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-accent-underline.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/lt-accent-underline.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Accent Underline preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add lt-accent-underline
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add lt-accent-underline" />
|
||||
|
||||
That writes one file: `compositions/lt-accent-underline.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`lt-accent-underline.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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 {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
|
||||
#root .lt {
|
||||
position: absolute;
|
||||
left: 130px;
|
||||
bottom: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
#root .lt-name {
|
||||
font-family: "Oswald", sans-serif;
|
||||
font-size: 76px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 0.96;
|
||||
letter-spacing: 0.005em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 2px 22px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
#root .lt-rule {
|
||||
height: 6px;
|
||||
width: 100%;
|
||||
background: #46e5b7;
|
||||
border-radius: 3px;
|
||||
transform-origin: 0% 50%;
|
||||
}
|
||||
#root .lt-role {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
color: #e7eaf0;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 2px 16px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="lt-accent-underline"
|
||||
data-composition-src="compositions/lt-accent-underline.html"
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
>
|
||||
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
|
||||
<div id="lt" class="lt">
|
||||
<div id="lt-name" class="lt-name">Dr. Maya Chen</div>
|
||||
<div id="lt-rule" class="lt-rule"></div>
|
||||
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var wrap = document.getElementById("lt");
|
||||
var nameEl = document.getElementById("lt-name");
|
||||
var rule = document.getElementById("lt-rule");
|
||||
var role = document.getElementById("lt-role");
|
||||
|
||||
// Name slides up, accent rule draws left→right, role fades up under it.
|
||||
gsap.set(nameEl, { y: 28, opacity: 0 });
|
||||
gsap.set(rule, { scaleX: 0 });
|
||||
gsap.set(role, { y: 16, opacity: 0 });
|
||||
|
||||
tl.to(nameEl, { y: 0, opacity: 1, duration: 0.55, ease: "power3.out" }, 0.1);
|
||||
tl.to(rule, { scaleX: 1, duration: 0.5, ease: "power4.out" }, 0.3);
|
||||
tl.to(role, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.46);
|
||||
|
||||
// Exit: role fades, rule retracts, name lifts, then hard kill.
|
||||
tl.to(role, { opacity: 0, duration: 0.3, ease: "power2.in" }, 4.25);
|
||||
tl.to(rule, { scaleX: 0, duration: 0.3, ease: "power2.in" }, 4.3);
|
||||
tl.to(nameEl, { y: -16, opacity: 0, duration: 0.32, ease: "power2.in" }, 4.35);
|
||||
tl.set(wrap, { visibility: "hidden" }, 4.75);
|
||||
|
||||
window.__timelines["lt-accent-underline"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="lt-accent-underline" data-composition-src="compositions/lt-accent-underline.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,140 @@ title: "Lower Third — Bold Block"
|
||||
description: "High-energy podcast lower third: solid dark block wipes in, uppercase name slams up, accent tag pops"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-bold-block.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-bold-block.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/lt-bold-block.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Bold Block preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add lt-bold-block
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add lt-bold-block" />
|
||||
|
||||
That writes one file: `compositions/lt-bold-block.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`lt-bold-block.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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 {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
|
||||
#root .lt {
|
||||
position: absolute;
|
||||
left: 120px;
|
||||
bottom: 116px;
|
||||
}
|
||||
#root .lt-block {
|
||||
background: #141518;
|
||||
padding: 18px 34px 22px 30px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
will-change: clip-path;
|
||||
}
|
||||
#root .lt-name {
|
||||
font-family: "Archivo Black", sans-serif;
|
||||
font-size: 58px;
|
||||
font-weight: 400;
|
||||
color: #ffffff;
|
||||
line-height: 0.98;
|
||||
letter-spacing: -0.015em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#root .lt-tagwrap {
|
||||
overflow: hidden;
|
||||
}
|
||||
#root .lt-tag {
|
||||
display: inline-block;
|
||||
background: #ffd23f;
|
||||
color: #141518;
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
padding: 7px 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="lt-bold-block"
|
||||
data-composition-src="compositions/lt-bold-block.html"
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
>
|
||||
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
|
||||
<div id="lt" class="lt">
|
||||
<div id="lt-block" class="lt-block">
|
||||
<div id="lt-name" class="lt-name">Maya Chen</div>
|
||||
<div class="lt-tagwrap">
|
||||
<span id="lt-tag" class="lt-tag">Host · Neuroscientist</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var wrap = document.getElementById("lt");
|
||||
var block = document.getElementById("lt-block");
|
||||
var nameEl = document.getElementById("lt-name");
|
||||
var tag = document.getElementById("lt-tag");
|
||||
|
||||
// Solid block wipes in from the left, name slams up inside it, tag pops up after.
|
||||
gsap.set(block, { clipPath: "inset(0 100% 0 0)" });
|
||||
gsap.set(nameEl, { y: 30, opacity: 0 });
|
||||
gsap.set(tag, { y: 40 });
|
||||
|
||||
tl.to(block, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "power4.out" }, 0.1);
|
||||
tl.to(nameEl, { y: 0, opacity: 1, duration: 0.42, ease: "back.out(1.6)" }, 0.26);
|
||||
tl.to(tag, { y: 0, duration: 0.45, ease: "back.out(2)" }, 0.5);
|
||||
|
||||
// Exit: block wipes out to the left, then hard kill.
|
||||
tl.to(block, { clipPath: "inset(0 0 0 100%)", duration: 0.4, ease: "power3.in" }, 4.3);
|
||||
tl.set(wrap, { visibility: "hidden" }, 4.75);
|
||||
|
||||
window.__timelines["lt-bold-block"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="lt-bold-block" data-composition-src="compositions/lt-bold-block.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,143 @@ title: "Lower Third — Clean Bar"
|
||||
description: "Minimal white-card lower third for podcasts/interviews: accent tab, name + role, clip-wipe entrance"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-clean-bar.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-clean-bar.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/lt-clean-bar.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Clean Bar preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add lt-clean-bar
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add lt-clean-bar" />
|
||||
|
||||
That writes one file: `compositions/lt-clean-bar.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`lt-clean-bar.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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 {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
|
||||
#root .lt {
|
||||
position: absolute;
|
||||
left: 120px;
|
||||
bottom: 110px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 14px 44px rgba(15, 17, 21, 0.18);
|
||||
will-change: clip-path;
|
||||
}
|
||||
#root .lt-tab {
|
||||
width: 12px;
|
||||
background: #ff5a36;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#root .lt-body {
|
||||
background: #ffffff;
|
||||
padding: 22px 40px 24px 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
}
|
||||
#root .lt-name {
|
||||
font-size: 52px;
|
||||
font-weight: 700;
|
||||
color: #0f1115;
|
||||
line-height: 1.06;
|
||||
letter-spacing: -0.015em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#root .lt-role {
|
||||
font-size: 27px;
|
||||
font-weight: 400;
|
||||
color: #5a6170;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.01em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="lt-clean-bar"
|
||||
data-composition-src="compositions/lt-clean-bar.html"
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
>
|
||||
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
|
||||
<div id="lt" class="lt">
|
||||
<div id="lt-tab" class="lt-tab"></div>
|
||||
<div class="lt-body">
|
||||
<div id="lt-name" class="lt-name">Dr. Maya Chen</div>
|
||||
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var card = document.getElementById("lt");
|
||||
var tab = document.getElementById("lt-tab");
|
||||
var nameEl = document.getElementById("lt-name");
|
||||
var role = document.getElementById("lt-role");
|
||||
|
||||
// Card wipes in from the left (clip-path), accent tab grows, text rises with a small stagger.
|
||||
gsap.set(card, { clipPath: "inset(0 100% 0 0)" });
|
||||
gsap.set(tab, { scaleY: 0, transformOrigin: "50% 0%" });
|
||||
gsap.set(nameEl, { y: 22, opacity: 0 });
|
||||
gsap.set(role, { y: 22, opacity: 0 });
|
||||
|
||||
tl.to(card, { clipPath: "inset(0 0% 0 0)", duration: 0.55, ease: "power3.out" }, 0.1);
|
||||
tl.to(tab, { scaleY: 1, duration: 0.45, ease: "power2.out" }, 0.28);
|
||||
tl.to(nameEl, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.34);
|
||||
tl.to(role, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.44);
|
||||
|
||||
// Exit: settle out, then hard kill the inner element.
|
||||
tl.to(card, { y: 18, opacity: 0, duration: 0.35, ease: "power2.in" }, 4.3);
|
||||
tl.set(card, { visibility: "hidden" }, 4.75);
|
||||
|
||||
window.__timelines["lt-clean-bar"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="lt-clean-bar" data-composition-src="compositions/lt-clean-bar.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,132 @@ title: "Lower Third — Color Block"
|
||||
description: "High-energy lower third: bold accent-color block slides in with overshoot, condensed name + mono role"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-color-block.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-color-block.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/lt-color-block.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Color Block preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add lt-color-block
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add lt-color-block" />
|
||||
|
||||
That writes one file: `compositions/lt-color-block.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`lt-color-block.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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 {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
|
||||
#root .lt {
|
||||
position: absolute;
|
||||
left: 120px;
|
||||
bottom: 116px;
|
||||
}
|
||||
#root .lt-block {
|
||||
background: #2756ff;
|
||||
padding: 20px 36px 24px 32px;
|
||||
box-shadow: 0 16px 44px rgba(39, 86, 255, 0.34);
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
#root .lt-name {
|
||||
font-family: "League Gothic", sans-serif;
|
||||
font-size: 84px;
|
||||
font-weight: 400;
|
||||
color: #ffffff;
|
||||
line-height: 0.86;
|
||||
letter-spacing: 0.01em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#root .lt-role {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.86);
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="lt-color-block"
|
||||
data-composition-src="compositions/lt-color-block.html"
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
>
|
||||
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
|
||||
<div id="lt" class="lt">
|
||||
<div id="lt-block" class="lt-block">
|
||||
<div id="lt-name" class="lt-name">Dr. Maya Chen</div>
|
||||
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var wrap = document.getElementById("lt");
|
||||
var block = document.getElementById("lt-block");
|
||||
var nameEl = document.getElementById("lt-name");
|
||||
var role = document.getElementById("lt-role");
|
||||
|
||||
// Color block slides in from the left and overshoots slightly; name and role rise inside it.
|
||||
gsap.set(block, { x: -80, opacity: 0 });
|
||||
gsap.set(nameEl, { y: 24, opacity: 0 });
|
||||
gsap.set(role, { y: 18, opacity: 0 });
|
||||
|
||||
tl.to(block, { x: 0, opacity: 1, duration: 0.5, ease: "back.out(1.4)" }, 0.1);
|
||||
tl.to(nameEl, { y: 0, opacity: 1, duration: 0.42, ease: "power3.out" }, 0.3);
|
||||
tl.to(role, { y: 0, opacity: 1, duration: 0.42, ease: "power3.out" }, 0.4);
|
||||
|
||||
// Exit: slide out left, hard kill.
|
||||
tl.to(block, { x: -60, opacity: 0, duration: 0.35, ease: "power2.in" }, 4.3);
|
||||
tl.set(wrap, { visibility: "hidden" }, 4.75);
|
||||
|
||||
window.__timelines["lt-color-block"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="lt-color-block" data-composition-src="compositions/lt-color-block.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,142 @@ title: "Lower Third — Dark Card"
|
||||
description: "Charcoal card lower third for bright footage: name, drawn accent underline, role; slide-up entrance"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-dark-card.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-dark-card.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/lt-dark-card.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Dark Card preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add lt-dark-card
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add lt-dark-card" />
|
||||
|
||||
That writes one file: `compositions/lt-dark-card.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`lt-dark-card.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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 {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
|
||||
#root .lt {
|
||||
position: absolute;
|
||||
left: 120px;
|
||||
bottom: 110px;
|
||||
}
|
||||
#root .lt-card {
|
||||
background: #16181d;
|
||||
border-radius: 14px;
|
||||
padding: 24px 38px 26px 32px;
|
||||
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
#root .lt-name {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.015em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#root .lt-underline {
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
background: #f5b942;
|
||||
border-radius: 2px;
|
||||
transform-origin: 0% 50%;
|
||||
}
|
||||
#root .lt-role {
|
||||
font-size: 25px;
|
||||
font-weight: 400;
|
||||
color: #aeb6c2;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="lt-dark-card"
|
||||
data-composition-src="compositions/lt-dark-card.html"
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
>
|
||||
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
|
||||
<div id="lt" class="lt">
|
||||
<div id="lt-card" class="lt-card">
|
||||
<div id="lt-name" class="lt-name">Dr. Maya Chen</div>
|
||||
<div id="lt-underline" class="lt-underline"></div>
|
||||
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var wrap = document.getElementById("lt");
|
||||
var card = document.getElementById("lt-card");
|
||||
var nameEl = document.getElementById("lt-name");
|
||||
var underline = document.getElementById("lt-underline");
|
||||
var role = document.getElementById("lt-role");
|
||||
|
||||
// Card slides up from below, name fades, accent underline draws, role fades.
|
||||
gsap.set(card, { y: 60, opacity: 0 });
|
||||
gsap.set(nameEl, { y: 14, opacity: 0 });
|
||||
gsap.set(underline, { scaleX: 0 });
|
||||
gsap.set(role, { opacity: 0 });
|
||||
|
||||
tl.to(card, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.1);
|
||||
tl.to(nameEl, { y: 0, opacity: 1, duration: 0.45, ease: "power3.out" }, 0.26);
|
||||
tl.to(underline, { scaleX: 1, duration: 0.5, ease: "power4.out" }, 0.42);
|
||||
tl.to(role, { opacity: 1, duration: 0.45, ease: "power2.out" }, 0.56);
|
||||
|
||||
// Exit: slide down + hard kill.
|
||||
tl.to(card, { y: 24, opacity: 0, duration: 0.35, ease: "power2.in" }, 4.3);
|
||||
tl.set(wrap, { visibility: "hidden" }, 4.75);
|
||||
|
||||
window.__timelines["lt-dark-card"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="lt-dark-card" data-composition-src="compositions/lt-dark-card.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,145 @@ title: "Lower Third — Kicker Name"
|
||||
description: "Cardless lower third with an accent eyebrow/kicker tag, heavy name, and a drawn baseline; for footage"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-kicker-name.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-kicker-name.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/lt-kicker-name.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Kicker Name preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add lt-kicker-name
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add lt-kicker-name" />
|
||||
|
||||
That writes one file: `compositions/lt-kicker-name.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`lt-kicker-name.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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 {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
|
||||
#root .lt {
|
||||
position: absolute;
|
||||
left: 130px;
|
||||
bottom: 122px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
#root .lt-kickwrap {
|
||||
overflow: hidden;
|
||||
}
|
||||
#root .lt-kicker {
|
||||
display: inline-block;
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #141518;
|
||||
background: #46e5b7;
|
||||
padding: 6px 14px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#root .lt-name {
|
||||
font-family: "Archivo Black", sans-serif;
|
||||
font-size: 70px;
|
||||
font-weight: 400;
|
||||
color: #ffffff;
|
||||
line-height: 0.98;
|
||||
letter-spacing: -0.015em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 2px 22px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
#root .lt-baseline {
|
||||
height: 5px;
|
||||
width: 100%;
|
||||
background: #46e5b7;
|
||||
border-radius: 3px;
|
||||
transform-origin: 0% 50%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="lt-kicker-name"
|
||||
data-composition-src="compositions/lt-kicker-name.html"
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
>
|
||||
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
|
||||
<div id="lt" class="lt">
|
||||
<div class="lt-kickwrap">
|
||||
<span id="lt-kicker" class="lt-kicker">Episode 47</span>
|
||||
</div>
|
||||
<div id="lt-name" class="lt-name">Maya Chen</div>
|
||||
<div id="lt-baseline" class="lt-baseline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var wrap = document.getElementById("lt");
|
||||
var kicker = document.getElementById("lt-kicker");
|
||||
var nameEl = document.getElementById("lt-name");
|
||||
var baseline = document.getElementById("lt-baseline");
|
||||
|
||||
// Kicker drops in, name slams up, accent baseline draws.
|
||||
gsap.set(kicker, { y: -40 });
|
||||
gsap.set(nameEl, { y: 34, opacity: 0 });
|
||||
gsap.set(baseline, { scaleX: 0 });
|
||||
|
||||
tl.to(kicker, { y: 0, duration: 0.42, ease: "back.out(2)" }, 0.1);
|
||||
tl.to(nameEl, { y: 0, opacity: 1, duration: 0.45, ease: "back.out(1.5)" }, 0.32);
|
||||
tl.to(baseline, { scaleX: 1, duration: 0.5, ease: "power4.out" }, 0.5);
|
||||
|
||||
// Exit: baseline retracts, text lifts, hard kill.
|
||||
tl.to(baseline, { scaleX: 0, duration: 0.3, ease: "power2.in" }, 4.25);
|
||||
tl.to(nameEl, { y: -16, opacity: 0, duration: 0.32, ease: "power2.in" }, 4.3);
|
||||
tl.to(kicker, { y: -40, duration: 0.3, ease: "power2.in" }, 4.32);
|
||||
tl.set(wrap, { visibility: "hidden" }, 4.75);
|
||||
|
||||
window.__timelines["lt-kicker-name"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="lt-kicker-name" data-composition-src="compositions/lt-kicker-name.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,151 @@ title: "Lower Third — Mask Reveal"
|
||||
description: "Cardless lower third: an accent sweep crosses and clip-path-reveals a heavy name, role fades up; for footage"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-mask-reveal.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-mask-reveal.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/lt-mask-reveal.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Mask Reveal preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add lt-mask-reveal
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add lt-mask-reveal" />
|
||||
|
||||
That writes one file: `compositions/lt-mask-reveal.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`lt-mask-reveal.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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 {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
|
||||
#root .lt {
|
||||
position: absolute;
|
||||
left: 130px;
|
||||
bottom: 122px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
#root .lt-namewrap {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 2px 0;
|
||||
}
|
||||
#root .lt-name {
|
||||
font-size: 72px;
|
||||
font-weight: 900;
|
||||
color: #ffffff;
|
||||
line-height: 0.98;
|
||||
letter-spacing: -0.02em;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 2px 22px rgba(0, 0, 0, 0.45);
|
||||
will-change: clip-path;
|
||||
}
|
||||
#root .lt-sweep {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 8px;
|
||||
background: #ffd23f;
|
||||
}
|
||||
#root .lt-role {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: #e7eaf0;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 2px 16px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="lt-mask-reveal"
|
||||
data-composition-src="compositions/lt-mask-reveal.html"
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
>
|
||||
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
|
||||
<div id="lt" class="lt">
|
||||
<div class="lt-namewrap">
|
||||
<div id="lt-name" class="lt-name">Dr. Maya Chen</div>
|
||||
<div id="lt-sweep" class="lt-sweep"></div>
|
||||
</div>
|
||||
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var wrap = document.getElementById("lt");
|
||||
var nameEl = document.getElementById("lt-name");
|
||||
var sweep = document.getElementById("lt-sweep");
|
||||
var role = document.getElementById("lt-role");
|
||||
|
||||
// An accent sweep crosses left→right; the name is revealed by a clip-path wipe behind it; role fades up.
|
||||
// Sweep travel: left:"100%" was parent-relative (offsetParent = .lt-namewrap). Since the sweep is
|
||||
// only 8px wide, xPercent would travel just 8px, so translate by the container width in pixels instead.
|
||||
// The .lt-namewrap shrink-wraps the name "Dr. Maya Chen" to 536px (no horizontal padding/border, so
|
||||
// clientWidth == content width), which is exactly what left:"100%" resolved to.
|
||||
var SWEEP_TRAVEL = 536;
|
||||
gsap.set(nameEl, { clipPath: "inset(0 100% 0 0)" });
|
||||
gsap.set(sweep, { x: 0, opacity: 0 });
|
||||
gsap.set(role, { y: 14, opacity: 0 });
|
||||
|
||||
tl.to(sweep, { opacity: 1, duration: 0.12, ease: "none" }, 0.1);
|
||||
tl.to(sweep, { x: SWEEP_TRAVEL, duration: 0.55, ease: "power2.inOut" }, 0.12);
|
||||
tl.to(nameEl, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "power2.inOut" }, 0.16);
|
||||
tl.to(sweep, { opacity: 0, duration: 0.15, ease: "none" }, 0.6);
|
||||
tl.to(role, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.55);
|
||||
|
||||
// Exit: name un-wipes, role fades, hard kill.
|
||||
tl.to(role, { opacity: 0, duration: 0.3, ease: "power2.in" }, 4.25);
|
||||
tl.to(nameEl, { clipPath: "inset(0 0 0 100%)", duration: 0.4, ease: "power2.in" }, 4.3);
|
||||
tl.set(wrap, { visibility: "hidden" }, 4.75);
|
||||
|
||||
window.__timelines["lt-mask-reveal"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="lt-mask-reveal" data-composition-src="compositions/lt-mask-reveal.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
---
|
||||
title: "Neon Border"
|
||||
description: "Two arcs of light travel the edge of a rounded panel and snap corner to corner, trailing a soft three-layer bloom. A lower third for a name and role."
|
||||
---
|
||||
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
|
||||
|
||||
<VariablesExplorer
|
||||
previewSrc="/public/catalog/preview/blocks/lt-neon-border.html"
|
||||
compositionId="lt-neon-border"
|
||||
compositionSrc="compositions/lt-neon-border.html"
|
||||
variables={[{"id":"name","type":"string","label":"Name","default":"Dr. Maya Chen"},{"id":"role","type":"string","label":"Role","default":"Host · Neuroscientist"},{"id":"accent","type":"color","label":"Accent","default":"#cc9149"},{"id":"movement","type":"enum","label":"Movement","default":"step","options":[{"value":"step","label":"Step (snap at corners)"},{"value":"glide","label":"Glide (smooth)"}]},{"id":"speed","type":"number","label":"Speed","default":1.6,"min":0.2,"max":6,"step":0.1,"unit":"corners/s"},{"id":"cornerRadius","type":"number","label":"Corner radius","default":26,"min":0,"max":90,"step":1,"unit":"px"},{"id":"thickness","type":"number","label":"Thickness","default":5,"min":1,"max":16,"step":1,"unit":"px"},{"id":"arcLength","type":"number","label":"Arc length","default":22,"min":5,"max":100,"step":1,"unit":"%"},{"id":"glow","type":"number","label":"Glow","default":1,"min":0,"max":2,"step":0.05,"unit":"x"}]}
|
||||
>
|
||||
|
||||
```html lt-neon-border.html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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 {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
#root .nb-wrap {
|
||||
position: absolute;
|
||||
left: 50px;
|
||||
bottom: 56px;
|
||||
width: 1120px;
|
||||
height: 510px;
|
||||
}
|
||||
#root .nb-svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: block;
|
||||
}
|
||||
/* The frame's inner box, inset by the bloom padding so the copy lands
|
||||
inside the traced rectangle. */
|
||||
#root .nb-panel {
|
||||
position: absolute;
|
||||
left: 150px;
|
||||
top: 150px;
|
||||
width: 820px;
|
||||
height: 210px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
padding: 0 52px;
|
||||
}
|
||||
#root .nb-name {
|
||||
display: block;
|
||||
font-size: 54px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.015em;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 2px 20px rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
#root .nb-role {
|
||||
display: block;
|
||||
font-size: 25px;
|
||||
font-weight: 400;
|
||||
color: #d5dae3;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 2px 16px rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="lt-neon-border"
|
||||
data-composition-variables='[
|
||||
{"id":"name","type":"string","label":"Name","default":"Dr. Maya Chen"},
|
||||
{"id":"role","type":"string","label":"Role","default":"Host · Neuroscientist"},
|
||||
{"id":"accent","type":"color","label":"Accent","default":"#cc9149"},
|
||||
{"id":"movement","type":"enum","label":"Movement","default":"step","options":[{"value":"step","label":"Step (snap at corners)"},{"value":"glide","label":"Glide (smooth)"}]},
|
||||
{"id":"speed","type":"number","label":"Speed","default":1.6,"min":0.2,"max":6,"step":0.1,"unit":"corners/s"},
|
||||
{"id":"cornerRadius","type":"number","label":"Corner radius","default":26,"min":0,"max":90,"step":1,"unit":"px"},
|
||||
{"id":"thickness","type":"number","label":"Thickness","default":5,"min":1,"max":16,"step":1,"unit":"px"},
|
||||
{"id":"arcLength","type":"number","label":"Arc length","default":22,"min":5,"max":100,"step":1,"unit":"%"},
|
||||
{"id":"glow","type":"number","label":"Glow","default":1,"min":0,"max":2,"step":0.05,"unit":"x"}
|
||||
]'
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="nb-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
|
||||
<div id="nb-wrap" class="nb-wrap">
|
||||
<svg
|
||||
id="nb-svg"
|
||||
class="nb-svg"
|
||||
width="1120"
|
||||
height="510"
|
||||
viewBox="0 0 1120 510"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
<g id="nb-seg-a-glow"></g>
|
||||
<g id="nb-seg-a-core"></g>
|
||||
<g id="nb-seg-b-glow"></g>
|
||||
<g id="nb-seg-b-core"></g>
|
||||
</defs>
|
||||
<g transform="translate(150,150)">
|
||||
<!-- Wide bloom sits UNDER the panel so the halo reads outside the
|
||||
frame and the interior stays hardware-clean. -->
|
||||
<g id="nb-bloom-under"></g>
|
||||
<path id="nb-fill" d="" fill="rgba(9,11,15,0.78)" />
|
||||
<path id="nb-rail" d="" fill="none" stroke-linejoin="round" />
|
||||
<g id="nb-bloom-over"></g>
|
||||
</g>
|
||||
</svg>
|
||||
<div id="nb-panel" class="nb-panel">
|
||||
<span id="nb-name" class="nb-name" data-var-text="name">Dr. Maya Chen</span>
|
||||
<span id="nb-role" class="nb-role" data-var-text="role">Host · Neuroscientist</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||
var W = 820; // traced rectangle width
|
||||
var H = 210; // traced rectangle height
|
||||
var DUR = 4.8;
|
||||
|
||||
var vars =
|
||||
window.__hyperframes && window.__hyperframes.getVariables
|
||||
? window.__hyperframes.getVariables()
|
||||
: {};
|
||||
|
||||
function num(v, fallback) {
|
||||
var n = typeof v === "number" ? v : parseFloat(v);
|
||||
return isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
var accent = /^#[0-9a-fA-F]{6}$/.test(String(vars.accent))
|
||||
? String(vars.accent)
|
||||
: "#cc9149";
|
||||
var mode = vars.movement === "glide" ? "glide" : "step";
|
||||
var speed = Math.max(0.05, num(vars.speed, 1.6)); // corners per second
|
||||
var radius = Math.min(Math.min(W, H) / 2, Math.max(0, num(vars.cornerRadius, 26)));
|
||||
var thickness = Math.max(0.5, num(vars.thickness, 5));
|
||||
var arcFrac = Math.min(1, Math.max(0.02, num(vars.arcLength, 22) / 100));
|
||||
var glow = Math.max(0, num(vars.glow, 1));
|
||||
|
||||
// Hot core: the accent lifted most of the way to white, the way a real
|
||||
// filament reads brighter than its halo.
|
||||
function towardWhite(hex, amount) {
|
||||
var v = parseInt(hex.slice(1), 16);
|
||||
var out = "#";
|
||||
for (var s = 16; s >= 0; s -= 8) {
|
||||
var c = (v >> s) & 255;
|
||||
var m = Math.round(c + (255 - c) * amount);
|
||||
out += (m < 16 ? "0" : "") + m.toString(16);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
var hot = towardWhite(accent, 0.78);
|
||||
|
||||
// ---- geometry -------------------------------------------------------
|
||||
// Rounded rect, clockwise, starting where the top edge leaves the
|
||||
// top-left corner arc.
|
||||
var r = radius;
|
||||
var arc = "A " + r + " " + r + " 0 0 1 ";
|
||||
var d = [
|
||||
"M " + r + " 0",
|
||||
"H " + (W - r),
|
||||
arc + W + " " + r,
|
||||
"V " + (H - r),
|
||||
arc + (W - r) + " " + H,
|
||||
"H " + r,
|
||||
arc + "0 " + (H - r),
|
||||
"V " + r,
|
||||
arc + r + " 0",
|
||||
"Z",
|
||||
].join(" ");
|
||||
|
||||
var fillEl = document.getElementById("nb-fill");
|
||||
var railEl = document.getElementById("nb-rail");
|
||||
fillEl.setAttribute("d", d);
|
||||
railEl.setAttribute("d", d);
|
||||
railEl.setAttribute("stroke", accent);
|
||||
railEl.setAttribute("stroke-width", String(thickness));
|
||||
railEl.setAttribute("stroke-opacity", "0.14");
|
||||
|
||||
// Analytic perimeter, then rescaled to whatever length the renderer
|
||||
// actually measures for the path so dash distances line up exactly.
|
||||
var SW = W - 2 * r; // straight run, horizontal
|
||||
var SH = H - 2 * r; // straight run, vertical
|
||||
var AC = (Math.PI * r) / 2; // one corner arc
|
||||
var P_ANALYTIC = 2 * SW + 2 * SH + 4 * AC;
|
||||
var P = railEl.getTotalLength() || P_ANALYTIC;
|
||||
var K = P / P_ANALYTIC;
|
||||
|
||||
// Distance along the path of the MIDPOINT of each corner arc, so the
|
||||
// light parks centred on the corner rather than skidding past it.
|
||||
var CORNERS = [
|
||||
(SW + AC / 2) * K, // top-right
|
||||
(SW + SH + 1.5 * AC) * K, // bottom-right
|
||||
(2 * SW + SH + 2.5 * AC) * K, // bottom-left
|
||||
(2 * SW + 2 * SH + 3.5 * AC) * K, // top-left
|
||||
];
|
||||
|
||||
/** Absolute path distance of corner k, for any integer k (k may be
|
||||
* negative or beyond one lap). Pure function of k — no accumulator. */
|
||||
function cornerPos(k) {
|
||||
var lap = Math.floor(k / 4);
|
||||
var i = k - lap * 4;
|
||||
return CORNERS[i] + lap * P;
|
||||
}
|
||||
|
||||
// ---- easing ---------------------------------------------------------
|
||||
// The step curve overshoots (y2 = 1.05); that overshoot IS the snap.
|
||||
function cubicBezier(x1, y1, x2, y2) {
|
||||
function axis(t, a, b) {
|
||||
var u = 1 - t;
|
||||
return 3 * u * u * t * a + 3 * u * t * t * b + t * t * t;
|
||||
}
|
||||
return function (x) {
|
||||
if (x <= 0) return 0;
|
||||
if (x >= 1) return 1;
|
||||
var lo = 0;
|
||||
var hi = 1;
|
||||
var t = x;
|
||||
// Fixed iteration count: same input, same output, every run.
|
||||
for (var i = 0; i < 32; i++) {
|
||||
t = (lo + hi) / 2;
|
||||
if (axis(t, x1, x2) < x) lo = t;
|
||||
else hi = t;
|
||||
}
|
||||
return axis(t, y1, y2);
|
||||
};
|
||||
}
|
||||
var ease =
|
||||
mode === "glide" ? cubicBezier(0.65, 0, 0.35, 1) : cubicBezier(0.72, 0.16, 0.18, 1.05);
|
||||
|
||||
var beat = 1 / speed; // seconds per corner hop
|
||||
|
||||
/** Head position (path distance) at time t. floor/frac of t/beat is the
|
||||
* closed form of the reference's `stepT += dt` accumulator. */
|
||||
function headAt(t) {
|
||||
var total = t / beat;
|
||||
var k = Math.floor(total);
|
||||
var u = total - k;
|
||||
var a = cornerPos(k);
|
||||
var b = cornerPos(k + 1);
|
||||
return a + (b - a) * ease(u);
|
||||
}
|
||||
|
||||
// ---- arc construction ----------------------------------------------
|
||||
// Each arc is a run of abutting dash segments whose opacity falls off
|
||||
// behind the head; that ramp is the taper. Geometry lives once in
|
||||
// <defs> and is shared by every bloom layer through <use>.
|
||||
function buildSegments(groupId, count, falloff) {
|
||||
var g = document.getElementById(groupId);
|
||||
var segs = [];
|
||||
for (var i = 0; i < count; i++) {
|
||||
var p = document.createElementNS(SVG_NS, "path");
|
||||
p.setAttribute("d", d);
|
||||
p.setAttribute("fill", "none");
|
||||
p.setAttribute("stroke-linecap", i === 0 ? "round" : "butt");
|
||||
p.setAttribute("opacity", String(Math.pow(1 - i / count, falloff)));
|
||||
g.appendChild(p);
|
||||
segs.push(p);
|
||||
}
|
||||
return segs;
|
||||
}
|
||||
|
||||
var GLOW_SEGS = 20;
|
||||
var CORE_SEGS = 26;
|
||||
var glowLen = arcFrac * P;
|
||||
var coreLen = 0.34 * glowLen;
|
||||
|
||||
var arcs = [
|
||||
{
|
||||
offset: 0,
|
||||
glow: buildSegments("nb-seg-a-glow", GLOW_SEGS, 2.6),
|
||||
core: buildSegments("nb-seg-a-core", CORE_SEGS, 1.4),
|
||||
},
|
||||
{
|
||||
// Half a lap behind: the diametrically opposite corner. The pairing
|
||||
// is most of why the effect reads as designed rather than random.
|
||||
offset: 2,
|
||||
glow: buildSegments("nb-seg-b-glow", GLOW_SEGS, 2.6),
|
||||
core: buildSegments("nb-seg-b-core", CORE_SEGS, 1.4),
|
||||
},
|
||||
];
|
||||
|
||||
// Three stacked bloom layers (soft / medium / wide) plus the filament.
|
||||
// `spread` widens the stroke before it is blurred — that pairing, not
|
||||
// the blur alone, is what makes the halo read as emitted light.
|
||||
var MAX_REACH = 36;
|
||||
var LAYERS = [
|
||||
{ blur: 30, spread: 1, opacity: 0.18, stroke: accent, deep: true },
|
||||
{ blur: 15, spread: 0.6, opacity: 0.3, stroke: accent, deep: true },
|
||||
{ blur: 8, spread: 0.3, opacity: 0.5, stroke: accent, deep: true },
|
||||
{ blur: 4, spread: 0.05, opacity: 0.9, stroke: hot, core: true },
|
||||
{ blur: 0, spread: -0.012, opacity: 1, stroke: "#ffffff", core: true },
|
||||
];
|
||||
|
||||
var under = document.getElementById("nb-bloom-under");
|
||||
var over = document.getElementById("nb-bloom-over");
|
||||
["a", "b"].forEach(function (tag) {
|
||||
LAYERS.forEach(function (layer) {
|
||||
var u = document.createElementNS(SVG_NS, "use");
|
||||
u.setAttribute("href", "#nb-seg-" + tag + "-" + (layer.core ? "core" : "glow"));
|
||||
u.setAttribute("stroke", layer.stroke);
|
||||
u.setAttribute(
|
||||
"stroke-width",
|
||||
String(Math.max(0.5, thickness + 2 * layer.spread * MAX_REACH * glow)),
|
||||
);
|
||||
u.setAttribute("opacity", String(Math.min(1, layer.opacity * glow)));
|
||||
if (layer.blur > 0) u.style.filter = "blur(" + layer.blur + "px)";
|
||||
(layer.deep ? under : over).appendChild(u);
|
||||
});
|
||||
});
|
||||
|
||||
/** Lay a segment run out behind `head`. Every value derives from head. */
|
||||
function layout(segs, head, length) {
|
||||
var step = length / segs.length;
|
||||
for (var i = 0; i < segs.length; i++) {
|
||||
var start = head - (i + 1) * step;
|
||||
start = ((start % P) + P) % P;
|
||||
segs[i].setAttribute("stroke-dasharray", step + " " + (P - step));
|
||||
segs[i].setAttribute("stroke-dashoffset", String(-start));
|
||||
}
|
||||
}
|
||||
|
||||
function render(t) {
|
||||
for (var i = 0; i < arcs.length; i++) {
|
||||
var head = headAt(t + arcs[i].offset * beat);
|
||||
layout(arcs[i].glow, head, glowLen);
|
||||
layout(arcs[i].core, head, coreLen);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- timeline -------------------------------------------------------
|
||||
var wrap = document.getElementById("nb-wrap");
|
||||
var panel = document.getElementById("nb-panel");
|
||||
var nameEl = document.getElementById("nb-name");
|
||||
var roleEl = document.getElementById("nb-role");
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
gsap.set(wrap, { opacity: 0, scale: 0.965, transformOrigin: "0% 100%" });
|
||||
gsap.set(nameEl, { y: 18, opacity: 0 });
|
||||
gsap.set(roleEl, { y: 12, opacity: 0 });
|
||||
|
||||
// The driver: linear, spans the whole clip, and every arc position is
|
||||
// recomputed from its own time value. Seeking to frame N gives the same
|
||||
// state as playing to frame N because nothing carries over.
|
||||
var driver = { t: 0 };
|
||||
tl.to(
|
||||
driver,
|
||||
{
|
||||
t: DUR,
|
||||
duration: DUR,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
render(driver.t);
|
||||
},
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
tl.to(wrap, { opacity: 1, scale: 1, duration: 0.55, ease: "power3.out" }, 0.08);
|
||||
tl.to(nameEl, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.3);
|
||||
tl.to(roleEl, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.44);
|
||||
|
||||
tl.to(roleEl, { y: -10, opacity: 0, duration: 0.3, ease: "power2.in" }, 4.24);
|
||||
tl.to(nameEl, { y: -14, opacity: 0, duration: 0.32, ease: "power2.in" }, 4.3);
|
||||
tl.to(wrap, { opacity: 0, scale: 0.985, duration: 0.36, ease: "power2.in" }, 4.36);
|
||||
tl.set(panel, { visibility: "hidden" }, 4.76);
|
||||
|
||||
render(0);
|
||||
|
||||
window.__timelines["lt-neon-border"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</VariablesExplorer>
|
||||
|
||||
## Install
|
||||
|
||||
<InstallCommand command="npx hyperframes add lt-neon-border" />
|
||||
|
||||
That writes one file: `compositions/lt-neon-border.html`.
|
||||
|
||||
## Add it to your video
|
||||
|
||||
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="lt-neon-border"
|
||||
data-composition-src="compositions/lt-neon-border.html"
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Change how it looks
|
||||
|
||||
Set these CSS variables on the block:
|
||||
|
||||
- `undefined` — Name. Defaults to `Dr. Maya Chen`.
|
||||
- `undefined` — Role. Defaults to `Host · Neuroscientist`.
|
||||
- `undefined` — Accent. Defaults to `#cc9149`.
|
||||
- `undefined` — Movement. Defaults to `step`. Options: `step`, `glide`.
|
||||
- `undefined` — Speed. Defaults to `1.6`.
|
||||
- `undefined` — Corner radius. Defaults to `26`.
|
||||
- `undefined` — Thickness. Defaults to `5`.
|
||||
- `undefined` — Arc length. Defaults to `22`.
|
||||
- `undefined` — Glow. Defaults to `1`.
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `lower-third` `glow` `loop` `video-primitive`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
||||
- [Build a richer composition](/go-further)
|
||||
@@ -3,34 +3,145 @@ title: "Lower Third — Side Rule"
|
||||
description: "Cardless lower third with a vertical accent bar; condensed display name + mono role, text-shadowed for footage"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-side-rule.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-side-rule.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/lt-side-rule.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Side Rule preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add lt-side-rule
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add lt-side-rule" />
|
||||
|
||||
That writes one file: `compositions/lt-side-rule.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`lt-side-rule.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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 {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
|
||||
#root .lt {
|
||||
position: absolute;
|
||||
left: 130px;
|
||||
bottom: 120px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 26px;
|
||||
}
|
||||
#root .lt-bar {
|
||||
width: 8px;
|
||||
background: #46e5b7;
|
||||
border-radius: 4px;
|
||||
transform-origin: 50% 0%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#root .lt-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
#root .lt-name {
|
||||
font-family: "League Gothic", sans-serif;
|
||||
font-size: 92px;
|
||||
font-weight: 400;
|
||||
color: #ffffff;
|
||||
line-height: 0.86;
|
||||
letter-spacing: 0.01em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 2px 22px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
#root .lt-role {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 26px;
|
||||
font-weight: 400;
|
||||
color: #e7eaf0;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 2px 16px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="lt-side-rule"
|
||||
data-composition-src="compositions/lt-side-rule.html"
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
>
|
||||
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
|
||||
<div id="lt" class="lt">
|
||||
<div id="lt-bar" class="lt-bar"></div>
|
||||
<div class="lt-text">
|
||||
<div id="lt-name" class="lt-name">Dr. Maya Chen</div>
|
||||
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var wrap = document.getElementById("lt");
|
||||
var bar = document.getElementById("lt-bar");
|
||||
var nameEl = document.getElementById("lt-name");
|
||||
var role = document.getElementById("lt-role");
|
||||
|
||||
// Vertical accent bar draws top→down, name and role slide in from the left.
|
||||
gsap.set(bar, { scaleY: 0 });
|
||||
gsap.set(nameEl, { x: -24, opacity: 0 });
|
||||
gsap.set(role, { x: -24, opacity: 0 });
|
||||
|
||||
tl.to(bar, { scaleY: 1, duration: 0.5, ease: "power3.out" }, 0.1);
|
||||
tl.to(nameEl, { x: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.26);
|
||||
tl.to(role, { x: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.38);
|
||||
|
||||
// Exit: bar retracts, text lifts, hard kill.
|
||||
tl.to(role, { opacity: 0, duration: 0.3, ease: "power2.in" }, 4.25);
|
||||
tl.to(nameEl, { y: -14, opacity: 0, duration: 0.32, ease: "power2.in" }, 4.3);
|
||||
tl.to(bar, { scaleY: 0, duration: 0.32, ease: "power2.in" }, 4.32);
|
||||
tl.set(wrap, { visibility: "hidden" }, 4.75);
|
||||
|
||||
window.__timelines["lt-side-rule"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="lt-side-rule" data-composition-src="compositions/lt-side-rule.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,150 @@ title: "Lower Third — Soft Pill"
|
||||
description: "Rounded white pill lower third for podcasts/interviews: status dot, name + role, scale-pop entrance"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-soft-pill.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-soft-pill.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/lt-soft-pill.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Soft Pill preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add lt-soft-pill
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add lt-soft-pill" />
|
||||
|
||||
That writes one file: `compositions/lt-soft-pill.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`lt-soft-pill.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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 {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
|
||||
#root .lt {
|
||||
position: absolute;
|
||||
left: 120px;
|
||||
bottom: 120px;
|
||||
}
|
||||
#root .lt-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22px;
|
||||
background: #ffffff;
|
||||
border-radius: 999px;
|
||||
padding: 20px 40px 20px 26px;
|
||||
box-shadow: 0 16px 46px rgba(15, 17, 21, 0.2);
|
||||
transform-origin: 0% 100%;
|
||||
will-change: transform;
|
||||
}
|
||||
#root .lt-dot {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #ff5a36;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#root .lt-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
#root .lt-name {
|
||||
font-size: 46px;
|
||||
font-weight: 700;
|
||||
color: #0f1115;
|
||||
line-height: 1.04;
|
||||
letter-spacing: -0.015em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#root .lt-role {
|
||||
font-size: 25px;
|
||||
font-weight: 400;
|
||||
color: #5a6170;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.01em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="lt-soft-pill"
|
||||
data-composition-src="compositions/lt-soft-pill.html"
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
>
|
||||
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
|
||||
<div id="lt" class="lt">
|
||||
<div id="lt-pill" class="lt-pill">
|
||||
<div id="lt-dot" class="lt-dot"></div>
|
||||
<div class="lt-text">
|
||||
<div id="lt-name" class="lt-name">Dr. Maya Chen</div>
|
||||
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var wrap = document.getElementById("lt");
|
||||
var pill = document.getElementById("lt-pill");
|
||||
var dot = document.getElementById("lt-dot");
|
||||
var nameEl = document.getElementById("lt-name");
|
||||
var role = document.getElementById("lt-role");
|
||||
|
||||
// Pill scales up from the lower-left anchor, dot pulses in, text settles.
|
||||
gsap.set(pill, { scale: 0.88, y: 18, opacity: 0 });
|
||||
gsap.set(dot, { scale: 0 });
|
||||
gsap.set([nameEl, role], { opacity: 0, x: -10 });
|
||||
|
||||
tl.to(pill, { scale: 1, y: 0, opacity: 1, duration: 0.55, ease: "back.out(1.7)" }, 0.1);
|
||||
tl.to(dot, { scale: 1, duration: 0.4, ease: "back.out(3)" }, 0.4);
|
||||
tl.to(nameEl, { opacity: 1, x: 0, duration: 0.45, ease: "power3.out" }, 0.42);
|
||||
tl.to(role, { opacity: 1, x: 0, duration: 0.45, ease: "power3.out" }, 0.5);
|
||||
|
||||
// Exit: settle down + hard kill.
|
||||
tl.to(pill, { scale: 0.94, y: 14, opacity: 0, duration: 0.35, ease: "power2.in" }, 4.3);
|
||||
tl.set(wrap, { visibility: "hidden" }, 4.75);
|
||||
|
||||
window.__timelines["lt-soft-pill"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="lt-soft-pill" data-composition-src="compositions/lt-soft-pill.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,140 @@ title: "Lower Third — Stack Bars"
|
||||
description: "Two stacked bars: a dark name bar wipes from the left, an accent role bar wipes from the right"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-stack-bars.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-stack-bars.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/lt-stack-bars.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Lower Third — Stack Bars preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add lt-stack-bars
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add lt-stack-bars" />
|
||||
|
||||
That writes one file: `compositions/lt-stack-bars.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 4.8 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`lt-stack-bars.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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 {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
|
||||
#root .lt {
|
||||
position: absolute;
|
||||
left: 120px;
|
||||
bottom: 116px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
#root .lt-namebar {
|
||||
background: #141518;
|
||||
padding: 14px 30px;
|
||||
will-change: clip-path;
|
||||
}
|
||||
#root .lt-name {
|
||||
font-family: "Archivo Black", sans-serif;
|
||||
font-size: 52px;
|
||||
font-weight: 400;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.015em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#root .lt-rolebar {
|
||||
background: #ffd23f;
|
||||
padding: 9px 22px;
|
||||
margin-left: 14px;
|
||||
will-change: clip-path;
|
||||
}
|
||||
#root .lt-role {
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 23px;
|
||||
font-weight: 700;
|
||||
color: #141518;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="lt-stack-bars"
|
||||
data-composition-src="compositions/lt-stack-bars.html"
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="4.8"
|
||||
>
|
||||
<div id="lt-clip" class="clip" data-start="0" data-duration="4.8" data-track-index="0">
|
||||
<div id="lt" class="lt">
|
||||
<div id="lt-namebar" class="lt-namebar">
|
||||
<div id="lt-name" class="lt-name">Maya Chen</div>
|
||||
</div>
|
||||
<div id="lt-rolebar" class="lt-rolebar">
|
||||
<div id="lt-role" class="lt-role">Host · Neuroscientist</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
var wrap = document.getElementById("lt");
|
||||
var namebar = document.getElementById("lt-namebar");
|
||||
var rolebar = document.getElementById("lt-rolebar");
|
||||
|
||||
// Name bar wipes in from the left; role bar wipes in from the right (opposing).
|
||||
gsap.set(namebar, { clipPath: "inset(0 100% 0 0)" });
|
||||
gsap.set(rolebar, { clipPath: "inset(0 0 0 100%)" });
|
||||
|
||||
tl.to(namebar, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "power4.out" }, 0.1);
|
||||
tl.to(rolebar, { clipPath: "inset(0 0% 0 0%)", duration: 0.5, ease: "power4.out" }, 0.34);
|
||||
|
||||
// Exit: both wipe back out, hard kill.
|
||||
tl.to(rolebar, { clipPath: "inset(0 0 0 100%)", duration: 0.32, ease: "power3.in" }, 4.25);
|
||||
tl.to(namebar, { clipPath: "inset(0 100% 0 0)", duration: 0.36, ease: "power3.in" }, 4.32);
|
||||
tl.set(wrap, { visibility: "hidden" }, 4.75);
|
||||
|
||||
window.__timelines["lt-stack-bars"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="lt-stack-bars" data-composition-src="compositions/lt-stack-bars.html" data-start="0" data-duration="4.8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,233 @@ title: "macOS Notification"
|
||||
description: "Animated macOS-style notification banner with app icon and message"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/macos-notification.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/macos-notification.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/macos-notification.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="macOS Notification preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add macos-notification
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add macos-notification" />
|
||||
|
||||
That writes one file: `compositions/macos-notification.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 5 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`macos-notification.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=block"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<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 {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
[data-composition-id="macos-notification"] .notification-card {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
width: 380px;
|
||||
background: rgba(30, 30, 30, 0.82);
|
||||
backdrop-filter: blur(40px);
|
||||
-webkit-backdrop-filter: blur(40px);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
inset 0 0 0 0.5px rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
[data-composition-id="macos-notification"] .app-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-composition-id="macos-notification"] .app-icon svg {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
[data-composition-id="macos-notification"] .notification-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-composition-id="macos-notification"] .notification-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
[data-composition-id="macos-notification"] .app-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
[data-composition-id="macos-notification"] .time-label {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
[data-composition-id="macos-notification"] .notification-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
[data-composition-id="macos-notification"] .notification-body {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="macos-notification"
|
||||
data-composition-src="compositions/macos-notification.html"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
>
|
||||
<div
|
||||
id="notification"
|
||||
class="clip notification-card"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-track-index="0"
|
||||
>
|
||||
<div class="app-icon">
|
||||
<svg viewBox="0 0 36 36" fill="none">
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="icon-grad"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="36"
|
||||
y2="36"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0%" stop-color="#6366f1" />
|
||||
<stop offset="100%" stop-color="#8b5cf6" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="36" height="36" rx="8" fill="url(#icon-grad)" />
|
||||
<text
|
||||
x="18"
|
||||
y="23"
|
||||
text-anchor="middle"
|
||||
font-family="Inter, sans-serif"
|
||||
font-size="14"
|
||||
font-weight="700"
|
||||
fill="#fff"
|
||||
>
|
||||
HF
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="notification-content">
|
||||
<div class="notification-header">
|
||||
<span class="app-name">HyperFrames</span>
|
||||
<span class="time-label">now</span>
|
||||
</div>
|
||||
<div class="notification-title">Prompt to replace this title</div>
|
||||
<div class="notification-body">
|
||||
Prompt to change this body text, the app icon, and the app name to match your content.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
var notification = document.getElementById("notification");
|
||||
|
||||
// Start off-screen to the right
|
||||
gsap.set(notification, { x: 420, opacity: 0 });
|
||||
|
||||
// Slide in from right
|
||||
tl.to(
|
||||
notification,
|
||||
{
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
duration: 0.5,
|
||||
ease: "power3.out",
|
||||
},
|
||||
0.2,
|
||||
);
|
||||
|
||||
// Slide out to right
|
||||
tl.to(
|
||||
notification,
|
||||
{
|
||||
x: 420,
|
||||
opacity: 0,
|
||||
duration: 0.3,
|
||||
ease: "power3.in",
|
||||
},
|
||||
4.2,
|
||||
);
|
||||
|
||||
window.__timelines["macos-notification"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="macos-notification" data-composition-src="compositions/macos-notification.html" data-start="0" data-duration="5" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,13 @@ title: "Minimal Gradient Background"
|
||||
description: "Procedural soft-blob gradient backdrop in a minimalist presentation style: two radial blobs drifting over a base color, with an animatable rounded-card bar mask and optional frosted-glass mode"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-background.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-background.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/mk-background.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Minimal Gradient Background preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add mk-background
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add mk-background" />
|
||||
|
||||
That writes one file: `compositions/mk-background.html`.
|
||||
|
||||
@@ -32,6 +32,250 @@ It runs for 10 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`mk-background.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
#mk-bg-root {
|
||||
/* ---- mk tokens: override these in the host to re-skin the family ---- */
|
||||
--mk-font: "Inter", -apple-system, sans-serif;
|
||||
--mk-stage: #f5f5f7;
|
||||
--mk-base: #ffffff;
|
||||
--mk-blob-1: #ff7ac8;
|
||||
--mk-blob-2: #45d6c8;
|
||||
--mk-stage-dark: #000000;
|
||||
--mk-base-dark: #0a0a0c;
|
||||
--mk-blob-1-dark: #7d5cff;
|
||||
--mk-blob-2-dark: #2d1b69;
|
||||
--mk-radius-card: 40px;
|
||||
--mk-shadow: 0 30px 80px rgba(0, 0, 0, 0.18);
|
||||
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--mk-stage);
|
||||
}
|
||||
#mk-bg-root.mk-dark {
|
||||
background: var(--mk-stage-dark);
|
||||
}
|
||||
/* The "Bar" — an animatable rounded-rect mask. Full-bleed by default,
|
||||
animatable down to a floating card over the stage. */
|
||||
#mk-bg-card {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
background: var(--mk-base);
|
||||
box-shadow: var(--mk-shadow);
|
||||
}
|
||||
.mk-dark #mk-bg-card {
|
||||
background: var(--mk-base-dark);
|
||||
}
|
||||
/* Soft radial blobs — the procedural "mesh gradient". Two oversized
|
||||
radial gradients with a long falloff, drifting slowly. */
|
||||
.mk-bg-blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
will-change: transform;
|
||||
}
|
||||
#mk-bg-blob1 {
|
||||
background: radial-gradient(
|
||||
circle closest-side,
|
||||
var(--mk-blob-1) 0%,
|
||||
rgba(255, 255, 255, 0) 72%
|
||||
);
|
||||
}
|
||||
#mk-bg-blob2 {
|
||||
background: radial-gradient(
|
||||
circle closest-side,
|
||||
var(--mk-blob-2) 0%,
|
||||
rgba(255, 255, 255, 0) 72%
|
||||
);
|
||||
}
|
||||
.mk-dark #mk-bg-blob1 {
|
||||
background: radial-gradient(
|
||||
circle closest-side,
|
||||
var(--mk-blob-1-dark) 0%,
|
||||
rgba(0, 0, 0, 0) 72%
|
||||
);
|
||||
}
|
||||
.mk-dark #mk-bg-blob2 {
|
||||
background: radial-gradient(
|
||||
circle closest-side,
|
||||
var(--mk-blob-2-dark) 0%,
|
||||
rgba(0, 0, 0, 0) 72%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="mk-bg-root"
|
||||
data-composition-id="mk-background"
|
||||
data-start="0"
|
||||
data-duration="10"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="mk-bg-card">
|
||||
<div id="mk-bg-blob1" class="mk-bg-blob" data-layout-allow-overflow></div>
|
||||
<div id="mk-bg-blob2" class="mk-bg-blob" data-layout-allow-overflow></div>
|
||||
</div>
|
||||
<div
|
||||
id="mk-bg-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="10"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
/* ---- published parameters (inspector-style CONFIG) ---- */
|
||||
var CONFIG = {
|
||||
scheme: "light", // "light" | "dark"
|
||||
animationIn: true, // fade/settle entrance
|
||||
animationOut: true, // fade exit
|
||||
drift: true, // ambient blob motion
|
||||
// Soft BG blobs: center as fraction of canvas, radius in px
|
||||
blob1: { x: 0.28, y: 0.32, r: 950, opacity: 0.85 },
|
||||
blob2: { x: 0.74, y: 0.66, r: 1100, opacity: 0.8 },
|
||||
// Bar mask (rounded-rect card). demo:true plays full-bleed -> card -> full-bleed
|
||||
bar: {
|
||||
demo: true, // full-bleed -> rounded card -> full-bleed showcase
|
||||
x: 240,
|
||||
y: 140,
|
||||
width: 1440,
|
||||
height: 800,
|
||||
roundness: 40,
|
||||
},
|
||||
// Content opacity + frost only matter with footage beneath the block
|
||||
contentOpacity: 1,
|
||||
frostBlur: 0, // px; e.g. 24 for the frosted-card look over footage
|
||||
};
|
||||
|
||||
var W = 1920,
|
||||
H = 1080,
|
||||
DUR = 10;
|
||||
|
||||
var root = document.getElementById("mk-bg-root");
|
||||
var card = document.getElementById("mk-bg-card");
|
||||
var blob1 = document.getElementById("mk-bg-blob1");
|
||||
var blob2 = document.getElementById("mk-bg-blob2");
|
||||
|
||||
if (CONFIG.scheme === "dark") root.classList.add("mk-dark");
|
||||
|
||||
function placeBlob(el, b) {
|
||||
var d = b.r * 2;
|
||||
el.style.width = d + "px";
|
||||
el.style.height = d + "px";
|
||||
el.style.left = b.x * W - b.r + "px";
|
||||
el.style.top = b.y * H - b.r + "px";
|
||||
el.style.opacity = String(b.opacity);
|
||||
}
|
||||
placeBlob(blob1, CONFIG.blob1);
|
||||
placeBlob(blob2, CONFIG.blob2);
|
||||
|
||||
card.style.opacity = String(CONFIG.contentOpacity);
|
||||
if (CONFIG.frostBlur > 0) {
|
||||
card.style.backdropFilter = "blur(" + CONFIG.frostBlur + "px) saturate(1.4)";
|
||||
}
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
/* entrance — fade + blobs settle (power3, no bounce) */
|
||||
if (CONFIG.animationIn) {
|
||||
tl.from(root, { opacity: 0, duration: 0.7, ease: "power2.out" }, 0);
|
||||
tl.from([blob1, blob2], { scale: 1.14, duration: 1.2, ease: "power3.out" }, 0);
|
||||
}
|
||||
|
||||
/* ambient drift — slow sine yoyo, seek-safe timeline tweens */
|
||||
if (CONFIG.drift) {
|
||||
tl.to(
|
||||
blob1,
|
||||
{ x: 70, y: 42, duration: 2.35, ease: "sine.inOut", yoyo: true, repeat: 3 },
|
||||
0.3,
|
||||
);
|
||||
tl.to(
|
||||
blob2,
|
||||
{ x: -84, y: -36, duration: 2.35, ease: "sine.inOut", yoyo: true, repeat: 3 },
|
||||
0.3,
|
||||
);
|
||||
}
|
||||
|
||||
/* bar-mask demo: full-bleed -> rounded card over the stage -> full-bleed.
|
||||
Offset via x/y transforms, not top/left: layout properties snap to
|
||||
integer device pixels and stutter under seek-by-frame capture. The
|
||||
card's CSS base is top:0/left:0, so the transform values match 1:1. */
|
||||
if (CONFIG.bar.demo) {
|
||||
tl.to(
|
||||
card,
|
||||
{
|
||||
y: CONFIG.bar.y,
|
||||
x: CONFIG.bar.x,
|
||||
width: CONFIG.bar.width,
|
||||
height: CONFIG.bar.height,
|
||||
borderRadius: CONFIG.bar.roundness,
|
||||
duration: 1.1,
|
||||
ease: "power3.inOut",
|
||||
},
|
||||
3.8,
|
||||
);
|
||||
tl.to(
|
||||
card,
|
||||
{
|
||||
y: 0,
|
||||
x: 0,
|
||||
width: W,
|
||||
height: H,
|
||||
borderRadius: 0,
|
||||
duration: 1.0,
|
||||
ease: "power3.inOut",
|
||||
},
|
||||
7.6,
|
||||
);
|
||||
}
|
||||
|
||||
/* exit + hard kill */
|
||||
if (CONFIG.animationOut) {
|
||||
tl.to(root, { opacity: 0, duration: 0.4, ease: "power2.in" }, DUR - 0.45);
|
||||
}
|
||||
tl.set(root, { visibility: "hidden" }, DUR - 0.02);
|
||||
|
||||
window.__timelines["mk-background"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `background` `gradient` `minimal` `professional` `presentation`.
|
||||
|
||||
@@ -3,13 +3,13 @@ title: "Word-Sweep Highlight"
|
||||
description: "A sentence whose word-by-word emphasis is driven by a single scalar — tween it to sync the highlight with a voiceover"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-callout-highlight.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-callout-highlight.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/mk-callout-highlight.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Word-Sweep Highlight preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add mk-callout-highlight
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add mk-callout-highlight" />
|
||||
|
||||
That writes one file: `compositions/mk-callout-highlight.html`.
|
||||
|
||||
@@ -32,6 +32,171 @@ It runs for 8 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`mk-callout-highlight.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
#mk-ch-root {
|
||||
/* ---- mk tokens: override in the host to re-skin the family ---- */
|
||||
--mk-font: "Inter", -apple-system, sans-serif;
|
||||
--mk-ink: #1d1d1f;
|
||||
/* faint = transient pre-sweep tier (tweens up to full ink) — distinct from the family --mk-ink-dim */
|
||||
--mk-ink-faint: #bcbcc2;
|
||||
--mk-ink-dark: #f5f5f7;
|
||||
--mk-ink-faint-dark: rgba(245, 245, 247, 0.32);
|
||||
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent; /* overlays footage or mk-background */
|
||||
}
|
||||
#mk-ch-box {
|
||||
position: absolute;
|
||||
font-family: var(--mk-font);
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
.mk-ch-word {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="mk-ch-root"
|
||||
data-composition-id="mk-callout-highlight"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="mk-ch-box" data-layout-allow-occlusion></div>
|
||||
<div
|
||||
id="mk-ch-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
/* ---- published parameters (inspector-style CONFIG) ----
|
||||
The mechanic: one scalar (highlight progress 0..1) sweeps word-by-word
|
||||
emphasis across the sentence — a "highlight amount"
|
||||
slider. Tween the scalar over the speaker's sentence to follow speech. */
|
||||
var CONFIG = {
|
||||
scheme: "light", // "light" (ink on light bg) | "dark" (white on footage)
|
||||
text: "Functionality was our goal from the very beginning",
|
||||
fontSize: 60,
|
||||
lineHeight: 1.3,
|
||||
maxWidth: 700, // px; sentence wraps inside this
|
||||
x: 1030, // box left edge; null = centered horizontally
|
||||
y: null, // box top edge; null = centered vertically
|
||||
align: "left",
|
||||
sweep: { start: 1.2, end: 6.2 }, // block-local seconds, linear
|
||||
animationIn: true, // fade/rise entrance before the sweep
|
||||
animationOut: true,
|
||||
};
|
||||
|
||||
var DUR = 8;
|
||||
var root = document.getElementById("mk-ch-root");
|
||||
var box = document.getElementById("mk-ch-box");
|
||||
|
||||
var FULL = CONFIG.scheme === "dark" ? "#f5f5f7" : "#1d1d1f";
|
||||
var FAINT = CONFIG.scheme === "dark" ? "rgba(245,245,247,0.32)" : "#bcbcc2";
|
||||
|
||||
box.style.fontSize = CONFIG.fontSize + "px";
|
||||
box.style.lineHeight = String(CONFIG.lineHeight);
|
||||
box.style.maxWidth = CONFIG.maxWidth + "px";
|
||||
box.style.textAlign = CONFIG.align;
|
||||
|
||||
/* build word spans */
|
||||
var wordEls = [];
|
||||
CONFIG.text.split(/\s+/).forEach(function (w, i) {
|
||||
if (i > 0) box.appendChild(document.createTextNode(" "));
|
||||
var span = document.createElement("span");
|
||||
span.id = "mk-ch-w-" + i;
|
||||
span.className = "mk-ch-word";
|
||||
span.textContent = w;
|
||||
span.style.color = FAINT;
|
||||
box.appendChild(span);
|
||||
wordEls.push(span);
|
||||
});
|
||||
|
||||
/* position (measure after building) */
|
||||
var rect = box.getBoundingClientRect();
|
||||
box.style.left =
|
||||
(CONFIG.x === null ? Math.round((1920 - rect.width) / 2) : CONFIG.x) + "px";
|
||||
box.style.top =
|
||||
(CONFIG.y === null ? Math.round((1080 - rect.height) / 2) : CONFIG.y) + "px";
|
||||
|
||||
/* state proxy: GSAP animates st.p, render derives every word color from it */
|
||||
var st = { p: 0 };
|
||||
var mix = gsap.utils.interpolate(FAINT, FULL);
|
||||
function render() {
|
||||
var k = st.p * wordEls.length;
|
||||
for (var i = 0; i < wordEls.length; i++) {
|
||||
var f = Math.max(0, Math.min(1, k - i));
|
||||
wordEls[i].style.color = mix(f);
|
||||
}
|
||||
}
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
if (CONFIG.animationIn) {
|
||||
gsap.set(box, { opacity: 0, y: 24 });
|
||||
tl.to(box, { opacity: 1, y: 0, duration: 0.7, ease: "power3.out" }, 0.3);
|
||||
}
|
||||
|
||||
/* the highlight sweep — one scalar, linear so it tracks speech evenly.
|
||||
To follow a real voiceover, replace with several tweens between
|
||||
per-phrase progress values (keyframes on the slider). */
|
||||
tl.to(
|
||||
st,
|
||||
{ p: 1, duration: CONFIG.sweep.end - CONFIG.sweep.start, ease: "none" },
|
||||
CONFIG.sweep.start,
|
||||
);
|
||||
|
||||
if (CONFIG.animationOut) {
|
||||
tl.to(box, { opacity: 0, y: -16, duration: 0.4, ease: "power2.in" }, DUR - 0.5);
|
||||
}
|
||||
tl.set(root, { visibility: "hidden" }, DUR - 0.02);
|
||||
|
||||
tl.eventCallback("onUpdate", render);
|
||||
render();
|
||||
|
||||
window.__timelines["mk-callout-highlight"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `typography` `karaoke` `captions` `minimal`.
|
||||
|
||||
@@ -3,13 +3,13 @@ title: "Text Clone Wall Transition"
|
||||
description: "Overlay transition: a tiled word wall covers the frame, inverts via difference blending, and clears — hard-cut the content beneath while covered"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-clone-wall-transition.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-clone-wall-transition.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/mk-clone-wall-transition.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Text Clone Wall Transition preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add mk-clone-wall-transition
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add mk-clone-wall-transition" />
|
||||
|
||||
That writes one file: `compositions/mk-clone-wall-transition.html`.
|
||||
|
||||
@@ -32,6 +32,240 @@ It runs for 2.4 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`mk-clone-wall-transition.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
#mk-cw-root {
|
||||
/* ---- mk tokens: override in the host to re-skin the family ---- */
|
||||
--mk-font: "Inter", -apple-system, sans-serif;
|
||||
--mk-ink: #1d1d1f;
|
||||
--mk-paper: #ffffff;
|
||||
--mk-blob-1: #ff7ac8;
|
||||
--mk-blob-2: #45d6c8;
|
||||
--mk-radius-card: 40px;
|
||||
--mk-shadow: 0 30px 80px rgba(0, 0, 0, 0.22);
|
||||
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
/* the wall: paper sheet + tiled word rows + inversion layer, exits as one */
|
||||
#mk-cw-wall {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--mk-paper);
|
||||
/* no overflow:hidden — the composition root clips; isolation keeps the
|
||||
difference blend scoped to the wall's own tiles */
|
||||
isolation: isolate;
|
||||
}
|
||||
.mk-cw-row {
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
font-family: var(--mk-font);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--mk-ink);
|
||||
line-height: 1;
|
||||
}
|
||||
.mk-cw-tile {
|
||||
display: inline-block;
|
||||
}
|
||||
/* inversion sweep — a paper-colored rect in difference blend:
|
||||
a paper-colored rect XORs the wall, flipping paper<->ink as it arrives */
|
||||
#mk-cw-invert {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--mk-paper);
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
/* the outgoing frame shrinking to a rounded card mid-transition */
|
||||
#mk-cw-card {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(
|
||||
120deg,
|
||||
#fdfbfd 0%,
|
||||
var(--mk-blob-1) 38%,
|
||||
var(--mk-blob-2) 100%
|
||||
);
|
||||
box-shadow: var(--mk-shadow);
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
#mk-cw-card img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="mk-cw-root"
|
||||
data-composition-id="mk-clone-wall-transition"
|
||||
data-start="0"
|
||||
data-duration="2.4"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="mk-cw-wall" data-layout-allow-overflow>
|
||||
<div id="mk-cw-tiles" data-layout-allow-overflow></div>
|
||||
<div id="mk-cw-invert" data-layout-allow-overflow></div>
|
||||
</div>
|
||||
<div id="mk-cw-card" data-layout-allow-overflow></div>
|
||||
<div
|
||||
id="mk-cw-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="2.4"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
/* ---- published parameters (inspector-style CONFIG) ----
|
||||
Overlay-transition model: place this block spanning the host's cut
|
||||
point (e.g. start 0.9s before the cut). The wall fully covers the
|
||||
frame from ~0.6s to ~1.9s local — hard-cut the content beneath
|
||||
anywhere in that window. */
|
||||
var CONFIG = {
|
||||
// no scheme param — scheme-agnostic; re-skin via the --mk-* tokens
|
||||
word: "HyperFrames", // the cloned word
|
||||
fontSize: 240, // px — the tiled text size
|
||||
spreadX: 90, // px gap between tiles (tile spread X)
|
||||
spreadY: 60, // px gap between rows (Spread Y)
|
||||
brickOffset: true, // alternate rows offset by half a tile
|
||||
card: {
|
||||
enabled: true, // outgoing-frame card. In real use set src to a
|
||||
src: null, // still of the outgoing clip; null = family gradient
|
||||
},
|
||||
driftX: -46, // slow wall drift during the hold
|
||||
};
|
||||
|
||||
var W = 1920,
|
||||
H = 1080,
|
||||
DUR = 2.4;
|
||||
|
||||
var root = document.getElementById("mk-cw-root");
|
||||
var wall = document.getElementById("mk-cw-wall");
|
||||
var tilesBox = document.getElementById("mk-cw-tiles");
|
||||
var invert = document.getElementById("mk-cw-invert");
|
||||
var card = document.getElementById("mk-cw-card");
|
||||
|
||||
/* build the tile wall — rows of repeated words, brick-offset */
|
||||
var rowH = CONFIG.fontSize + CONFIG.spreadY;
|
||||
var rows = Math.ceil(H / rowH) + 1;
|
||||
/* rough tile width estimate to fill each row past both edges */
|
||||
var tileW = CONFIG.word.length * CONFIG.fontSize * 0.58 + CONFIG.spreadX;
|
||||
var perRow = Math.ceil(W / tileW) + 2;
|
||||
for (var r = 0; r < rows; r++) {
|
||||
var row = document.createElement("div");
|
||||
row.id = "mk-cw-row-" + r;
|
||||
row.className = "mk-cw-row";
|
||||
/* rows intentionally run past the canvas and sit beneath the card */
|
||||
row.setAttribute("data-layout-allow-overflow", "");
|
||||
row.setAttribute("data-layout-allow-occlusion", "");
|
||||
row.style.fontSize = CONFIG.fontSize + "px";
|
||||
row.style.top = r * rowH - CONFIG.fontSize * 0.35 + "px";
|
||||
var offset = CONFIG.brickOffset && r % 2 === 1 ? -Math.round(tileW / 2) : 0;
|
||||
row.style.left = offset - tileW + "px";
|
||||
for (var c = 0; c < perRow + 1; c++) {
|
||||
var tile = document.createElement("span");
|
||||
tile.className = "mk-cw-tile";
|
||||
tile.textContent = CONFIG.word;
|
||||
tile.style.marginRight = CONFIG.spreadX + "px";
|
||||
row.appendChild(tile);
|
||||
}
|
||||
tilesBox.appendChild(row);
|
||||
}
|
||||
|
||||
if (CONFIG.card.enabled && CONFIG.card.src) {
|
||||
var img = document.createElement("img");
|
||||
img.src = CONFIG.card.src;
|
||||
card.appendChild(img);
|
||||
}
|
||||
if (!CONFIG.card.enabled) card.style.display = "none";
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
/* The wall sits in place from t=0, fully hidden beneath the
|
||||
full-bleed card — the card's shrink IS the wall's reveal. */
|
||||
|
||||
/* 0.2–0.95 outgoing frame shrinks to a rounded card on the wall.
|
||||
x/y rather than left/top — layout properties snap to integer device
|
||||
pixels under seek-by-frame capture. Base is top:0/left:0, so the
|
||||
values carry over unchanged, and the later scale composes cleanly. */
|
||||
if (CONFIG.card.enabled) {
|
||||
tl.to(
|
||||
card,
|
||||
{
|
||||
y: 330,
|
||||
x: 640,
|
||||
width: 640,
|
||||
height: 420,
|
||||
borderRadius: 40,
|
||||
duration: 0.75,
|
||||
ease: "power3.inOut",
|
||||
},
|
||||
0.2,
|
||||
);
|
||||
/* 1.35–1.75 card is put away */
|
||||
tl.to(card, { scale: 0.6, opacity: 0, duration: 0.4, ease: "power2.in" }, 1.35);
|
||||
}
|
||||
|
||||
/* slow drift keeps the wall alive during the hold */
|
||||
tl.to(tilesBox, { x: CONFIG.driftX, duration: 1.8, ease: "sine.inOut" }, 0.3);
|
||||
|
||||
/* 0.85–1.5 inversion sweeps in and STAYS — wall flips ink<->paper */
|
||||
gsap.set(invert, { x: -W });
|
||||
tl.to(invert, { x: 0, duration: 0.65, ease: "power3.inOut" }, 0.85);
|
||||
|
||||
/* 1.9–2.35 the inverted wall fades off, revealing the cut beneath */
|
||||
tl.to(wall, { opacity: 0, duration: 0.45, ease: "power2.inOut" }, 1.9);
|
||||
|
||||
/* hard kills — wall and card die the moment their fades complete */
|
||||
if (CONFIG.card.enabled) tl.set(card, { visibility: "hidden" }, 1.76);
|
||||
tl.set(root, { visibility: "hidden" }, 2.36);
|
||||
|
||||
window.__timelines["mk-clone-wall-transition"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `transition` `typography` `wipe`.
|
||||
|
||||
@@ -3,13 +3,13 @@ title: "Minimal Line Graph"
|
||||
description: "One or two SVG series draw on with dots popping and value labels riding the draw front; dim baseline axis, x-labels, color-dot legend"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-line-graph.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-line-graph.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/mk-line-graph.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Minimal Line Graph preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add mk-line-graph
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add mk-line-graph" />
|
||||
|
||||
That writes one file: `compositions/mk-line-graph.html`.
|
||||
|
||||
@@ -32,6 +32,307 @@ It runs for 7 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`mk-line-graph.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
#mk-lg-root {
|
||||
/* ---- mk tokens: override in the host to re-skin the family ---- */
|
||||
--mk-font: "Inter", -apple-system, sans-serif;
|
||||
--mk-ink: #1d1d1f;
|
||||
--mk-ink-dim: #6e6e73;
|
||||
--mk-accent: #0071e3;
|
||||
--mk-paper: #ffffff;
|
||||
--mk-blob-2: #45d6c8;
|
||||
--mk-axis: rgba(29, 29, 31, 0.22);
|
||||
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent; /* overlays footage or mk-background */
|
||||
font-family: var(--mk-font);
|
||||
}
|
||||
#mk-lg-svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.mk-lg-line {
|
||||
fill: none;
|
||||
stroke-width: 5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.mk-lg-dot {
|
||||
stroke-width: 4;
|
||||
fill: var(--mk-paper);
|
||||
}
|
||||
.mk-lg-axis {
|
||||
stroke: var(--mk-axis);
|
||||
stroke-width: 2;
|
||||
}
|
||||
.mk-lg-value {
|
||||
position: absolute;
|
||||
font-weight: 600;
|
||||
font-size: 30px;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--mk-ink);
|
||||
font-variant-numeric: tabular-nums;
|
||||
/* Horizontal centring only. The label's vertical offset is baked into
|
||||
`top` instead of a CSS translate, because the entrance tween animates
|
||||
`y` and GSAP rewrites this transform, dropping any vertical shift. */
|
||||
transform: translate(-50%, 0);
|
||||
line-height: 38px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mk-lg-xlabel {
|
||||
position: absolute;
|
||||
font-weight: 400;
|
||||
font-size: 26px;
|
||||
color: var(--mk-ink-dim);
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mk-lg-legend {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
}
|
||||
.mk-lg-legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 30px;
|
||||
color: var(--mk-ink-dim);
|
||||
}
|
||||
.mk-lg-legend-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="mk-lg-root"
|
||||
data-composition-id="mk-line-graph"
|
||||
data-start="0"
|
||||
data-duration="7"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<svg id="mk-lg-svg" viewBox="0 0 1920 1080"></svg>
|
||||
<div id="mk-lg-labels"></div>
|
||||
<div
|
||||
id="mk-lg-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="7"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
/* ---- published parameters (inspector-style CONFIG) ----
|
||||
A minimal presentation line graph: 1–2 series draw on, dots pop, value labels
|
||||
ride their points, axes dimmer than labels (clear hierarchy). */
|
||||
var CONFIG = {
|
||||
// no scheme param — scheme-agnostic; re-skin via the --mk-* tokens
|
||||
series: [
|
||||
{ name: "Renders", color: null /* accent */, values: [12, 26, 22, 38, 44, 58] },
|
||||
{ name: "Projects", color: null /* blob-2 */, values: [8, 14, 18, 16, 28, 36] },
|
||||
],
|
||||
xLabels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
|
||||
showValues: true,
|
||||
area: { x: 160, y: 240, w: 980, h: 500 }, // plot rectangle
|
||||
animationIn: true,
|
||||
animationOut: true,
|
||||
};
|
||||
|
||||
var DUR = 7;
|
||||
var NS = "http://www.w3.org/2000/svg";
|
||||
var root = document.getElementById("mk-lg-root");
|
||||
var svg = document.getElementById("mk-lg-svg");
|
||||
var labels = document.getElementById("mk-lg-labels");
|
||||
var A = CONFIG.area;
|
||||
|
||||
var accent = getComputedStyle(root).getPropertyValue("--mk-accent").trim();
|
||||
var blob2 = getComputedStyle(root).getPropertyValue("--mk-blob-2").trim();
|
||||
if (CONFIG.series[1] && !CONFIG.series[1].color) CONFIG.series[1].color = blob2;
|
||||
|
||||
/* scale: 0..max(all values)*1.15 into the plot rect */
|
||||
var max = 0;
|
||||
CONFIG.series.forEach(function (s) {
|
||||
s.values.forEach(function (v) {
|
||||
if (v > max) max = v;
|
||||
});
|
||||
});
|
||||
max *= 1.15;
|
||||
var n = CONFIG.series[0].values.length;
|
||||
function px(i) {
|
||||
return A.x + (i / (n - 1)) * A.w;
|
||||
}
|
||||
function py(v) {
|
||||
return A.y + A.h - (v / max) * A.h;
|
||||
}
|
||||
|
||||
/* baseline axis */
|
||||
var axis = document.createElementNS(NS, "line");
|
||||
axis.setAttribute("class", "mk-lg-axis");
|
||||
axis.setAttribute("x1", A.x - 20);
|
||||
axis.setAttribute("y1", A.y + A.h);
|
||||
axis.setAttribute("x2", A.x + A.w + 20);
|
||||
axis.setAttribute("y2", A.y + A.h);
|
||||
axis.id = "mk-lg-axis";
|
||||
svg.appendChild(axis);
|
||||
|
||||
/* series paths, dots, value labels */
|
||||
var paths = [],
|
||||
dots = [],
|
||||
vals = [];
|
||||
CONFIG.series.forEach(function (s, si) {
|
||||
var color = s.color || accent;
|
||||
var d = s.values
|
||||
.map(function (v, i) {
|
||||
return (i === 0 ? "M" : "L") + px(i) + " " + py(v);
|
||||
})
|
||||
.join(" ");
|
||||
var path = document.createElementNS(NS, "path");
|
||||
path.setAttribute("class", "mk-lg-line");
|
||||
path.setAttribute("d", d);
|
||||
path.setAttribute("stroke", color);
|
||||
path.id = "mk-lg-path-" + si;
|
||||
svg.appendChild(path);
|
||||
paths.push(path);
|
||||
|
||||
s.values.forEach(function (v, i) {
|
||||
var dot = document.createElementNS(NS, "circle");
|
||||
dot.setAttribute("class", "mk-lg-dot");
|
||||
dot.setAttribute("cx", px(i));
|
||||
dot.setAttribute("cy", py(v));
|
||||
dot.setAttribute("r", 9);
|
||||
dot.setAttribute("stroke", color);
|
||||
dot.id = "mk-lg-dot-" + si + "-" + i;
|
||||
svg.appendChild(dot);
|
||||
dots.push({ el: dot, si: si, i: i });
|
||||
|
||||
if (CONFIG.showValues) {
|
||||
var val = document.createElement("div");
|
||||
/* Series after the first label downward. With every series
|
||||
labelling upward the numbers collide wherever the lines run
|
||||
close together. GAP clears the dot radius plus line stroke. */
|
||||
var below = si > 0;
|
||||
var GAP = 22;
|
||||
val.className = "mk-lg-value";
|
||||
val.id = "mk-lg-val-" + si + "-" + i;
|
||||
val.textContent = v;
|
||||
val.style.left = px(i) + "px";
|
||||
val.style.top = py(v) + (below ? GAP : -(GAP + 38)) + "px";
|
||||
labels.appendChild(val);
|
||||
vals.push({ el: val, si: si, i: i });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* x labels + legend */
|
||||
CONFIG.xLabels.slice(0, n).forEach(function (t, i) {
|
||||
var xl = document.createElement("div");
|
||||
xl.className = "mk-lg-xlabel";
|
||||
xl.id = "mk-lg-xl-" + i;
|
||||
xl.textContent = t;
|
||||
xl.style.left = px(i) + "px";
|
||||
xl.style.top = A.y + A.h + 24 + "px";
|
||||
labels.appendChild(xl);
|
||||
});
|
||||
var legend = document.createElement("div");
|
||||
legend.className = "mk-lg-legend";
|
||||
legend.id = "mk-lg-legend";
|
||||
legend.style.left = A.x + "px";
|
||||
legend.style.top = A.y + A.h + 90 + "px";
|
||||
CONFIG.series.forEach(function (s) {
|
||||
var item = document.createElement("div");
|
||||
item.className = "mk-lg-legend-item";
|
||||
var dot = document.createElement("span");
|
||||
dot.className = "mk-lg-legend-dot";
|
||||
dot.style.background = s.color || accent;
|
||||
item.appendChild(dot);
|
||||
item.appendChild(document.createTextNode(s.name));
|
||||
legend.appendChild(item);
|
||||
});
|
||||
labels.appendChild(legend);
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
var DRAW = 1.3;
|
||||
|
||||
/* entrance: axis + x labels settle first */
|
||||
gsap.set([axis, legend], { opacity: 0 });
|
||||
tl.to(axis, { opacity: 1, duration: 0.5, ease: "power2.out" }, 0.2);
|
||||
CONFIG.xLabels.slice(0, n).forEach(function (t, i) {
|
||||
var xl = document.getElementById("mk-lg-xl-" + i);
|
||||
gsap.set(xl, { opacity: 0, y: 10 });
|
||||
tl.to(xl, { opacity: 1, y: 0, duration: 0.4, ease: "power2.out" }, 0.25 + i * 0.05);
|
||||
});
|
||||
|
||||
/* draw each series, dots + values riding the draw front */
|
||||
CONFIG.series.forEach(function (s, si) {
|
||||
var t0 = (CONFIG.animationIn ? 0.5 : 0) + si * 0.35;
|
||||
var path = paths[si];
|
||||
var len = path.getTotalLength();
|
||||
gsap.set(path, { strokeDasharray: len, strokeDashoffset: len });
|
||||
tl.to(path, { strokeDashoffset: 0, duration: DRAW, ease: "power2.inOut" }, t0);
|
||||
|
||||
s.values.forEach(function (v, i) {
|
||||
var dot = document.getElementById("mk-lg-dot-" + si + "-" + i);
|
||||
var td = t0 + (i / (n - 1)) * DRAW * 0.92;
|
||||
gsap.set(dot, { scale: 0, transformOrigin: "50% 50%" });
|
||||
tl.to(dot, { scale: 1, duration: 0.35, ease: "back.out(1.2)" }, td);
|
||||
if (CONFIG.showValues) {
|
||||
var val = document.getElementById("mk-lg-val-" + si + "-" + i);
|
||||
gsap.set(val, { opacity: 0, y: 8 });
|
||||
tl.to(val, { opacity: 1, y: 0, duration: 0.35, ease: "power2.out" }, td + 0.06);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tl.to(legend, { opacity: 1, duration: 0.5, ease: "power2.out" }, 2.3);
|
||||
|
||||
/* exit + hard kill */
|
||||
if (CONFIG.animationOut) {
|
||||
tl.to([svg, labels], { opacity: 0, y: -20, duration: 0.4, ease: "power2.in" }, DUR - 0.5);
|
||||
}
|
||||
tl.set(root, { visibility: "hidden" }, DUR - 0.02);
|
||||
|
||||
window.__timelines["mk-line-graph"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `data-viz` `chart` `graph` `minimal`.
|
||||
|
||||
@@ -3,13 +3,13 @@ title: "Rounded Media Grid"
|
||||
description: "N-up rounded-corner media grid (2-up, 3-up, 4-up, 3x2, hero plus two): per-cell inside-scale framing, staggered box-in entrance, slow settle"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-placeholder-grid.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-placeholder-grid.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/mk-placeholder-grid.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Rounded Media Grid preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add mk-placeholder-grid
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add mk-placeholder-grid" />
|
||||
|
||||
That writes one file: `compositions/mk-placeholder-grid.html`.
|
||||
|
||||
@@ -32,6 +32,248 @@ It runs for 8 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`mk-placeholder-grid.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
#mk-pg-root {
|
||||
/* ---- mk tokens: override in the host to re-skin the family ---- */
|
||||
--mk-font: "Inter", -apple-system, sans-serif;
|
||||
--mk-ink-dim: #6e6e73;
|
||||
--mk-paper: #ffffff;
|
||||
--mk-blob-1: #ff7ac8;
|
||||
--mk-blob-2: #45d6c8;
|
||||
--mk-accent-dark: #7d5cff;
|
||||
--mk-blob-1-soft: #ffd3ec;
|
||||
--mk-blob-2-soft: #d9fff8;
|
||||
--mk-accent-dark-soft: #cabdff;
|
||||
--mk-well-warm: #ffb86b;
|
||||
--mk-well-warm-soft: #ffe3c2;
|
||||
--mk-well-sky: #5cb8ff;
|
||||
--mk-well-sky-soft: #cfe9ff;
|
||||
--mk-well-rose: #ff8f8f;
|
||||
--mk-well-rose-soft: #ffd6d6;
|
||||
--mk-well-num: rgba(255, 255, 255, 0.9);
|
||||
--mk-radius-card: 40px;
|
||||
--mk-shadow: 0 24px 64px rgba(0, 0, 0, 0.16);
|
||||
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent; /* pair with mk-background beneath for the gradient backdrop */
|
||||
}
|
||||
.mk-pg-cell {
|
||||
position: absolute;
|
||||
border-radius: var(--mk-radius-card);
|
||||
overflow: hidden;
|
||||
background: var(--mk-paper);
|
||||
box-shadow: var(--mk-shadow);
|
||||
}
|
||||
.mk-pg-media {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
/* empty drop-zone well: soft gradient + index numeral */
|
||||
.mk-pg-well {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.mk-pg-well-num {
|
||||
font-family: var(--mk-font);
|
||||
font-weight: 600;
|
||||
font-size: 88px;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--mk-well-num);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="mk-pg-root"
|
||||
data-composition-id="mk-placeholder-grid"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="mk-pg-grid"></div>
|
||||
<div
|
||||
id="mk-pg-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
/* ---- published parameters (inspector-style CONFIG) ---- */
|
||||
var CONFIG = {
|
||||
// no scheme param — scheme-agnostic; re-skin via the --mk-* tokens
|
||||
layout: "1+2", // "2up" | "3up" | "4up" | "3x2" | "1+2" (hero left + two stacked)
|
||||
margin: 120, // outer margin, px
|
||||
gap: 40, // px between cells
|
||||
radius: 40, // cell corner radius (rounded-edge look)
|
||||
// One entry per cell (in layout order). src: image path, or null for a
|
||||
// styled drop-zone well. insideScale: media scale inside the mask —
|
||||
// an "inside scale" convention (1 = cover-fit).
|
||||
cells: [
|
||||
{ src: null, insideScale: 1 },
|
||||
{ src: null, insideScale: 1 },
|
||||
{ src: null, insideScale: 1 },
|
||||
],
|
||||
kenBurns: true, // slow media settle inside the mask
|
||||
stagger: 0.15, // "box, box, box" entrance
|
||||
animationIn: true,
|
||||
animationOut: true,
|
||||
};
|
||||
|
||||
/* decorative fills for empty wells (cycled) */
|
||||
var WELL_FILLS = [
|
||||
"linear-gradient(135deg, var(--mk-blob-1) 0%, var(--mk-blob-1-soft) 100%)",
|
||||
"linear-gradient(135deg, var(--mk-blob-2) 0%, var(--mk-blob-2-soft) 100%)",
|
||||
"linear-gradient(135deg, var(--mk-accent-dark) 0%, var(--mk-accent-dark-soft) 100%)",
|
||||
"linear-gradient(135deg, var(--mk-well-warm) 0%, var(--mk-well-warm-soft) 100%)",
|
||||
"linear-gradient(135deg, var(--mk-well-sky) 0%, var(--mk-well-sky-soft) 100%)",
|
||||
"linear-gradient(135deg, var(--mk-well-rose) 0%, var(--mk-well-rose-soft) 100%)",
|
||||
];
|
||||
|
||||
var W = 1920,
|
||||
H = 1080,
|
||||
DUR = 8;
|
||||
|
||||
function rects(layout, m, g) {
|
||||
var iw = W - 2 * m,
|
||||
ih = H - 2 * m;
|
||||
function grid(cols, rows) {
|
||||
var cw = (iw - (cols - 1) * g) / cols,
|
||||
ch = (ih - (rows - 1) * g) / rows,
|
||||
out = [];
|
||||
for (var r = 0; r < rows; r++)
|
||||
for (var c = 0; c < cols; c++)
|
||||
out.push({ x: m + c * (cw + g), y: m + r * (ch + g), w: cw, h: ch });
|
||||
return out;
|
||||
}
|
||||
if (layout === "2up") return grid(2, 1);
|
||||
if (layout === "3up") return grid(3, 1);
|
||||
if (layout === "4up") return grid(2, 2);
|
||||
if (layout === "3x2") return grid(3, 2);
|
||||
/* "1+2": hero left (58%), two stacked right */
|
||||
var w1 = Math.round((iw - g) * 0.58),
|
||||
w2 = iw - g - w1,
|
||||
h2 = (ih - g) / 2;
|
||||
return [
|
||||
{ x: m, y: m, w: w1, h: ih },
|
||||
{ x: m + w1 + g, y: m, w: w2, h: h2 },
|
||||
{ x: m + w1 + g, y: m + h2 + g, w: w2, h: h2 },
|
||||
];
|
||||
}
|
||||
|
||||
var root = document.getElementById("mk-pg-root");
|
||||
var grid = document.getElementById("mk-pg-grid");
|
||||
var R = rects(CONFIG.layout, CONFIG.margin, CONFIG.gap);
|
||||
|
||||
var cells = [],
|
||||
medias = [];
|
||||
R.forEach(function (r, i) {
|
||||
var conf = CONFIG.cells[i] || {};
|
||||
var cell = document.createElement("div");
|
||||
cell.id = "mk-pg-cell-" + i;
|
||||
cell.className = "mk-pg-cell";
|
||||
cell.style.left = r.x + "px";
|
||||
cell.style.top = r.y + "px";
|
||||
cell.style.width = r.w + "px";
|
||||
cell.style.height = r.h + "px";
|
||||
cell.style.borderRadius = CONFIG.radius + "px";
|
||||
|
||||
var inner;
|
||||
if (conf.src) {
|
||||
inner = document.createElement("img");
|
||||
inner.className = "mk-pg-media";
|
||||
inner.src = conf.src;
|
||||
} else {
|
||||
inner = document.createElement("div");
|
||||
inner.className = "mk-pg-well";
|
||||
inner.style.background = WELL_FILLS[i % WELL_FILLS.length];
|
||||
var num = document.createElement("span");
|
||||
num.className = "mk-pg-well-num";
|
||||
num.textContent = String(i + 1);
|
||||
inner.appendChild(num);
|
||||
}
|
||||
inner.id = "mk-pg-inner-" + i;
|
||||
inner.setAttribute("data-layout-allow-overflow", ""); // ken-burns scales past the mask by design
|
||||
cell.appendChild(inner);
|
||||
grid.appendChild(cell);
|
||||
cells.push(cell);
|
||||
medias.push(inner);
|
||||
|
||||
gsap.set(inner, { scale: conf.insideScale || 1 });
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
cells.forEach(function (cell, i) {
|
||||
var t0 = (CONFIG.animationIn ? 0.3 : 0) + i * CONFIG.stagger;
|
||||
|
||||
if (CONFIG.animationIn) {
|
||||
gsap.set(cell, { opacity: 0, scale: 0.92, y: 28 });
|
||||
tl.to(cell, { opacity: 1, scale: 1, y: 0, duration: 0.65, ease: "power3.out" }, t0);
|
||||
}
|
||||
|
||||
if (CONFIG.kenBurns) {
|
||||
var base = (CONFIG.cells[i] && CONFIG.cells[i].insideScale) || 1;
|
||||
tl.fromTo(
|
||||
medias[i],
|
||||
{ scale: base * 1.06 },
|
||||
{ scale: base, duration: DUR - t0 - 0.6, ease: "sine.out" },
|
||||
t0,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/* exit + hard kill */
|
||||
if (CONFIG.animationOut) {
|
||||
tl.to(grid, { opacity: 0, y: -20, duration: 0.4, ease: "power2.in" }, DUR - 0.5);
|
||||
}
|
||||
tl.set(root, { visibility: "hidden" }, DUR - 0.02);
|
||||
|
||||
window.__timelines["mk-placeholder-grid"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `placeholder` `media` `grid` `minimal`.
|
||||
|
||||
@@ -3,13 +3,13 @@ title: "Count-Up Stat Card"
|
||||
description: "Big-numeral statistic with count-up, label, thin progress track filling to value/max, and caption; light and dark schemes"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-progress-stat.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-progress-stat.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/mk-progress-stat.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Count-Up Stat Card preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add mk-progress-stat
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add mk-progress-stat" />
|
||||
|
||||
That writes one file: `compositions/mk-progress-stat.html`.
|
||||
|
||||
@@ -32,6 +32,200 @@ It runs for 7 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`mk-progress-stat.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
#mk-ps-root {
|
||||
/* ---- mk tokens: override in the host to re-skin the family ---- */
|
||||
--mk-font: "Inter", -apple-system, sans-serif;
|
||||
--mk-ink: #1d1d1f;
|
||||
--mk-ink-dim: #6e6e73;
|
||||
--mk-ink-dark: #f5f5f7;
|
||||
--mk-ink-dim-dark: #a1a1a6;
|
||||
--mk-accent: #0071e3;
|
||||
--mk-accent-dark: #7d5cff;
|
||||
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent; /* overlays footage or mk-background */
|
||||
}
|
||||
#mk-ps-group {
|
||||
position: absolute;
|
||||
font-family: var(--mk-font);
|
||||
}
|
||||
#mk-ps-num {
|
||||
font-weight: 600;
|
||||
font-size: 190px;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--mk-ink);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
#mk-ps-label {
|
||||
margin-top: 10px;
|
||||
font-weight: 500;
|
||||
font-size: 42px;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--mk-ink);
|
||||
}
|
||||
#mk-ps-track {
|
||||
position: relative;
|
||||
margin-top: 28px;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: rgba(29, 29, 31, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
#mk-ps-fill {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 3px;
|
||||
background: var(--mk-accent);
|
||||
transform-origin: left center;
|
||||
}
|
||||
#mk-ps-caption {
|
||||
margin-top: 22px;
|
||||
font-weight: 400;
|
||||
font-size: 30px;
|
||||
color: var(--mk-ink-dim);
|
||||
}
|
||||
.mk-dark #mk-ps-num,
|
||||
.mk-dark #mk-ps-label {
|
||||
color: var(--mk-ink-dark);
|
||||
}
|
||||
.mk-dark #mk-ps-caption {
|
||||
color: var(--mk-ink-dim-dark);
|
||||
}
|
||||
.mk-dark #mk-ps-track {
|
||||
background: rgba(245, 245, 247, 0.18);
|
||||
}
|
||||
.mk-dark #mk-ps-fill {
|
||||
background: var(--mk-accent-dark);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="mk-ps-root"
|
||||
data-composition-id="mk-progress-stat"
|
||||
data-start="0"
|
||||
data-duration="7"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="mk-ps-group">
|
||||
<div id="mk-ps-num">0</div>
|
||||
<div id="mk-ps-label"></div>
|
||||
<div id="mk-ps-track"><div id="mk-ps-fill"></div></div>
|
||||
<div id="mk-ps-caption"></div>
|
||||
</div>
|
||||
<div
|
||||
id="mk-ps-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="7"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
/* ---- published parameters (inspector-style CONFIG) ----
|
||||
A "22 · Goals reached" progress card: big count-up
|
||||
numeral, label, thin accent track filling to value/max, caption. */
|
||||
var CONFIG = {
|
||||
scheme: "light", // "light" | "dark"
|
||||
value: 22,
|
||||
suffix: "", // e.g. "%", "K"
|
||||
label: "Goals reached",
|
||||
caption: "Great job, we are getting closer!",
|
||||
max: 30, // track fills to value/max
|
||||
trackWidth: 480, // px
|
||||
x: 1300, // group left edge; null = centered
|
||||
y: 250, // group top edge; null = centered
|
||||
animationIn: true,
|
||||
animationOut: true,
|
||||
};
|
||||
|
||||
var DUR = 7;
|
||||
var root = document.getElementById("mk-ps-root");
|
||||
var group = document.getElementById("mk-ps-group");
|
||||
var num = document.getElementById("mk-ps-num");
|
||||
var fill = document.getElementById("mk-ps-fill");
|
||||
|
||||
if (CONFIG.scheme === "dark") root.classList.add("mk-dark");
|
||||
document.getElementById("mk-ps-label").textContent = CONFIG.label;
|
||||
document.getElementById("mk-ps-caption").textContent = CONFIG.caption;
|
||||
document.getElementById("mk-ps-track").style.width = CONFIG.trackWidth + "px";
|
||||
|
||||
/* position (measure after filling text) */
|
||||
var rect = group.getBoundingClientRect();
|
||||
group.style.left =
|
||||
(CONFIG.x === null ? Math.round((1920 - rect.width) / 2) : CONFIG.x) + "px";
|
||||
group.style.top =
|
||||
(CONFIG.y === null ? Math.round((1080 - rect.height) / 2) : CONFIG.y) + "px";
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
if (CONFIG.animationIn) {
|
||||
gsap.set(group, { opacity: 0, y: 28 });
|
||||
tl.to(group, { opacity: 1, y: 0, duration: 0.7, ease: "power3.out" }, 0.3);
|
||||
}
|
||||
|
||||
/* count-up + track fill on the same ease (the data-viz recipe) */
|
||||
var st = { v: 0 };
|
||||
tl.to(
|
||||
st,
|
||||
{
|
||||
v: CONFIG.value,
|
||||
duration: 1.6,
|
||||
ease: "power2.out",
|
||||
onUpdate: function () {
|
||||
num.textContent = Math.round(st.v) + CONFIG.suffix;
|
||||
},
|
||||
},
|
||||
0.5,
|
||||
);
|
||||
gsap.set(fill, { scaleX: 0 });
|
||||
tl.to(fill, { scaleX: CONFIG.value / CONFIG.max, duration: 1.6, ease: "power2.out" }, 0.5);
|
||||
|
||||
if (CONFIG.animationOut) {
|
||||
tl.to(group, { opacity: 0, y: -20, duration: 0.4, ease: "power2.in" }, DUR - 0.5);
|
||||
}
|
||||
tl.set(root, { visibility: "hidden" }, DUR - 0.02);
|
||||
|
||||
window.__timelines["mk-progress-stat"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `data-viz` `stat` `minimal`.
|
||||
|
||||
@@ -3,13 +3,13 @@ title: "Specs Checklist"
|
||||
description: "Left-aligned specs/traits checklist over footage or a board: rows slide in staggered with an accent underline sweep; light and dark schemes"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-specs-list.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-specs-list.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/mk-specs-list.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Specs Checklist preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add mk-specs-list
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add mk-specs-list" />
|
||||
|
||||
That writes one file: `compositions/mk-specs-list.html`.
|
||||
|
||||
@@ -32,6 +32,237 @@ It runs for 8 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`mk-specs-list.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
#mk-sl-root {
|
||||
/* ---- mk tokens: override in the host to re-skin the family ---- */
|
||||
--mk-font: "Inter", -apple-system, sans-serif;
|
||||
--mk-ink: #1d1d1f;
|
||||
--mk-ink-dim: #6e6e73;
|
||||
--mk-ink-dark: #f5f5f7;
|
||||
--mk-ink-dim-dark: #a1a1a6;
|
||||
--mk-accent: #0071e3;
|
||||
--mk-accent-dark: #7d5cff;
|
||||
--mk-hairline: 2px;
|
||||
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent; /* overlays footage or mk-background */
|
||||
}
|
||||
#mk-sl-scrim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0) 62%);
|
||||
opacity: 0;
|
||||
}
|
||||
#mk-sl-col {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mk-sl-row {
|
||||
font-family: var(--mk-font);
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
}
|
||||
.mk-sl-text {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mk-sl-label {
|
||||
font-weight: 600;
|
||||
font-size: 46px;
|
||||
letter-spacing: -0.015em;
|
||||
color: var(--mk-ink);
|
||||
}
|
||||
.mk-sl-value {
|
||||
font-weight: 400;
|
||||
font-size: 40px;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--mk-ink-dim);
|
||||
}
|
||||
.mk-sl-line {
|
||||
position: relative;
|
||||
margin-top: 22px;
|
||||
height: var(--mk-hairline);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mk-sl-line-fill {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--mk-accent);
|
||||
transform-origin: left center;
|
||||
}
|
||||
/* dark scheme: white rows for use over footage (overlay look) */
|
||||
.mk-dark .mk-sl-label {
|
||||
color: var(--mk-ink-dark);
|
||||
}
|
||||
.mk-dark .mk-sl-value {
|
||||
color: var(--mk-ink-dim-dark);
|
||||
}
|
||||
.mk-dark .mk-sl-line {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
.mk-dark .mk-sl-line-fill {
|
||||
background: var(--mk-accent-dark);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="mk-sl-root"
|
||||
data-composition-id="mk-specs-list"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="mk-sl-scrim"></div>
|
||||
<div id="mk-sl-col"></div>
|
||||
<div
|
||||
id="mk-sl-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
/* ---- published parameters (inspector-style CONFIG) ---- */
|
||||
var CONFIG = {
|
||||
scheme: "light", // "light" (ink rows) | "dark" (white rows, for footage)
|
||||
rows: [
|
||||
{ label: "Design", value: "Minimal, clean lines" },
|
||||
{ label: "Typography", value: "Inter SemiBold" },
|
||||
{ label: "Motion", value: "Restrained, no bounce" },
|
||||
{ label: "Color", value: "Stage + one accent" },
|
||||
{ label: "Shapes", value: "Capsules & rounded cards" },
|
||||
],
|
||||
underline: true, // accent sweep under each row
|
||||
lineWidth: 660, // px
|
||||
scrim: 0, // 0–1 left-edge dark scrim for readability over footage
|
||||
x: 160, // column left edge
|
||||
rowGap: 44, // px between rows
|
||||
stagger: 0.18, // s between row entrances
|
||||
animationIn: true,
|
||||
animationOut: true,
|
||||
};
|
||||
|
||||
var DUR = 8;
|
||||
var root = document.getElementById("mk-sl-root");
|
||||
var col = document.getElementById("mk-sl-col");
|
||||
var scrim = document.getElementById("mk-sl-scrim");
|
||||
|
||||
if (CONFIG.scheme === "dark") root.classList.add("mk-dark");
|
||||
scrim.style.opacity = String(CONFIG.scrim);
|
||||
|
||||
/* build rows */
|
||||
CONFIG.rows.forEach(function (r, i) {
|
||||
var row = document.createElement("div");
|
||||
row.id = "mk-sl-row-" + i;
|
||||
row.className = "mk-sl-row";
|
||||
if (i > 0) row.style.marginTop = CONFIG.rowGap + "px";
|
||||
|
||||
var text = document.createElement("div");
|
||||
text.className = "mk-sl-text";
|
||||
var label = document.createElement("span");
|
||||
label.className = "mk-sl-label";
|
||||
label.textContent = r.label;
|
||||
text.appendChild(label);
|
||||
if (r.value) {
|
||||
var value = document.createElement("span");
|
||||
value.className = "mk-sl-value";
|
||||
value.textContent = r.value;
|
||||
text.appendChild(value);
|
||||
}
|
||||
row.appendChild(text);
|
||||
|
||||
if (CONFIG.underline) {
|
||||
var line = document.createElement("div");
|
||||
line.className = "mk-sl-line";
|
||||
line.style.width = CONFIG.lineWidth + "px";
|
||||
var fill = document.createElement("div");
|
||||
fill.id = "mk-sl-fill-" + i;
|
||||
fill.className = "mk-sl-line-fill";
|
||||
line.appendChild(fill);
|
||||
row.appendChild(line);
|
||||
}
|
||||
col.appendChild(row);
|
||||
});
|
||||
|
||||
/* center the column vertically */
|
||||
var colH =
|
||||
CONFIG.rows.length * (46 + 22 + (CONFIG.underline ? 2 : 0)) +
|
||||
(CONFIG.rows.length - 1) * CONFIG.rowGap;
|
||||
col.style.left = CONFIG.x + "px";
|
||||
col.style.top = Math.round((1080 - colH) / 2) + "px";
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
CONFIG.rows.forEach(function (r, i) {
|
||||
var row = document.getElementById("mk-sl-row-" + i);
|
||||
var t0 = (CONFIG.animationIn ? 0.3 : 0) + i * CONFIG.stagger;
|
||||
|
||||
if (CONFIG.animationIn) {
|
||||
gsap.set(row, { opacity: 0, x: -36 });
|
||||
tl.to(row, { opacity: 1, x: 0, duration: 0.7, ease: "power3.out" }, t0);
|
||||
} else {
|
||||
gsap.set(row, { opacity: 1 });
|
||||
}
|
||||
|
||||
if (CONFIG.underline) {
|
||||
var fill = document.getElementById("mk-sl-fill-" + i);
|
||||
gsap.set(fill, { scaleX: 0 });
|
||||
tl.to(fill, { scaleX: 1, duration: 0.8, ease: "power2.inOut" }, t0 + 0.15);
|
||||
}
|
||||
});
|
||||
|
||||
/* exit + hard kill */
|
||||
if (CONFIG.animationOut) {
|
||||
tl.to(col, { opacity: 0, y: -20, duration: 0.4, ease: "power2.in" }, DUR - 0.5);
|
||||
tl.to(scrim, { opacity: 0, duration: 0.4, ease: "power2.in" }, DUR - 0.5);
|
||||
}
|
||||
tl.set(root, { visibility: "hidden" }, DUR - 0.02);
|
||||
|
||||
window.__timelines["mk-specs-list"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `data-viz` `list` `minimal` `professional`.
|
||||
|
||||
@@ -3,34 +3,440 @@ title: "News Ticker"
|
||||
description: "Premium broadcast-style lower-third ticker with live label, headline ribbon, and scrolling news crawl."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/news-ticker.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/news-ticker.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/news-ticker.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="News Ticker preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add news-ticker
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add news-ticker" />
|
||||
|
||||
That writes one file: `compositions/news-ticker.html`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 7 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`news-ticker.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>News Ticker</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
#nt-root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
color: #f8fafc;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#nt-root .nt-shell {
|
||||
position: absolute;
|
||||
left: 96px;
|
||||
right: 96px;
|
||||
bottom: 72px;
|
||||
height: 216px;
|
||||
filter: drop-shadow(0 28px 44px rgba(0, 0, 0, 0.32));
|
||||
}
|
||||
|
||||
#nt-root .nt-panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 28px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(21, 31, 48, 0.96), rgba(7, 13, 24, 0.98)),
|
||||
linear-gradient(90deg, rgba(201, 31, 45, 0.72), transparent 38%);
|
||||
}
|
||||
|
||||
#nt-root .nt-panel::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
content: "";
|
||||
background:
|
||||
radial-gradient(circle at 14% 0%, rgba(255, 255, 255, 0.2), transparent 26%),
|
||||
linear-gradient(
|
||||
110deg,
|
||||
transparent 0 42%,
|
||||
rgba(255, 255, 255, 0.06) 42% 43%,
|
||||
transparent 43% 100%
|
||||
);
|
||||
opacity: 0.72;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nt-root .nt-top-row {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 126px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
#nt-root .nt-brand {
|
||||
width: 294px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
background: linear-gradient(135deg, #e11d2e, #9f1220);
|
||||
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
#nt-root .nt-live-dot {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
#nt-root .nt-brand-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#nt-root .nt-live {
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
|
||||
#nt-root .nt-desk {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.22em;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
#nt-root .nt-headline-wrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 250px 0 44px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#nt-root .nt-headline {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 48px;
|
||||
font-weight: 850;
|
||||
line-height: 1.02;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 2px 14px rgba(0, 0, 0, 0.34);
|
||||
}
|
||||
|
||||
#nt-root .nt-headline-accent {
|
||||
color: #ff4455;
|
||||
}
|
||||
|
||||
#nt-root .nt-time {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
right: 38px;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.74);
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.08em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
#nt-root .nt-bottom-row {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 90px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.11);
|
||||
background: rgba(2, 6, 13, 0.58);
|
||||
}
|
||||
|
||||
#nt-root .nt-label {
|
||||
width: 294px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.14);
|
||||
background: #5f0712;
|
||||
color: #ffffff;
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#nt-root .nt-ticker-window {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #050b15;
|
||||
}
|
||||
|
||||
#nt-root .nt-ticker-window::before,
|
||||
#nt-root .nt-ticker-window::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
width: 90px;
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nt-root .nt-ticker-window::before {
|
||||
left: 0;
|
||||
background: linear-gradient(90deg, #050b15, transparent);
|
||||
}
|
||||
|
||||
#nt-root .nt-ticker-window::after {
|
||||
right: 0;
|
||||
background: linear-gradient(270deg, #050b15, transparent);
|
||||
}
|
||||
|
||||
#nt-root .nt-ticker-track {
|
||||
position: absolute;
|
||||
left: 34px;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 34px;
|
||||
white-space: nowrap;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
#nt-root .nt-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 13px 18px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 999px;
|
||||
background: #08111f;
|
||||
color: #ffffff;
|
||||
font-size: 31px;
|
||||
font-weight: 650;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
#nt-root .nt-separator {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 999px;
|
||||
background: #e11d2e;
|
||||
box-shadow: 0 0 22px rgba(225, 29, 46, 0.75);
|
||||
}
|
||||
|
||||
#nt-root .nt-metric {
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
background: #063344;
|
||||
color: #c8f8ff;
|
||||
font-weight: 850;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
#nt-root .nt-flash {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -260px;
|
||||
z-index: 3;
|
||||
width: 210px;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.18), transparent);
|
||||
transform: skewX(-16deg);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="nt-root"
|
||||
data-composition-id="news-ticker"
|
||||
data-composition-src="compositions/news-ticker.html"
|
||||
data-start="0"
|
||||
data-duration="7"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
data-start="0"
|
||||
data-duration="7"
|
||||
>
|
||||
<section
|
||||
id="nt-shell"
|
||||
class="clip nt-shell"
|
||||
data-start="0"
|
||||
data-duration="7"
|
||||
data-track-index="0"
|
||||
>
|
||||
<div class="nt-panel">
|
||||
<div id="nt-flash" class="nt-flash"></div>
|
||||
<div class="nt-top-row">
|
||||
<div id="nt-brand" class="nt-brand">
|
||||
<div id="nt-live-dot" class="nt-live-dot"></div>
|
||||
<div class="nt-brand-text">
|
||||
<div class="nt-live">Live</div>
|
||||
<div class="nt-desk">Global Desk</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nt-headline-wrap">
|
||||
<h1 id="nt-headline" class="nt-headline">
|
||||
AI video tools move from <span class="nt-headline-accent">prototype</span> to
|
||||
production
|
||||
</h1>
|
||||
<div id="nt-time" class="nt-time">09:41 UTC</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nt-bottom-row">
|
||||
<div id="nt-label" class="nt-label">Breaking</div>
|
||||
<div class="nt-ticker-window">
|
||||
<div id="nt-ticker-track" class="nt-ticker-track">
|
||||
<span class="nt-item"
|
||||
><span class="nt-separator"></span>Creator teams shorten launch cycles by
|
||||
<span class="nt-metric">38%</span></span
|
||||
>
|
||||
<span class="nt-item"
|
||||
><span class="nt-separator"></span>Rendering queues clear in under
|
||||
<span class="nt-metric">2 min</span></span
|
||||
>
|
||||
<span class="nt-item"
|
||||
><span class="nt-separator"></span>New catalog blocks trending across product
|
||||
demos</span
|
||||
>
|
||||
<span class="nt-item"
|
||||
><span class="nt-separator"></span>Studio editors adopt HTML-first motion
|
||||
systems</span
|
||||
>
|
||||
<span class="nt-item"
|
||||
><span class="nt-separator"></span>Creator teams shorten launch cycles by
|
||||
<span class="nt-metric">38%</span></span
|
||||
>
|
||||
<span class="nt-item"
|
||||
><span class="nt-separator"></span>Rendering queues clear in under
|
||||
<span class="nt-metric">2 min</span></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
var shell = document.getElementById("nt-shell");
|
||||
var brand = document.getElementById("nt-brand");
|
||||
var headline = document.getElementById("nt-headline");
|
||||
var label = document.getElementById("nt-label");
|
||||
var tickerTrack = document.getElementById("nt-ticker-track");
|
||||
var liveDot = document.getElementById("nt-live-dot");
|
||||
var flash = document.getElementById("nt-flash");
|
||||
var time = document.getElementById("nt-time");
|
||||
|
||||
gsap.set(shell, { y: 260, opacity: 0 });
|
||||
gsap.set(brand, { x: -80, opacity: 0 });
|
||||
gsap.set(headline, { y: 26, opacity: 0, clipPath: "inset(0 100% 0 0)" });
|
||||
gsap.set(label, { y: 18, opacity: 0 });
|
||||
gsap.set(time, { y: -10, opacity: 0 });
|
||||
gsap.set(tickerTrack, { x: 1260 });
|
||||
|
||||
tl.to(shell, { y: 0, opacity: 1, duration: 0.52, ease: "power4.out" }, 0);
|
||||
tl.to(brand, { x: 0, opacity: 1, duration: 0.42, ease: "power3.out" }, 0.08);
|
||||
tl.to(
|
||||
headline,
|
||||
{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
clipPath: "inset(0 0% 0 0)",
|
||||
duration: 0.62,
|
||||
ease: "power3.out",
|
||||
},
|
||||
0.18,
|
||||
);
|
||||
tl.to(time, { y: 0, opacity: 1, duration: 0.32, ease: "power2.out" }, 0.5);
|
||||
tl.to(label, { y: 0, opacity: 1, duration: 0.28, ease: "power2.out" }, 0.55);
|
||||
tl.to(flash, { x: 2040, duration: 0.86, ease: "power2.inOut" }, 0.34);
|
||||
tl.to(
|
||||
tickerTrack,
|
||||
{
|
||||
x: -1120,
|
||||
duration: 6.05,
|
||||
ease: "none",
|
||||
},
|
||||
0.62,
|
||||
);
|
||||
tl.to(
|
||||
liveDot,
|
||||
{
|
||||
scale: 1.32,
|
||||
boxShadow: "0 0 0 18px rgba(255,255,255,0)",
|
||||
duration: 0.42,
|
||||
repeat: 11,
|
||||
yoyo: true,
|
||||
ease: "sine.inOut",
|
||||
},
|
||||
0.58,
|
||||
);
|
||||
tl.to(flash, { x: 4080, duration: 0.72, ease: "power2.inOut" }, 3.42);
|
||||
tl.to(shell, { y: 170, opacity: 0, duration: 0.34, ease: "power3.in" }, 6.62);
|
||||
|
||||
window.__timelines["news-ticker"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="news-ticker" data-composition-src="compositions/news-ticker.html" data-start="0" data-duration="7" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,337 @@ title: "North Korea Locked Down"
|
||||
description: "Realistic map zoom into North Korea with a red scribble circle, locked-down pop-up label, and reddish editorial wash."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/north-korea-locked-down.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/north-korea-locked-down.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/north-korea-locked-down.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="North Korea Locked Down preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add north-korea-locked-down
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add north-korea-locked-down" />
|
||||
|
||||
That writes `compositions/north-korea-locked-down.html`, plus 1 supporting file under `assets/`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 7 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`north-korea-locked-down.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #eef3f4;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #eef3f4;
|
||||
color: #151515;
|
||||
}
|
||||
|
||||
.camera {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
transform-origin: 50% 51%;
|
||||
will-change: transform, filter;
|
||||
}
|
||||
|
||||
.map {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.paper-depth {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at 52% 50%, rgba(255, 255, 255, 0.08), transparent 28%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(10, 20, 24, 0.12));
|
||||
mix-blend-mode: multiply;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.annotation {
|
||||
position: absolute;
|
||||
left: 652px;
|
||||
top: 235px;
|
||||
z-index: 4;
|
||||
width: 560px;
|
||||
height: 392px;
|
||||
overflow: visible;
|
||||
transform-origin: center;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.scribble {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 560px;
|
||||
height: 392px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.scribble path {
|
||||
fill: none;
|
||||
stroke: #e21d2f;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
filter: drop-shadow(0 8px 12px rgba(226, 29, 47, 0.24));
|
||||
}
|
||||
|
||||
#circle-a {
|
||||
stroke-width: 12;
|
||||
stroke-dasharray: 980;
|
||||
stroke-dashoffset: 980;
|
||||
}
|
||||
|
||||
#circle-b {
|
||||
stroke-width: 6;
|
||||
opacity: 0.82;
|
||||
stroke-dasharray: 910;
|
||||
stroke-dashoffset: 910;
|
||||
}
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: -146px;
|
||||
transform: translateX(-50%) scale(0.82);
|
||||
transform-origin: 50% 100%;
|
||||
min-width: 370px;
|
||||
padding: 18px 26px 20px;
|
||||
border-radius: 18px;
|
||||
color: #ffffff;
|
||||
background: #111111;
|
||||
box-shadow:
|
||||
0 22px 54px rgba(25, 4, 7, 0.24),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
|
||||
font-size: 60px;
|
||||
line-height: 0.9;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.label::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: #111111;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.red-wash {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 50% 48%,
|
||||
rgba(255, 59, 48, 0.3),
|
||||
rgba(255, 59, 48, 0.06) 42%,
|
||||
rgba(79, 0, 8, 0.22) 100%
|
||||
),
|
||||
linear-gradient(90deg, rgba(99, 0, 12, 0.12), rgba(255, 59, 48, 0.2));
|
||||
opacity: 0;
|
||||
mix-blend-mode: multiply;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scanline {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
opacity: 0;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(65, 0, 0, 0.12) 0,
|
||||
rgba(65, 0, 0, 0.12) 1px,
|
||||
transparent 1px,
|
||||
transparent 7px
|
||||
);
|
||||
mix-blend-mode: multiply;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.corner {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
border-color: rgba(17, 17, 17, 0.62);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.corner.tl {
|
||||
left: 86px;
|
||||
top: 78px;
|
||||
border-top: 5px solid;
|
||||
border-left: 5px solid;
|
||||
}
|
||||
|
||||
.corner.tr {
|
||||
right: 86px;
|
||||
top: 78px;
|
||||
border-top: 5px solid;
|
||||
border-right: 5px solid;
|
||||
}
|
||||
|
||||
.corner.bl {
|
||||
left: 86px;
|
||||
bottom: 78px;
|
||||
border-bottom: 5px solid;
|
||||
border-left: 5px solid;
|
||||
}
|
||||
|
||||
.corner.br {
|
||||
right: 86px;
|
||||
bottom: 78px;
|
||||
border-bottom: 5px solid;
|
||||
border-right: 5px solid;
|
||||
}
|
||||
|
||||
.attribution {
|
||||
position: absolute;
|
||||
right: 28px;
|
||||
bottom: 22px;
|
||||
z-index: 7;
|
||||
color: rgba(17, 17, 17, 0.44);
|
||||
font-size: 19px;
|
||||
font-weight: 650;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="north-korea-locked-down"
|
||||
data-composition-src="compositions/north-korea-locked-down.html"
|
||||
data-root="true"
|
||||
data-start="0"
|
||||
data-duration="7"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
>
|
||||
<div id="camera" class="camera">
|
||||
<img class="map" src="assets/korea-map.png" alt="" />
|
||||
<div class="paper-depth"></div>
|
||||
</div>
|
||||
|
||||
<div id="annotation" class="annotation">
|
||||
<div id="label" class="label">LOCKED<br />DOWN</div>
|
||||
<svg class="scribble" viewBox="0 0 560 392" aria-hidden="true" data-layout-ignore>
|
||||
<path
|
||||
id="circle-a"
|
||||
d="M 74 211 C 42 119 118 42 254 36 C 410 30 524 111 508 222 C 494 320 350 377 208 346 C 94 322 37 278 74 211"
|
||||
/>
|
||||
<path
|
||||
id="circle-b"
|
||||
d="M 92 231 C 33 150 105 55 243 44 C 408 31 529 126 501 244 C 480 334 335 370 197 342 C 83 319 47 269 92 231"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div id="redWash" class="red-wash"></div>
|
||||
<div id="scanline" class="scanline"></div>
|
||||
<div class="corner tl"></div>
|
||||
<div class="corner tr"></div>
|
||||
<div class="corner bl"></div>
|
||||
<div class="corner br"></div>
|
||||
<div class="attribution">© OpenStreetMap contributors © CARTO</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const tl = gsap.timeline({
|
||||
paused: true,
|
||||
defaults: { overwrite: "auto" },
|
||||
});
|
||||
|
||||
tl.fromTo(
|
||||
"#camera",
|
||||
{ scale: 0.92, x: -70, y: 18, filter: "saturate(0.92) contrast(0.98)" },
|
||||
{
|
||||
scale: 2.12,
|
||||
x: -56,
|
||||
y: -22,
|
||||
filter: "saturate(1.08) contrast(1.04)",
|
||||
duration: 3.18,
|
||||
ease: "expo.inOut",
|
||||
},
|
||||
0,
|
||||
);
|
||||
tl.to("#camera", { scale: 2.2, x: -44, y: -34, duration: 0.42, ease: "back.out(1.5)" }, 3.18);
|
||||
tl.from(
|
||||
".corner",
|
||||
{ opacity: 0, scale: 0.72, duration: 0.28, stagger: 0.035, ease: "power3.out" },
|
||||
0.42,
|
||||
);
|
||||
tl.to(".corner", { opacity: 0.72, duration: 0.18, ease: "power2.out" }, 1.2);
|
||||
tl.fromTo(
|
||||
"#annotation",
|
||||
{ opacity: 0, scale: 0.92, x: 4, y: -4 },
|
||||
{ opacity: 1, scale: 1, x: 0, y: 0, duration: 0.16, ease: "power2.out" },
|
||||
3.12,
|
||||
);
|
||||
tl.to("#circle-a", { strokeDashoffset: 0, duration: 0.62, ease: "power2.out" }, 3.24);
|
||||
tl.to("#circle-b", { strokeDashoffset: 0, duration: 0.44, ease: "power3.out" }, 3.46);
|
||||
tl.to("#label", { opacity: 1, scale: 1, duration: 0.28, ease: "back.out(2.1)" }, 3.78);
|
||||
tl.to("#redWash", { opacity: 1, duration: 0.24, ease: "power2.out" }, 3.82);
|
||||
tl.to("#scanline", { opacity: 0.44, duration: 0.18, ease: "none" }, 3.86);
|
||||
tl.to(
|
||||
"#annotation",
|
||||
{ scale: 1.045, duration: 0.14, yoyo: true, repeat: 1, ease: "sine.inOut" },
|
||||
4.1,
|
||||
);
|
||||
tl.to("#camera", { scale: 2.06, x: -82, y: -8, duration: 2.35, ease: "sine.inOut" }, 4.2);
|
||||
tl.to("#redWash", { opacity: 0.82, duration: 2.35, ease: "sine.inOut" }, 4.2);
|
||||
tl.to(".corner", { opacity: 0.38, duration: 0.72, ease: "sine.inOut" }, 5.82);
|
||||
tl.to("#label", { y: -6, duration: 0.4, yoyo: true, repeat: 1, ease: "sine.inOut" }, 5.94);
|
||||
|
||||
window.__timelines["north-korea-locked-down"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="north-korea-locked-down" data-composition-src="compositions/north-korea-locked-down.html" data-start="0" data-duration="7" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,34 +3,557 @@ title: "NYC Paris Flight"
|
||||
description: "Apple-style realistic map animation with a plane flying from New York to Paris, marker circle, landing pop, and sound effects."
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/nyc-paris-flight.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/nyc-paris-flight.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/nyc-paris-flight.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="NYC Paris Flight preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add nyc-paris-flight
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add nyc-paris-flight" />
|
||||
|
||||
That writes `compositions/nyc-paris-flight.html`, plus 2 supporting files under `assets/`.
|
||||
|
||||
## Add it to your video
|
||||
## Source
|
||||
|
||||
It runs for 6 seconds at 1920×1080. Paste this into your composition:
|
||||
<Accordion title={`nyc-paris-flight.html`}>
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #f5f5f7;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #f5f5f7;
|
||||
color: #1d1d1f;
|
||||
}
|
||||
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #f5f5f7;
|
||||
}
|
||||
|
||||
.map-plane {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform-origin: 52% 48%;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.map-image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.soft-ocean {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at 50% 24%, rgba(0, 113, 227, 0.18), rgba(0, 113, 227, 0) 34%),
|
||||
linear-gradient(180deg, rgba(245, 245, 247, 0.05), rgba(245, 245, 247, 0.3));
|
||||
mix-blend-mode: multiply;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.route-svg,
|
||||
.doodle-svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.route-glow {
|
||||
fill: none;
|
||||
stroke: rgba(0, 113, 227, 0.23);
|
||||
stroke-width: 24;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 2000;
|
||||
stroke-dashoffset: 2000;
|
||||
filter: blur(4px);
|
||||
}
|
||||
|
||||
.route-path {
|
||||
fill: none;
|
||||
stroke: #0071e3;
|
||||
stroke-width: 9;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 2000;
|
||||
stroke-dashoffset: 2000;
|
||||
filter: drop-shadow(0 10px 20px rgba(0, 113, 227, 0.22));
|
||||
}
|
||||
|
||||
.route-thread {
|
||||
fill: none;
|
||||
stroke: rgba(255, 255, 255, 0.86);
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 2000;
|
||||
stroke-dashoffset: 2000;
|
||||
}
|
||||
|
||||
.route-tick {
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: -7px 0 0 -7px;
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
border: 3px solid #0071e3;
|
||||
box-shadow: 0 8px 26px rgba(0, 113, 227, 0.32);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pin {
|
||||
position: absolute;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
border: 7px solid #0071e3;
|
||||
box-shadow:
|
||||
0 15px 34px rgba(0, 0, 0, 0.18),
|
||||
0 0 0 10px rgba(0, 113, 227, 0.12);
|
||||
transform: translate(-50%, -50%);
|
||||
transform-origin: center;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.pin::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -20px;
|
||||
border-radius: 999px;
|
||||
border: 2px solid rgba(0, 113, 227, 0.25);
|
||||
}
|
||||
|
||||
.pin-label {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
min-width: 142px;
|
||||
padding: 12px 16px 13px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
border: 1px solid rgba(255, 255, 255, 0.88);
|
||||
color: #1d1d1f;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
box-shadow: 0 18px 38px rgba(29, 29, 31, 0.16);
|
||||
backdrop-filter: blur(18px);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pin-label span {
|
||||
display: block;
|
||||
margin-top: 7px;
|
||||
color: #6e6e73;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#nyc-pin {
|
||||
left: 308.41px;
|
||||
top: 584.16px;
|
||||
}
|
||||
|
||||
#nyc-pin .pin-label {
|
||||
left: 28px;
|
||||
}
|
||||
|
||||
#paris-pin {
|
||||
left: 1611.59px;
|
||||
top: 387.84px;
|
||||
}
|
||||
|
||||
#paris-pin .pin-label {
|
||||
right: 28px;
|
||||
}
|
||||
|
||||
.plane {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
offset-path: path("M 308.41 584.158 C 650 520 1120 245 1611.59 387.842");
|
||||
offset-distance: 0%;
|
||||
offset-rotate: auto;
|
||||
offset-anchor: 50% 50%;
|
||||
transform-origin: center;
|
||||
filter: drop-shadow(0 22px 22px rgba(0, 0, 0, 0.24));
|
||||
opacity: 1;
|
||||
will-change: offset-distance, transform, opacity;
|
||||
}
|
||||
|
||||
.plane-core {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform-origin: center;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.plane svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.doodle-line {
|
||||
fill: none;
|
||||
stroke: #ff3b30;
|
||||
stroke-width: 13;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-dasharray: 900;
|
||||
stroke-dashoffset: 900;
|
||||
filter: drop-shadow(0 10px 14px rgba(255, 59, 48, 0.18));
|
||||
}
|
||||
|
||||
.doodle-line.secondary {
|
||||
stroke-width: 6;
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.paris-pulse {
|
||||
position: absolute;
|
||||
left: 1611.59px;
|
||||
top: 387.84px;
|
||||
width: 126px;
|
||||
height: 126px;
|
||||
margin: -63px 0 0 -63px;
|
||||
border-radius: 999px;
|
||||
border: 4px solid rgba(255, 59, 48, 0.34);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.landing-badge {
|
||||
position: absolute;
|
||||
left: 1500px;
|
||||
top: 502px;
|
||||
padding: 15px 20px 16px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #d70015;
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
box-shadow: 0 20px 44px rgba(29, 29, 31, 0.14);
|
||||
opacity: 0;
|
||||
transform: scale(0.78);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.title-stack {
|
||||
position: absolute;
|
||||
left: 82px;
|
||||
top: 70px;
|
||||
z-index: 6;
|
||||
max-width: 780px;
|
||||
padding: 30px 34px 33px;
|
||||
border-radius: 30px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid rgba(255, 255, 255, 0.86);
|
||||
box-shadow: 0 28px 70px rgba(29, 29, 31, 0.12);
|
||||
backdrop-filter: blur(22px);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
color: #6e6e73;
|
||||
font-size: 19px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.headline {
|
||||
margin-top: 11px;
|
||||
color: #1d1d1f;
|
||||
font-size: 84px;
|
||||
font-weight: 900;
|
||||
line-height: 0.92;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.headline span {
|
||||
color: #0071e3;
|
||||
}
|
||||
|
||||
.route-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-top: 18px;
|
||||
color: #6e6e73;
|
||||
font-size: 25px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.route-meta i {
|
||||
display: block;
|
||||
width: 54px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: #0071e3;
|
||||
}
|
||||
|
||||
.attribution {
|
||||
position: absolute;
|
||||
right: 28px;
|
||||
bottom: 24px;
|
||||
z-index: 5;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
color: rgba(29, 29, 31, 0.76);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.white-screen {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 30;
|
||||
background: #ffffff;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="nyc-paris-flight"
|
||||
data-composition-src="compositions/nyc-paris-flight.html"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div
|
||||
id="flight-scene"
|
||||
class="clip stage"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="0"
|
||||
>
|
||||
<div id="map-plane" class="map-plane" data-layout-allow-overflow>
|
||||
<img class="map-image" src="assets/map-nyc-paris.png" alt="" />
|
||||
<div class="soft-ocean" data-layout-ignore></div>
|
||||
|
||||
<svg class="route-svg" viewBox="0 0 1920 1080" aria-hidden="true" data-layout-ignore>
|
||||
<path
|
||||
id="route-glow"
|
||||
class="route-glow"
|
||||
d="M 308.41 584.158 C 650 520 1120 245 1611.59 387.842"
|
||||
/>
|
||||
<path
|
||||
id="route-path"
|
||||
class="route-path"
|
||||
d="M 308.41 584.158 C 650 520 1120 245 1611.59 387.842"
|
||||
/>
|
||||
<path
|
||||
id="route-thread"
|
||||
class="route-thread"
|
||||
d="M 308.41 584.158 C 650 520 1120 245 1611.59 387.842"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div class="route-tick" style="left: 481.8px; top: 539.7px"></div>
|
||||
<div class="route-tick" style="left: 647.6px; top: 486.5px"></div>
|
||||
<div class="route-tick" style="left: 824.8px; top: 430.6px"></div>
|
||||
<div class="route-tick" style="left: 1011.5px; top: 382.4px"></div>
|
||||
<div class="route-tick" style="left: 1206.1px; top: 352.3px"></div>
|
||||
<div class="route-tick" style="left: 1406.7px; top: 350.7px"></div>
|
||||
|
||||
<div id="nyc-pin" class="pin">
|
||||
<div class="pin-label">New York<span>JFK / NYC</span></div>
|
||||
</div>
|
||||
|
||||
<div id="paris-pin" class="pin">
|
||||
<div class="pin-label">Paris<span>CDG / FR</span></div>
|
||||
</div>
|
||||
|
||||
<div id="plane" class="plane" aria-hidden="true">
|
||||
<div id="plane-core" class="plane-core">
|
||||
<svg viewBox="-64 -64 128 128">
|
||||
<path
|
||||
d="M 58 0 C 52 -10 37 -15 15 -16 L -18 -56 C -23 -62 -34 -58 -32 -49 L -21 -15 L -48 -12 C -56 -11 -60 -6 -60 0 C -60 6 -56 11 -48 12 L -21 15 L -32 49 C -34 58 -23 62 -18 56 L 15 16 C 37 15 52 10 58 0 Z"
|
||||
fill="#ffffff"
|
||||
/>
|
||||
<path
|
||||
d="M 54 0 C 46 -6 33 -9 14 -10 L -19 -50 C -21 -53 -25 -52 -25 -47 L -15 -10 L -49 -6 C -53 -5 -55 -2 -55 0 C -55 2 -53 5 -49 6 L -15 10 L -25 47 C -25 52 -21 53 -19 50 L 14 10 C 33 9 46 6 54 0 Z"
|
||||
fill="#0071e3"
|
||||
/>
|
||||
<circle cx="18" cy="0" r="6" fill="#f5f5f7" opacity="0.95" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svg class="doodle-svg" viewBox="0 0 1920 1080" aria-hidden="true" data-layout-ignore>
|
||||
<path
|
||||
id="doodle-a"
|
||||
class="doodle-line"
|
||||
d="M 1510 382 C 1505 323 1560 287 1627 300 C 1700 315 1740 358 1720 416 C 1705 468 1624 486 1556 462 C 1518 449 1494 417 1510 382"
|
||||
/>
|
||||
<path
|
||||
id="doodle-b"
|
||||
class="doodle-line secondary"
|
||||
d="M 1518 396 C 1488 356 1528 307 1605 296 C 1688 284 1748 338 1730 397 C 1713 453 1641 480 1570 463 C 1526 453 1504 427 1518 396"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div id="paris-pulse" class="paris-pulse"></div>
|
||||
<div id="landing-badge" class="landing-badge">ARRIVED</div>
|
||||
</div>
|
||||
|
||||
<div class="title-stack">
|
||||
<div class="eyebrow">Six-second transatlantic hop</div>
|
||||
<div class="headline">NEW YORK <span>TO</span> PARIS</div>
|
||||
<div class="route-meta">
|
||||
<i></i>
|
||||
<p>5,837 km compressed</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="attribution">© OpenStreetMap contributors © CARTO</div>
|
||||
<div id="white-screen" class="white-screen"></div>
|
||||
</div>
|
||||
|
||||
<audio
|
||||
id="sfx"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
src="assets/sfx-mix.wav"
|
||||
data-volume="0.95"
|
||||
></audio>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
const tl = gsap.timeline({
|
||||
paused: true,
|
||||
defaults: { overwrite: "auto" },
|
||||
});
|
||||
|
||||
tl.from(
|
||||
"#map-plane",
|
||||
{ scale: 1.1, x: -38, y: 20, opacity: 0.94, duration: 1.05, ease: "power2.out" },
|
||||
0.16,
|
||||
);
|
||||
tl.from(
|
||||
".title-stack",
|
||||
{ y: 28, scale: 0.97, opacity: 0, duration: 0.52, ease: "expo.out" },
|
||||
0.24,
|
||||
);
|
||||
tl.from(".eyebrow", { x: -22, opacity: 0, duration: 0.36, ease: "power3.out" }, 0.32);
|
||||
tl.from(".headline", { y: 34, opacity: 0, duration: 0.5, ease: "back.out(1.45)" }, 0.4);
|
||||
tl.from(".route-meta", { x: -26, opacity: 0, duration: 0.38, ease: "circ.out" }, 0.58);
|
||||
|
||||
tl.from("#nyc-pin", { scale: 0.12, opacity: 0, duration: 0.34, ease: "back.out(2.8)" }, 0.7);
|
||||
tl.from(
|
||||
"#paris-pin",
|
||||
{ scale: 0.12, opacity: 0, duration: 0.34, ease: "elastic.out(1, 0.5)" },
|
||||
0.86,
|
||||
);
|
||||
|
||||
tl.from("#plane", { scale: 0.48, opacity: 0, duration: 0.3, ease: "back.out(2.2)" }, 1.08);
|
||||
tl.to("#route-glow", { strokeDashoffset: 0, duration: 3.1, ease: "power1.inOut" }, 1.16);
|
||||
tl.to("#route-path", { strokeDashoffset: 0, duration: 3.05, ease: "power2.inOut" }, 1.2);
|
||||
tl.to("#route-thread", { strokeDashoffset: 0, duration: 2.92, ease: "sine.inOut" }, 1.26);
|
||||
tl.to("#plane", { offsetDistance: "100%", duration: 3.25, ease: "sine.inOut" }, 1.17);
|
||||
tl.to(
|
||||
"#plane-core",
|
||||
{ scale: 1.06, duration: 0.2, repeat: 12, yoyo: true, ease: "sine.inOut" },
|
||||
1.22,
|
||||
);
|
||||
tl.to("#map-plane", { scale: 1.025, duration: 3.2, ease: "sine.inOut" }, 1.28);
|
||||
|
||||
gsap.utils.toArray(".route-tick").forEach((tick, index) => {
|
||||
tl.fromTo(
|
||||
tick,
|
||||
{ scale: 0.2, opacity: 0 },
|
||||
{ scale: 1, opacity: 1, duration: 0.16, ease: "back.out(3)" },
|
||||
1.34 + index * 0.44,
|
||||
);
|
||||
tl.to(
|
||||
tick,
|
||||
{ scale: 0.6, opacity: 0.18, duration: 0.34, ease: "power2.out" },
|
||||
1.48 + index * 0.44,
|
||||
);
|
||||
});
|
||||
|
||||
tl.from(
|
||||
"#paris-pulse",
|
||||
{ scale: 0.52, opacity: 0, duration: 0.26, ease: "back.out(2.4)" },
|
||||
4.06,
|
||||
);
|
||||
tl.to("#doodle-a", { strokeDashoffset: 0, duration: 0.54, ease: "power2.out" }, 4.18);
|
||||
tl.to("#doodle-b", { strokeDashoffset: 0, duration: 0.36, ease: "power3.out" }, 4.36);
|
||||
tl.to("#paris-pulse", { scale: 1.5, opacity: 0, duration: 0.58, ease: "power3.out" }, 4.64);
|
||||
|
||||
tl.to("#plane-core", { scale: 1.23, duration: 0.16, ease: "back.out(3)" }, 4.96);
|
||||
tl.to("#plane-core", { scale: 0.9, duration: 0.12, ease: "power2.in" }, 5.12);
|
||||
tl.to("#plane-core", { scale: 1, duration: 0.22, ease: "elastic.out(1, 0.45)" }, 5.24);
|
||||
tl.to(
|
||||
"#landing-badge",
|
||||
{ scale: 1, opacity: 1, duration: 0.28, ease: "back.out(2.4)" },
|
||||
5.02,
|
||||
);
|
||||
tl.to(
|
||||
"#landing-badge",
|
||||
{ scale: 1.06, duration: 0.16, yoyo: true, repeat: 1, ease: "sine.inOut" },
|
||||
5.3,
|
||||
);
|
||||
|
||||
tl.set("#white-screen", { opacity: 1 }, 5.5);
|
||||
|
||||
window.__timelines["nyc-paris-flight"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="nyc-paris-flight" data-composition-src="compositions/nyc-paris-flight.html" data-start="0" data-duration="6" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ title: "Organic Light Leak Overlay"
|
||||
description: "Finite CSS light-leak overlay for memory beats and motivated transitions"
|
||||
---
|
||||
|
||||
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/organic-light-leak-overlay.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/organic-light-leak-overlay.png" autoPlay muted loop playsInline />
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe src="/public/catalog/preview/blocks/organic-light-leak-overlay.html" className="w-full aspect-video rounded-xl border border-zinc-200 dark:border-zinc-800" loading="lazy" title="Organic Light Leak Overlay preview" />
|
||||
|
||||
## Install
|
||||
|
||||
```bash Terminal
|
||||
npx hyperframes add organic-light-leak-overlay
|
||||
```
|
||||
<InstallCommand command="npx hyperframes add organic-light-leak-overlay" />
|
||||
|
||||
That writes one file: `compositions/organic-light-leak-overlay.html`.
|
||||
|
||||
@@ -32,6 +32,140 @@ It runs for 4 seconds at 1920×1080. Paste this into your composition:
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`organic-light-leak-overlay.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#hf-light-leak {
|
||||
position: absolute;
|
||||
inset: -16%;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
#hf-light-leak .hf-leak-layer {
|
||||
position: absolute;
|
||||
inset: -20%;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
#hf-light-leak .hf-leak-amber {
|
||||
background:
|
||||
radial-gradient(ellipse 52% 110% at 10% 58%, rgb(255 241 197 / 92%), transparent 45%),
|
||||
radial-gradient(ellipse 72% 120% at 24% 54%, rgb(255 126 57 / 78%), transparent 66%);
|
||||
filter: blur(2.8vmin);
|
||||
}
|
||||
|
||||
#hf-light-leak .hf-leak-red {
|
||||
background: radial-gradient(
|
||||
ellipse 58% 105% at 78% 34%,
|
||||
rgb(255 79 61 / 72%),
|
||||
rgb(255 44 92 / 28%) 48%,
|
||||
transparent 76%
|
||||
);
|
||||
filter: blur(4vmin);
|
||||
}
|
||||
|
||||
#hf-light-leak .hf-leak-gold {
|
||||
background: linear-gradient(
|
||||
104deg,
|
||||
transparent 24%,
|
||||
rgb(255 144 62 / 30%) 40%,
|
||||
rgb(255 247 205 / 72%) 51%,
|
||||
rgb(255 124 48 / 22%) 62%,
|
||||
transparent 78%
|
||||
);
|
||||
filter: blur(2vmin);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="organic-light-leak-overlay"
|
||||
data-composition-id="organic-light-leak-overlay"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="hf-light-leak" aria-hidden="true">
|
||||
<div class="hf-leak-layer hf-leak-amber"></div>
|
||||
<div class="hf-leak-layer hf-leak-red"></div>
|
||||
<div class="hf-leak-layer hf-leak-gold"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
var leak = document.getElementById("hf-light-leak");
|
||||
var host = leak && leak.closest("[data-composition-id]");
|
||||
if (!host) throw new Error("Organic Light Leak could not find its composition host");
|
||||
|
||||
var compositionId = host.getAttribute("data-composition-id");
|
||||
var duration = Number(host.getAttribute("data-duration")) || 4;
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.to({}, { duration: duration, ease: "none" }, 0);
|
||||
tl.fromTo(
|
||||
leak,
|
||||
{ opacity: 0 },
|
||||
{ opacity: 0.9, duration: duration * 0.24, ease: "sine.out" },
|
||||
duration * 0.08,
|
||||
);
|
||||
tl.to(leak, { opacity: 0.46, duration: duration * 0.2, ease: "sine.inOut" }, duration * 0.42);
|
||||
tl.to(leak, { opacity: 0, duration: duration * 0.26, ease: "sine.in" }, duration * 0.68);
|
||||
tl.fromTo(
|
||||
".hf-leak-amber",
|
||||
{ xPercent: -20, scale: 0.92, rotation: -4 },
|
||||
{ xPercent: 18, scale: 1.1, rotation: 2, duration: duration, ease: "sine.inOut" },
|
||||
0,
|
||||
);
|
||||
tl.fromTo(
|
||||
".hf-leak-red",
|
||||
{ xPercent: 20, yPercent: -8, scale: 1.08 },
|
||||
{ xPercent: -16, yPercent: 8, scale: 0.94, duration: duration, ease: "sine.inOut" },
|
||||
0,
|
||||
);
|
||||
tl.fromTo(
|
||||
".hf-leak-gold",
|
||||
{ xPercent: -28, rotation: -6 },
|
||||
{ xPercent: 24, rotation: 4, duration: duration, ease: "sine.inOut" },
|
||||
0,
|
||||
);
|
||||
|
||||
window.__timelines[compositionId] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `light-leak` `film` `memory` `transition` `overlay` `media-treatment-overlay`.
|
||||
|
||||
@@ -0,0 +1,574 @@
|
||||
---
|
||||
title: "Oscilloscope Trace"
|
||||
description: "CRT oscilloscope beam sweeping a waveform across a 10x8 graticule, with closed-form phosphor persistence: the tail decays as exp(-age/tau) and brightens where the beam slows"
|
||||
---
|
||||
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
|
||||
|
||||
<VariablesExplorer
|
||||
previewSrc="/public/catalog/preview/blocks/oscilloscope-trace.html"
|
||||
compositionId="oscilloscope-trace"
|
||||
compositionSrc="compositions/oscilloscope-trace.html"
|
||||
variables={[{"id":"waveform","type":"enum","label":"Waveform","default":"sine","options":[{"value":"sine","label":"Sine"},{"value":"square","label":"Square"},{"value":"triangle","label":"Triangle"},{"value":"data","label":"Data series"}]},{"id":"frequency","type":"number","label":"Signal frequency","unit":"Hz","default":12,"min":0.1,"max":200,"step":0.1},{"id":"amplitude","type":"number","label":"Amplitude","unit":"div","default":3,"min":0.1,"max":4,"step":0.1},{"id":"persistenceMs","type":"number","label":"Phosphor persistence (tau)","unit":"ms","default":100,"min":1,"max":1000,"step":1},{"id":"phosphorColor","type":"color","label":"Phosphor colour","default":"#5dff8f"},{"id":"sweepRate","type":"number","label":"Sweep rate","unit":"sweeps/s","default":5,"min":0.25,"max":60,"step":0.25},{"id":"dataSeries","type":"string","label":"Data series (waveform=data)","default":"","placeholder":"0,0.4,0.9,0.3,-0.6,-1,-0.2,0.5"}]}
|
||||
>
|
||||
|
||||
```html oscilloscope-trace.html
|
||||
<!doctype html>
|
||||
<html
|
||||
lang="en"
|
||||
data-composition-variables='[
|
||||
{"id":"waveform","type":"enum","label":"Waveform","default":"sine","options":[{"value":"sine","label":"Sine"},{"value":"square","label":"Square"},{"value":"triangle","label":"Triangle"},{"value":"data","label":"Data series"}]},
|
||||
{"id":"frequency","type":"number","label":"Signal frequency","unit":"Hz","default":12,"min":0.1,"max":200,"step":0.1},
|
||||
{"id":"amplitude","type":"number","label":"Amplitude","unit":"div","default":3,"min":0.1,"max":4,"step":0.1},
|
||||
{"id":"persistenceMs","type":"number","label":"Phosphor persistence (tau)","unit":"ms","default":100,"min":1,"max":1000,"step":1},
|
||||
{"id":"phosphorColor","type":"color","label":"Phosphor colour","default":"#5dff8f"},
|
||||
{"id":"sweepRate","type":"number","label":"Sweep rate","unit":"sweeps/s","default":5,"min":0.25,"max":60,"step":0.25},
|
||||
{"id":"dataSeries","type":"string","label":"Data series (waveform=data)","default":"","placeholder":"0,0.4,0.9,0.3,-0.6,-1,-0.2,0.5"}
|
||||
]'
|
||||
>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Oscilloscope Trace</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: #05070a;
|
||||
}
|
||||
#os-root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
font-family: "JetBrains Mono", ui-monospace, monospace;
|
||||
}
|
||||
#os-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(120% 120% at 30% 25%, #0d141b 0%, #05070a 68%);
|
||||
}
|
||||
#os-screen {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
display: block;
|
||||
}
|
||||
/* Purely decorative CRT falloff — no timing, no motion. */
|
||||
#os-vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(
|
||||
78% 78% at 34% 50%,
|
||||
rgba(0, 0, 0, 0) 55%,
|
||||
rgba(0, 0, 0, 0.55) 100%
|
||||
);
|
||||
}
|
||||
#os-readout {
|
||||
position: absolute;
|
||||
left: 1250px;
|
||||
top: 150px;
|
||||
width: 520px;
|
||||
color: #6f8794;
|
||||
font-size: 26px;
|
||||
line-height: 1.15;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
#os-readout .os-title {
|
||||
color: #b9ccd6;
|
||||
font-size: 34px;
|
||||
letter-spacing: 0.22em;
|
||||
padding-bottom: 26px;
|
||||
border-bottom: 2px solid #1d2a33;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
#os-readout .os-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 13px 0;
|
||||
}
|
||||
#os-readout .os-val {
|
||||
color: #d7e6ee;
|
||||
}
|
||||
#os-readout .os-note {
|
||||
margin-top: 28px;
|
||||
font-size: 20px;
|
||||
color: #6a7b85;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="os-root"
|
||||
data-composition-id="oscilloscope-trace"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="os-bg"></div>
|
||||
<canvas
|
||||
id="os-screen"
|
||||
class="clip"
|
||||
width="1920"
|
||||
height="1080"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="0"
|
||||
></canvas>
|
||||
<div id="os-vignette"></div>
|
||||
<div id="os-readout" class="clip" data-start="0" data-duration="6" data-track-index="1">
|
||||
<div class="os-title">OSCILLOSCOPE</div>
|
||||
<div class="os-row"><span>SOURCE</span><span class="os-val" id="os-r-wave">SINE</span></div>
|
||||
<div class="os-row"><span>FREQ</span><span class="os-val" id="os-r-freq">-</span></div>
|
||||
<div class="os-row"><span>TIMEBASE</span><span class="os-val" id="os-r-time">-</span></div>
|
||||
<div class="os-row"><span>VERT</span><span class="os-val" id="os-r-amp">-</span></div>
|
||||
<div class="os-row"><span>PHOSPHOR</span><span class="os-val" id="os-r-phos">-</span></div>
|
||||
<div class="os-note" id="os-r-note"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Oscilloscope trace with phosphor persistence.
|
||||
//
|
||||
// Two pieces of physical truth drive the look:
|
||||
//
|
||||
// 1. PERSISTENCE. The phosphor keeps emitting after the beam has
|
||||
// passed, decaying as exp(-age/tau). Decay constants by EIA
|
||||
// phosphor class (research entry F#10):
|
||||
// P11/P31 - 0.01-1 ms (what a real bench scope uses: no
|
||||
// visible afterglow at video rates)
|
||||
// P1 - 80-150 ms (the "vintage glow" look; tau ~= 100 ms
|
||||
// is the entry's recommended default)
|
||||
// P33 - > 1 s
|
||||
// P2/P7 - 30 s .. ~1 min (radar territory, out of range here)
|
||||
// The tail window is 3*tau: after three time constants ~5% of the
|
||||
// brightness remains, which the entry gives as the safe cutoff.
|
||||
//
|
||||
// 2. BEAM-VELOCITY BRIGHTNESS. A trace is brighter where the beam
|
||||
// moves slower, because the same deposited energy is spread over
|
||||
// a shorter path. Each sub-step deposits a fixed amount of energy
|
||||
// (constant dt), so surface brightness goes as 1/segment-length.
|
||||
// Flat parts of the waveform are bright; fast vertical edges are
|
||||
// faint. Qualitative law only - the research entry states it with
|
||||
// no proportionality constant, so the reference length below is
|
||||
// the physical minimum (pure horizontal sweep motion), not a
|
||||
// fudge factor.
|
||||
//
|
||||
// SWEEP RATE PROVENANCE. The phosphor decay constants above are
|
||||
// measured; the sweep rate is NOT - the research entry gives no
|
||||
// timebase figure. The default of 5 sweeps/s is authored: across the
|
||||
// 10-division graticule it works out to 20 ms/div, a real value from
|
||||
// the standard 1-2-5 timebase sequence, picked because it puts a few
|
||||
// cycles of the default signal on screen. Treat it as a dial, not as
|
||||
// a measurement.
|
||||
//
|
||||
// NO FEEDBACK BUFFER. The canvas is cleared every frame and the tail
|
||||
// is recomputed by evaluating the beam curve backwards in time from
|
||||
// the current frame: P(t - k*dt) for k = 0..K. Frame N depends only
|
||||
// on N, so seeking anywhere is exact rather than approximate.
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
var COMP_ID = "oscilloscope-trace";
|
||||
var DURATION = 6;
|
||||
|
||||
// Screen geometry: a 10x8 division graticule of SQUARE divisions,
|
||||
// the standard CRT scope face.
|
||||
var DIV = 100;
|
||||
var DIVS_X = 10;
|
||||
var DIVS_Y = 8;
|
||||
var SW = DIV * DIVS_X;
|
||||
var SH = DIV * DIVS_Y;
|
||||
var SX = 140;
|
||||
var SY = 140;
|
||||
var CY = SY + SH / 2;
|
||||
|
||||
// Tail window in time constants (research entry F#10: 3*tau leaves
|
||||
// ~5% weight, stated there as the safe cutoff).
|
||||
var TAIL_TAUS = 3;
|
||||
// Sub-step count. The entry suggests K ~= 24 steps across the window,
|
||||
// which is enough to quantise the DECAY but far too coarse spatially:
|
||||
// at 12 Hz the beam would advance 0.15 of a cycle per step and the
|
||||
// trace would render as a polygon. So K is derived from how fast the
|
||||
// beam actually moves - keep each sub-step under SEG_TARGET_PX of
|
||||
// travel. That target and the clamps are MY numbers, not measured
|
||||
// ones; the clamp bounds per-frame cost when persistence is long.
|
||||
var SEG_TARGET_PX = 3;
|
||||
var K_MIN = 120;
|
||||
var K_MAX = 8000;
|
||||
|
||||
// Band-limit for the square wave. A real generator + a real scope
|
||||
// front end both have finite bandwidth, so the edge is steep but not
|
||||
// instantaneous; a mathematical step would make the vertical edge one
|
||||
// sub-step long and the velocity law would erase it entirely.
|
||||
var SQUARE_SHARPNESS = 8;
|
||||
|
||||
function readVariables() {
|
||||
var api = window.__hyperframes && window.__hyperframes.getVariables;
|
||||
if (typeof api === "function") return api() || {};
|
||||
// Standalone fallback (raw file opened without the runtime): read
|
||||
// the same declaration the runtime reads, so defaults have exactly
|
||||
// one home.
|
||||
var out = {};
|
||||
try {
|
||||
var raw = document.documentElement.getAttribute("data-composition-variables");
|
||||
var decls = JSON.parse(raw || "[]");
|
||||
for (var i = 0; i < decls.length; i++) out[decls[i].id] = decls[i].default;
|
||||
} catch (err) {
|
||||
/* declaration missing or malformed - fall through to hard defaults */
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function num(value, fallback, min, max) {
|
||||
var n = typeof value === "number" ? value : parseFloat(value);
|
||||
if (!isFinite(n)) n = fallback;
|
||||
return Math.min(max, Math.max(min, n));
|
||||
}
|
||||
|
||||
var V = readVariables();
|
||||
var waveform =
|
||||
["sine", "square", "triangle", "data"].indexOf(String(V.waveform)) >= 0
|
||||
? String(V.waveform)
|
||||
: "sine";
|
||||
var frequency = num(V.frequency, 12, 0.1, 200);
|
||||
var amplitude = num(V.amplitude, 3, 0.1, 4);
|
||||
var tau = num(V.persistenceMs, 100, 1, 1000) / 1000;
|
||||
var sweepRate = num(V.sweepRate, 5, 0.25, 60);
|
||||
var phosphor = parseColor(V.phosphorColor, [93, 255, 143]);
|
||||
|
||||
// waveform="data" replays a supplied series instead of a synthetic
|
||||
// shape: comma/whitespace separated numbers, clamped to -1..1, linearly
|
||||
// interpolated, one full pass of the series per `frequency` cycle.
|
||||
// Empty or unparseable -> falls back to sine.
|
||||
var series = String(V.dataSeries == null ? "" : V.dataSeries)
|
||||
.split(/[\s,]+/)
|
||||
.map(parseFloat)
|
||||
.filter(function (n) {
|
||||
return isFinite(n);
|
||||
})
|
||||
.map(function (n) {
|
||||
return Math.min(1, Math.max(-1, n));
|
||||
});
|
||||
if (waveform === "data" && series.length < 2) waveform = "sine";
|
||||
|
||||
function parseColor(value, fallback) {
|
||||
var m = /^#?([0-9a-f]{6})$/i.exec(String(value == null ? "" : value).trim());
|
||||
if (!m) return fallback;
|
||||
var v = parseInt(m[1], 16);
|
||||
return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
|
||||
}
|
||||
|
||||
function rgba(c, a) {
|
||||
return "rgba(" + c[0] + "," + c[1] + "," + c[2] + "," + a.toFixed(4) + ")";
|
||||
}
|
||||
|
||||
// --- the signal: pure function of time, no state ------------------
|
||||
|
||||
function wave(t) {
|
||||
var p = frequency * t; // cycles elapsed
|
||||
if (waveform === "square") {
|
||||
return Math.tanh(SQUARE_SHARPNESS * Math.sin(2 * Math.PI * p));
|
||||
}
|
||||
if (waveform === "triangle") {
|
||||
return (2 / Math.PI) * Math.asin(Math.sin(2 * Math.PI * p));
|
||||
}
|
||||
if (waveform === "data") {
|
||||
var frac = p - Math.floor(p);
|
||||
var pos = frac * (series.length - 1);
|
||||
var i = Math.floor(pos);
|
||||
var f = pos - i;
|
||||
var a = series[i];
|
||||
var b = series[Math.min(series.length - 1, i + 1)];
|
||||
return a + (b - a) * f;
|
||||
}
|
||||
return Math.sin(2 * Math.PI * p);
|
||||
}
|
||||
|
||||
/** Horizontal sweep position, 0..1 across the graticule. */
|
||||
function sweepU(t) {
|
||||
var s = t * sweepRate;
|
||||
return s - Math.floor(s);
|
||||
}
|
||||
|
||||
function beamX(t) {
|
||||
return SX + sweepU(t) * SW;
|
||||
}
|
||||
|
||||
function beamY(t) {
|
||||
return CY - wave(t) * amplitude * DIV;
|
||||
}
|
||||
|
||||
// Upper bound on beam speed, in px/s, used to pick the sub-step count.
|
||||
// Horizontal is the constant sweep; vertical is the waveform's steepest
|
||||
// slope, which differs per shape - a band-limited square is
|
||||
// SQUARE_SHARPNESS times steeper at its edge than a sine of the same
|
||||
// frequency, and under-sampling exactly there is what turns the edge
|
||||
// into a polygon.
|
||||
var slopeBound;
|
||||
if (waveform === "square") {
|
||||
slopeBound = SQUARE_SHARPNESS * 2 * Math.PI * frequency;
|
||||
} else if (waveform === "triangle") {
|
||||
slopeBound = 4 * frequency;
|
||||
} else if (waveform === "data") {
|
||||
var maxStep = 0;
|
||||
for (var si = 1; si < series.length; si++) {
|
||||
maxStep = Math.max(maxStep, Math.abs(series[si] - series[si - 1]));
|
||||
}
|
||||
slopeBound = maxStep * (series.length - 1) * frequency;
|
||||
} else {
|
||||
slopeBound = 2 * Math.PI * frequency;
|
||||
}
|
||||
var beamSpeedMax = Math.sqrt(
|
||||
Math.pow(SW * sweepRate, 2) + Math.pow(slopeBound * amplitude * DIV, 2),
|
||||
);
|
||||
|
||||
// --- graticule (drawn once to an offscreen canvas) ----------------
|
||||
|
||||
var grat = document.createElement("canvas");
|
||||
grat.width = 1920;
|
||||
grat.height = 1080;
|
||||
(function drawGraticule() {
|
||||
var g = grat.getContext("2d");
|
||||
g.fillStyle = "#04080a";
|
||||
g.fillRect(SX, SY, SW, SH);
|
||||
g.strokeStyle = "rgba(120,180,160,0.16)";
|
||||
g.lineWidth = 1;
|
||||
for (var i = 1; i < DIVS_X; i++) {
|
||||
g.beginPath();
|
||||
g.moveTo(SX + i * DIV + 0.5, SY);
|
||||
g.lineTo(SX + i * DIV + 0.5, SY + SH);
|
||||
g.stroke();
|
||||
}
|
||||
for (var j = 1; j < DIVS_Y; j++) {
|
||||
g.beginPath();
|
||||
g.moveTo(SX, SY + j * DIV + 0.5);
|
||||
g.lineTo(SX + SW, SY + j * DIV + 0.5);
|
||||
g.stroke();
|
||||
}
|
||||
// Centre axes carry the fine 0.2-division ticks, as on a real face.
|
||||
g.strokeStyle = "rgba(150,205,185,0.32)";
|
||||
var cx = SX + SW / 2 + 0.5;
|
||||
var cy = CY + 0.5;
|
||||
g.beginPath();
|
||||
g.moveTo(cx, SY);
|
||||
g.lineTo(cx, SY + SH);
|
||||
g.moveTo(SX, cy);
|
||||
g.lineTo(SX + SW, cy);
|
||||
g.stroke();
|
||||
g.strokeStyle = "rgba(150,205,185,0.42)";
|
||||
for (var k = 1; k < DIVS_X * 5; k++) {
|
||||
var x = SX + (k * DIV) / 5 + 0.5;
|
||||
g.beginPath();
|
||||
g.moveTo(x, cy - 9);
|
||||
g.lineTo(x, cy + 9);
|
||||
g.stroke();
|
||||
}
|
||||
for (var m = 1; m < DIVS_Y * 5; m++) {
|
||||
var y = SY + (m * DIV) / 5 + 0.5;
|
||||
g.beginPath();
|
||||
g.moveTo(cx - 9, y);
|
||||
g.lineTo(cx + 9, y);
|
||||
g.stroke();
|
||||
}
|
||||
g.strokeStyle = "rgba(160,215,195,0.55)";
|
||||
g.lineWidth = 2;
|
||||
g.strokeRect(SX + 1, SY + 1, SW - 2, SH - 2);
|
||||
})();
|
||||
|
||||
// --- per-frame paint ----------------------------------------------
|
||||
|
||||
var canvas = document.getElementById("os-screen");
|
||||
var ctx = canvas.getContext("2d");
|
||||
|
||||
function draw(t) {
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
ctx.clearRect(0, 0, 1920, 1080);
|
||||
ctx.drawImage(grat, 0, 0);
|
||||
|
||||
var windowS = TAIL_TAUS * tau;
|
||||
var K = Math.max(
|
||||
K_MIN,
|
||||
Math.min(K_MAX, Math.round((windowS * beamSpeedMax) / SEG_TARGET_PX)),
|
||||
);
|
||||
var dt = windowS / K;
|
||||
// Length the beam covers in one sub-step with zero vertical motion.
|
||||
// That is the slowest the beam can ever move, so it is the maximum
|
||||
// brightness reference and the velocity weight never exceeds 1.
|
||||
var lRef = SW * sweepRate * dt;
|
||||
|
||||
ctx.globalCompositeOperation = "lighter";
|
||||
ctx.lineCap = "round";
|
||||
|
||||
// Oldest -> newest, so the bright head lands on top.
|
||||
var prevX = 0;
|
||||
var prevY = 0;
|
||||
var prevU = 0;
|
||||
var havePrev = false;
|
||||
for (var k = K; k >= 0; k--) {
|
||||
var tk = t - k * dt;
|
||||
if (tk < 0) {
|
||||
// Before frame 0 the beam had not been switched on yet.
|
||||
havePrev = false;
|
||||
continue;
|
||||
}
|
||||
var u = sweepU(tk);
|
||||
var x = SX + u * SW;
|
||||
var y = beamY(tk);
|
||||
if (havePrev && u >= prevU) {
|
||||
var dx = x - prevX;
|
||||
var dy = y - prevY;
|
||||
var len = Math.sqrt(dx * dx + dy * dy);
|
||||
// Beam-velocity law: brightness ~ 1 / path length per unit time.
|
||||
var vel = len > 1e-6 ? Math.min(1, lRef / len) : 1;
|
||||
var decay = Math.exp((-k * dt) / tau);
|
||||
var a = decay * vel;
|
||||
if (a > 0.002) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(prevX, prevY);
|
||||
ctx.lineTo(x, y);
|
||||
ctx.strokeStyle = rgba(phosphor, a * 0.13);
|
||||
ctx.lineWidth = 13;
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = rgba(phosphor, a * 0.9);
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
if (a > 0.55) {
|
||||
// Overdriven phosphor saturates towards white at the head.
|
||||
ctx.strokeStyle =
|
||||
"rgba(255,255,255," + (((a - 0.55) / 0.45) * 0.7).toFixed(4) + ")";
|
||||
ctx.lineWidth = 1.4;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Retrace is blanked on a real scope: when u wraps, drop the
|
||||
// connecting segment instead of drawing a line back across.
|
||||
prevX = x;
|
||||
prevY = y;
|
||||
prevU = u;
|
||||
havePrev = true;
|
||||
}
|
||||
|
||||
// Beam head: the spot itself, brightest point on the screen.
|
||||
var hx = beamX(t);
|
||||
var hy = beamY(t);
|
||||
var glow = ctx.createRadialGradient(hx, hy, 0, hx, hy, 22);
|
||||
glow.addColorStop(0, "rgba(255,255,255,0.95)");
|
||||
glow.addColorStop(0.22, rgba(phosphor, 0.85));
|
||||
glow.addColorStop(1, rgba(phosphor, 0));
|
||||
ctx.fillStyle = glow;
|
||||
ctx.beginPath();
|
||||
ctx.arc(hx, hy, 22, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
}
|
||||
|
||||
// --- readout --------------------------------------------------------
|
||||
|
||||
(function fillReadout() {
|
||||
// Nearest EIA phosphor class for the chosen tau (bands from the
|
||||
// research entry). Labelling, not simulation.
|
||||
var tauMs = tau * 1000;
|
||||
var cls = tauMs <= 1 ? "P31" : tauMs <= 150 ? "P1" : "P33";
|
||||
var msPerDiv = 1000 / (sweepRate * DIVS_X);
|
||||
function set(id, text) {
|
||||
document.getElementById(id).textContent = text;
|
||||
}
|
||||
set("os-r-wave", waveform.toUpperCase());
|
||||
set("os-r-freq", frequency.toFixed(frequency < 10 ? 2 : 1) + " Hz");
|
||||
set(
|
||||
"os-r-time",
|
||||
(msPerDiv >= 10 ? msPerDiv.toFixed(0) : msPerDiv.toFixed(2)) + " ms/div",
|
||||
);
|
||||
set("os-r-amp", amplitude.toFixed(1) + " div pk");
|
||||
set("os-r-phos", cls + " tau " + tauMs.toFixed(0) + " ms");
|
||||
set(
|
||||
"os-r-note",
|
||||
"Trace brightness falls as exp(-age/tau) behind the beam, and rises where the beam slows.",
|
||||
);
|
||||
})();
|
||||
|
||||
// --- timeline -------------------------------------------------------
|
||||
//
|
||||
// tl.eventCallback("onUpdate", ...) is NOT usable here: the runtime
|
||||
// seeks with suppressEvents, so the callback never fires on a seek and
|
||||
// the canvas would keep whatever the last played frame drew. A tweened
|
||||
// property with an accessor is applied by GSAP on every render,
|
||||
// including suppressed ones, so the repaint is driven from the setter.
|
||||
|
||||
var beam = { t: 0 };
|
||||
var driver = {};
|
||||
Object.defineProperty(driver, "t", {
|
||||
get: function () {
|
||||
return beam.t;
|
||||
},
|
||||
set: function (value) {
|
||||
beam.t = value;
|
||||
draw(value);
|
||||
},
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
tl.to(driver, { t: DURATION, duration: DURATION, ease: "none", lazy: false }, 0);
|
||||
window.__timelines[COMP_ID] = tl;
|
||||
|
||||
draw(0);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</VariablesExplorer>
|
||||
|
||||
## Install
|
||||
|
||||
<InstallCommand command="npx hyperframes add oscilloscope-trace" />
|
||||
|
||||
That writes one file: `compositions/oscilloscope-trace.html`.
|
||||
|
||||
## Add it to your video
|
||||
|
||||
It runs for 6 seconds at 1920×1080. Paste this into your composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="oscilloscope-trace"
|
||||
data-composition-src="compositions/oscilloscope-trace.html"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `instrument` `data` `showcase` `retro`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
||||
- [Build a richer composition](/go-further)
|
||||
@@ -0,0 +1,675 @@
|
||||
---
|
||||
title: "Rack Focus"
|
||||
description: "A focus pull with real aperture bokeh: the focal plane travels from a near subject to a far one, so one resolves as the other blows into hard-edged polygon discs that clip to a cat's eye toward the corners. A focus pull only reads against content at two clearly separated depths, so this block carries its own depth-layered night exterior and puts both subject depths on variables."
|
||||
---
|
||||
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
|
||||
|
||||
<VariablesExplorer
|
||||
previewSrc="/public/catalog/preview/blocks/rack-focus.html"
|
||||
compositionId="rack-focus"
|
||||
compositionSrc="compositions/rack-focus.html"
|
||||
variables={[{"id":"nearfocus","type":"number","label":"Near focal distance","default":1.2,"min":0.2,"max":100,"step":0.05,"unit":"m"},{"id":"farfocus","type":"number","label":"Far focal distance","default":80,"min":0.3,"max":400,"step":0.5,"unit":"m"},{"id":"focallength","type":"number","label":"Focal length","default":85,"min":12,"max":300,"step":1,"unit":"mm"},{"id":"aperture","type":"number","label":"Aperture (f-number)","default":1.8,"min":0.95,"max":22,"step":0.05},{"id":"blades","type":"number","label":"Aperture blades (bokeh shape)","default":6,"min":3,"max":14,"step":1},{"id":"catseye","type":"number","label":"Cat eye clipping at the corners","default":0.62,"min":0,"max":1,"step":0.02},{"id":"pullstart","type":"number","label":"Pull start","default":1.2,"min":0,"max":30,"step":0.05,"unit":"s"},{"id":"pullduration","type":"number","label":"Pull duration","default":3.2,"min":0.1,"max":30,"step":0.05,"unit":"s"},{"id":"pullease","type":"string","label":"Pull easing (GSAP ease)","default":"power2.inOut","placeholder":"power2.inOut"},{"id":"bokeh","type":"number","label":"Bokeh exposure","default":1,"min":0,"max":3,"step":0.05},{"id":"backdrop","type":"color","label":"Backdrop","default":"#05060a"}]}
|
||||
>
|
||||
|
||||
```html rack-focus.html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Rack Focus</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<!--
|
||||
RACK FOCUS: a focus pull with real aperture bokeh.
|
||||
|
||||
WHAT THIS NEEDS TO READ AT ALL
|
||||
------------------------------
|
||||
A focus pull is a depth effect. It only reads when the frame holds
|
||||
content at two clearly separated depths: something near the lens and
|
||||
something far behind it. Point it at flat, single-plane content and
|
||||
nothing happens, exactly like a dolly zoom on a flat card.
|
||||
|
||||
This block therefore carries its own depth-layered scene, a night
|
||||
exterior built from light sources spread from 1.15 m to 90 m, so it
|
||||
works standalone. Every depth is a variable: `nearfocus` is where the
|
||||
pull starts, `farfocus` is where it ends, and the scene's two subjects
|
||||
sit at those depths. Change them and the subjects move with them.
|
||||
|
||||
WHY THIS IS NOT A BLUR
|
||||
----------------------
|
||||
A CSS/Gaussian blur softens everything uniformly. A real defocus turns
|
||||
each point of light into an image of the APERTURE, scaled by its circle
|
||||
of confusion, so a bright point becomes a hard-edged polygon disc whose
|
||||
size grows with distance from the focal plane, and which clips to a
|
||||
cat's-eye toward the frame corners. Every light in this scene is
|
||||
splatted as an aperture-shaped sprite at its own circle of confusion.
|
||||
|
||||
CIRCLE OF CONFUSION, from three.js BokehShader2 (MIT), Martins Upitis
|
||||
--------------------------------------------------------------------
|
||||
Read from examples/jsm/shaders/BokehShader2.js:
|
||||
|
||||
float CoC = 0.03; // circle of confusion in mm
|
||||
// (35mm film = 0.03mm)
|
||||
float f = focalLength; // mm
|
||||
float d = fDepth * 1000.0; // focal plane in mm
|
||||
float o = depth * 1000.0; // object depth in mm
|
||||
float a = (o * f) / (o - f);
|
||||
float b = (d * f) / (d - f);
|
||||
float c = (d - f) / (d * fstop * CoC);
|
||||
blur = abs(a - b) * c;
|
||||
|
||||
`blur` comes out in units of that 0.03 mm acceptable-sharpness circle,
|
||||
so the defocus DIAMETER on the sensor is `blur * 0.03` mm, which this
|
||||
block converts to pixels with the sensor width. Same constants, same
|
||||
formula, independently written, no code copied. This agrees with the
|
||||
Zeiss thin-lens form CoC = (f²/N)·|1/S - 1/U| for S >> f.
|
||||
|
||||
Aperture shape is the community-standard regular-polygon boundary
|
||||
d(θ) = cos(π/n) / cos(mod(θ, 2π/n) - π/n), n = blade count (real
|
||||
irises ship 5, 6, 8 or 9 blades). Cat's-eye clipping is the aperture
|
||||
intersected with two barrel openings offset along the radial direction,
|
||||
which is the actual mechanism of mechanical vignetting.
|
||||
|
||||
DETERMINISM
|
||||
-----------
|
||||
State at frame N is computed from N. The focal distance is a closed-form
|
||||
function of t, every circle of confusion follows from a static depth and
|
||||
that focal distance, and the scene point cloud is built once from a
|
||||
seeded PRNG. No accumulation, no clocks, no unseeded randomness.
|
||||
-->
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
#rf-root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#rf-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #05060a;
|
||||
}
|
||||
#rf-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="rf-root"
|
||||
data-composition-id="rack-focus"
|
||||
data-root="true"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-composition-variables='[
|
||||
{"id":"nearfocus","type":"number","label":"Near focal distance","default":1.2,"min":0.2,"max":100,"step":0.05,"unit":"m"},
|
||||
{"id":"farfocus","type":"number","label":"Far focal distance","default":80,"min":0.3,"max":400,"step":0.5,"unit":"m"},
|
||||
{"id":"focallength","type":"number","label":"Focal length","default":85,"min":12,"max":300,"step":1,"unit":"mm"},
|
||||
{"id":"aperture","type":"number","label":"Aperture (f-number)","default":1.8,"min":0.95,"max":22,"step":0.05},
|
||||
{"id":"blades","type":"number","label":"Aperture blades (bokeh shape)","default":6,"min":3,"max":14,"step":1},
|
||||
{"id":"catseye","type":"number","label":"Cat eye clipping at the corners","default":0.62,"min":0,"max":1,"step":0.02},
|
||||
{"id":"pullstart","type":"number","label":"Pull start","default":1.2,"min":0,"max":30,"step":0.05,"unit":"s"},
|
||||
{"id":"pullduration","type":"number","label":"Pull duration","default":3.2,"min":0.1,"max":30,"step":0.05,"unit":"s"},
|
||||
{"id":"pullease","type":"string","label":"Pull easing (GSAP ease)","default":"power2.inOut","placeholder":"power2.inOut"},
|
||||
{"id":"bokeh","type":"number","label":"Bokeh exposure","default":1,"min":0,"max":3,"step":0.05},
|
||||
{"id":"backdrop","type":"color","label":"Backdrop","default":"#05060a"}
|
||||
]'
|
||||
>
|
||||
<div id="rf-backdrop"></div>
|
||||
<canvas id="rf-canvas" width="1920" height="1080"></canvas>
|
||||
|
||||
<!-- Driver clip: gives HyperFrames a timed element to own on track 0. -->
|
||||
<div
|
||||
id="rf-drv"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var DUR = 6;
|
||||
var W = 1920;
|
||||
var H = 1080;
|
||||
|
||||
// 36mm-wide sensor, 16:9 active area. Pixels per millimetre is the
|
||||
// only thing the projection needs, and it is the same on both axes.
|
||||
var SENSOR_W_MM = 36;
|
||||
var PX_PER_MM = W / SENSOR_W_MM;
|
||||
|
||||
// BokehShader2's acceptable-sharpness circle, in mm (35mm film).
|
||||
var COC_MM = 0.03;
|
||||
|
||||
// Defocus is clamped so a wildly out-of-range focus setting cannot
|
||||
// splat sprites the size of the frame. BokehShader2 clamps the same
|
||||
// quantity with its `maxblur` uniform.
|
||||
var MAX_COC_R_PX = 0.1 * H;
|
||||
// Smallest sprite half-width. Below roughly one pixel a splat is an
|
||||
// aliasing machine, so points in focus bottom out here.
|
||||
var MIN_R_PX = 0.75;
|
||||
|
||||
var V = (window.__hyperframes && window.__hyperframes.getVariables()) || {};
|
||||
var CS = getComputedStyle(document.getElementById("rf-root"));
|
||||
|
||||
// The runtime defines every declared variable as `--<slug>` on the
|
||||
// root (packages/core/src/tokenSlug.ts), so a host stylesheet can
|
||||
// override one there too. Read the custom property first, fall back
|
||||
// to the declared value when it is unset.
|
||||
function raw(id) {
|
||||
var css = CS.getPropertyValue("--" + id.toLowerCase()).trim();
|
||||
return css !== "" ? css : V[id];
|
||||
}
|
||||
function num(id, fallback) {
|
||||
var n = parseFloat(raw(id));
|
||||
return isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
var NEAR = num("nearfocus", 1.2);
|
||||
var FAR = num("farfocus", 80);
|
||||
var FOCAL = num("focallength", 85);
|
||||
var FSTOP = num("aperture", 1.8);
|
||||
var BLADES = Math.max(3, Math.round(num("blades", 6)));
|
||||
var CATSEYE = num("catseye", 0.62);
|
||||
var PULL_START = num("pullstart", 1.2);
|
||||
var PULL_DUR = Math.max(0.001, num("pullduration", 3.2));
|
||||
var BOKEH = num("bokeh", 1);
|
||||
var EASE_NAME = String(raw("pullease") || "power2.inOut");
|
||||
var BACKDROP =
|
||||
typeof raw("backdrop") === "string" && raw("backdrop") ? raw("backdrop") : "#05060a";
|
||||
|
||||
document.getElementById("rf-backdrop").style.background = BACKDROP;
|
||||
|
||||
var EASE = gsap.parseEase(EASE_NAME) || gsap.parseEase("power2.inOut");
|
||||
|
||||
// A focus ring is roughly linear in dioptres, not in metres: a rack
|
||||
// from 1.2m to 80m spends its first millimetre of barrel rotation
|
||||
// crossing most of the distance. Interpolating 1/distance is what
|
||||
// makes the pull travel evenly instead of snapping to the far plane.
|
||||
function focusAt(t) {
|
||||
var u = Math.min(1, Math.max(0, (t - PULL_START) / PULL_DUR));
|
||||
var e = EASE(u);
|
||||
var inv = 1 / NEAR + (1 / FAR - 1 / NEAR) * e;
|
||||
return 1 / inv;
|
||||
}
|
||||
|
||||
// ── Scene ────────────────────────────────────────────────────────
|
||||
// A night exterior: a string of practical lights right in front of
|
||||
// the lens, a lit city block far behind it, and scattered lights
|
||||
// through every depth between so the pull reads as a continuous
|
||||
// travel rather than a cut between two planes.
|
||||
|
||||
function mulberry32(a) {
|
||||
return function () {
|
||||
a |= 0;
|
||||
a = (a + 0x6d2b79f5) | 0;
|
||||
var t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
var rnd = mulberry32(0x5eed1a3);
|
||||
function rr(lo, hi) {
|
||||
return lo + (hi - lo) * rnd();
|
||||
}
|
||||
|
||||
// x, y in metres (y up, origin on the optical axis), z in metres,
|
||||
// r0 = the light's own physical radius in metres, b = peak
|
||||
// brightness when perfectly in focus (values above 1 are highlights
|
||||
// that clip, which is exactly why they stay visible once spread
|
||||
// across a bokeh disc), rgb = colour.
|
||||
var P = [];
|
||||
function light(x, y, z, r0, b, c) {
|
||||
P.push(x, y, z, r0, b, c[0], c[1], c[2]);
|
||||
}
|
||||
|
||||
var TUNGSTEN = [1.0, 0.74, 0.45];
|
||||
var FILAMENT = [1.0, 0.9, 0.74];
|
||||
var WIRE = [0.86, 0.74, 0.6];
|
||||
var WARM_WIN = [1.0, 0.79, 0.52];
|
||||
var COOL_WIN = [0.6, 0.75, 1.0];
|
||||
var SIGN_A = [0.35, 0.95, 1.0];
|
||||
var SIGN_B = [1.0, 0.42, 0.72];
|
||||
|
||||
// Half the frame's width, in metres, at depth z. Both subjects are
|
||||
// laid out against the reference framing (85mm, 1.2m / 80m) and then
|
||||
// scaled by this, so retuning the lens or either focal distance moves
|
||||
// the subjects with the frame instead of pushing them out of it.
|
||||
function halfW(z) {
|
||||
return (0.5 * SENSOR_W_MM * z) / FOCAL;
|
||||
}
|
||||
|
||||
// Near subject: a catenary string of bulbs at `nearfocus`. The wire is
|
||||
// what makes "sharp" unmistakable, a one-pixel line either resolves
|
||||
// or it does not, and the filament inside each bulb is the second cue.
|
||||
var NEAR_Z = NEAR;
|
||||
var NS = halfW(NEAR_Z) / 0.25412;
|
||||
var X_END = 0.4;
|
||||
var SAG_A = 0.3;
|
||||
var SAG = 0.1;
|
||||
var COSH_END = Math.cosh(X_END / SAG_A);
|
||||
function stringY(x) {
|
||||
var s = (COSH_END - Math.cosh(x / SAG_A)) / (COSH_END - 1);
|
||||
return 0.075 - SAG * s - 0.035 * (x / X_END);
|
||||
}
|
||||
function stringZ(x) {
|
||||
return NEAR_Z + 0.05 * (x / X_END);
|
||||
}
|
||||
for (var i = 0; i < 1100; i++) {
|
||||
var wx = -0.42 + (0.84 * i) / 1099;
|
||||
light(wx * NS, stringY(wx) * NS, stringZ(wx), 0.0006 * NS, 1.4, WIRE);
|
||||
}
|
||||
for (var k = -5; k <= 5; k++) {
|
||||
var bx = k * 0.085;
|
||||
var by = stringY(bx) - 0.011;
|
||||
var bz = stringZ(bx);
|
||||
light(bx * NS, by * NS, bz, 0.006 * NS, 26, TUNGSTEN);
|
||||
light((bx - 0.0022) * NS, by * NS, bz, 0.0006 * NS, 7, FILAMENT);
|
||||
light(bx * NS, (by - 0.0022) * NS, bz, 0.0006 * NS, 7, FILAMENT);
|
||||
light((bx + 0.0022) * NS, by * NS, bz, 0.0006 * NS, 7, FILAMENT);
|
||||
}
|
||||
|
||||
// Far subject: three lit towers plus a dense LED sign strip, sitting
|
||||
// around `farfocus`. The sign's pitch is fine enough that it only
|
||||
// resolves into separate lamps when focus actually arrives.
|
||||
var FAR_Z = FAR;
|
||||
var FS = halfW(FAR_Z) / 16.941;
|
||||
function tower(cx, hw, topY, botY, cols, rows, z, lit) {
|
||||
for (var c = 0; c < cols; c++) {
|
||||
for (var r = 0; r < rows; r++) {
|
||||
if (rnd() > lit) continue;
|
||||
var x = cx - hw + (2 * hw * (c + 0.5)) / cols;
|
||||
var y = botY + ((topY - botY) * (r + 0.5)) / rows;
|
||||
var cool = rnd() < 0.28;
|
||||
light(x, y, z + rr(-0.4, 0.4) * FS, 0.24 * FS, rr(9, 20), cool ? COOL_WIN : WARM_WIN);
|
||||
}
|
||||
}
|
||||
}
|
||||
tower(-9.5 * FS, 3.5 * FS, 9.6 * FS, -4.0 * FS, 6, 14, FAR_Z * 1.03, 0.34);
|
||||
tower(2.0 * FS, 4.5 * FS, 6.4 * FS, -4.0 * FS, 8, 12, FAR_Z * 0.98, 0.3);
|
||||
tower(12.5 * FS, 3.0 * FS, 11.0 * FS, -4.0 * FS, 5, 15, FAR_Z * 1.08, 0.36);
|
||||
for (var s = 0; s < 40; s++) {
|
||||
var sx = (-5.5 + (11 * s) / 39) * FS;
|
||||
var mixc = s / 39;
|
||||
var sc = [
|
||||
SIGN_A[0] + (SIGN_B[0] - SIGN_A[0]) * mixc,
|
||||
SIGN_A[1] + (SIGN_B[1] - SIGN_A[1]) * mixc,
|
||||
SIGN_A[2] + (SIGN_B[2] - SIGN_A[2]) * mixc,
|
||||
];
|
||||
light(sx, -5.0 * FS, FAR_Z * 0.99, 0.1 * FS, 7, sc);
|
||||
light(sx, -5.55 * FS, FAR_Z * 0.99, 0.1 * FS, 7, sc);
|
||||
}
|
||||
|
||||
// Everything between. Depth is drawn log-uniform between the two
|
||||
// subjects and the screen position is uniform, so the mid-ground
|
||||
// stays evenly spread whatever the two focal distances are.
|
||||
var Z_LO = Math.min(NEAR_Z, FAR_Z) * 1.9;
|
||||
var Z_HI = Math.max(NEAR_Z, FAR_Z) * 0.85;
|
||||
for (var m = 0; m < 90; m++) {
|
||||
var z = Z_LO * Math.pow(Z_HI / Z_LO, rnd());
|
||||
var halfWm = (0.5 * SENSOR_W_MM * z) / FOCAL;
|
||||
var halfHm = (halfWm * H) / W;
|
||||
var warm = rnd() < 0.66;
|
||||
light(
|
||||
rr(-1.05, 1.05) * halfWm,
|
||||
rr(-1.0, 0.75) * halfHm,
|
||||
z,
|
||||
rr(0.006, 0.05) * (z / 12),
|
||||
rr(5, 18),
|
||||
warm ? WARM_WIN : COOL_WIN,
|
||||
);
|
||||
}
|
||||
|
||||
// ── GL ───────────────────────────────────────────────────────────
|
||||
var canvas = document.getElementById("rf-canvas");
|
||||
var gl =
|
||||
canvas.getContext("webgl", {
|
||||
alpha: true,
|
||||
antialias: false,
|
||||
depth: false,
|
||||
stencil: false,
|
||||
preserveDrawingBuffer: true,
|
||||
powerPreference: "high-performance",
|
||||
}) ||
|
||||
canvas.getContext("experimental-webgl", {
|
||||
alpha: true,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
|
||||
var VERT = [
|
||||
"precision highp float;",
|
||||
"attribute vec2 aCorner;", // -1..1 quad corner
|
||||
"attribute vec3 aPos;", // metres, y up, z away from the lens
|
||||
"attribute vec2 aSize;", // x = own radius (m), y = in-focus peak
|
||||
"attribute vec3 aColor;",
|
||||
"uniform vec2 uRes;",
|
||||
"uniform float uPxPerMm;",
|
||||
"uniform float uFocal;", // mm
|
||||
"uniform float uFocus;", // metres
|
||||
"uniform float uFstop;",
|
||||
"uniform float uCoCmm;",
|
||||
"uniform float uMaxR;",
|
||||
"uniform float uMinR;",
|
||||
"uniform float uCatsEye;",
|
||||
"uniform float uBokeh;",
|
||||
"varying vec2 vQ;",
|
||||
"varying vec3 vColor;",
|
||||
"varying float vGain;",
|
||||
"varying float vShape;",
|
||||
"varying float vAA;",
|
||||
"varying vec2 vRadial;",
|
||||
"varying float vCat;",
|
||||
"void main() {",
|
||||
" float z = max(aPos.z, 0.001);",
|
||||
" float ppm = uPxPerMm * uFocal / z;", // pixels per metre at this depth
|
||||
" vec2 centre = uRes * 0.5 + aPos.xy * ppm;",
|
||||
" float r0 = max(aSize.x * ppm, uMinR);",
|
||||
"",
|
||||
" // BokehShader2's circle of confusion, in units of uCoCmm.",
|
||||
" float f = uFocal;",
|
||||
" float d = uFocus * 1000.0;",
|
||||
" float o = z * 1000.0;",
|
||||
" float a = (o * f) / max(o - f, 1e-4);",
|
||||
" float b = (d * f) / max(d - f, 1e-4);",
|
||||
" float c = (d - f) / max(d * uFstop * uCoCmm, 1e-6);",
|
||||
" float blur = abs(a - b) * c;",
|
||||
" // -> defocus diameter in mm -> pixels -> radius.",
|
||||
" float cocR = min(blur * uCoCmm * uPxPerMm * 0.5, uMaxR);",
|
||||
"",
|
||||
" // A finite source convolved with the defocus disc: radii add in",
|
||||
" // quadrature. Flux is conserved, so peak brightness falls as the",
|
||||
" // inverse square of the radius. That single term is why an",
|
||||
" // in-focus lamp clips to white and a defocused one is a readable",
|
||||
" // disc instead of a smear.",
|
||||
" float R = sqrt(r0 * r0 + cocR * cocR);",
|
||||
" vGain = aSize.y * uBokeh * (r0 * r0) / (R * R);",
|
||||
" // Near focus the sprite is the lamp (round); far from it the",
|
||||
" // sprite is an image of the aperture (polygonal).",
|
||||
" vShape = (cocR * cocR) / (cocR * cocR + r0 * r0);",
|
||||
" vAA = 1.0 / max(R, 0.5);",
|
||||
" vColor = aColor;",
|
||||
" vQ = aCorner;",
|
||||
"",
|
||||
" // Mechanical vignetting: the barrel openings clip the aperture",
|
||||
" // harder the further the sprite sits from the optical axis.",
|
||||
" vec2 off = centre - uRes * 0.5;",
|
||||
" float rad = length(off);",
|
||||
" vRadial = rad > 1.0 ? off / rad : vec2(1.0, 0.0);",
|
||||
" vCat = uCatsEye * min(1.0, rad / (length(uRes) * 0.5));",
|
||||
"",
|
||||
" // A light spread thin enough to land under half a display code",
|
||||
" // value contributes nothing but fill rate. Culling it here is what",
|
||||
" // keeps a 900-point wire from splatting 900 invisible discs the",
|
||||
" // moment it goes out of focus.",
|
||||
" if (vGain < 0.0015) {",
|
||||
" gl_Position = vec4(2.0, 2.0, 2.0, 1.0);",
|
||||
" return;",
|
||||
" }",
|
||||
"",
|
||||
" vec2 p = centre + aCorner * R;",
|
||||
" gl_Position = vec4((p / uRes) * 2.0 - 1.0, 0.0, 1.0);",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
var FRAG = [
|
||||
"precision highp float;",
|
||||
"varying vec2 vQ;",
|
||||
"varying vec3 vColor;",
|
||||
"varying float vGain;",
|
||||
"varying float vShape;",
|
||||
"varying float vAA;",
|
||||
"varying vec2 vRadial;",
|
||||
"varying float vCat;",
|
||||
"uniform float uBlades;",
|
||||
"const float PI = 3.14159265;",
|
||||
"void main() {",
|
||||
" float r = length(vQ);",
|
||||
" if (r > 1.0) discard;",
|
||||
" float th = r > 1e-5 ? atan(vQ.y, vQ.x) : 0.0;",
|
||||
"",
|
||||
" // Regular-polygon aperture boundary: circumradius 1 at a blade",
|
||||
" // vertex, cos(PI/n) at a blade midpoint.",
|
||||
" float n = uBlades;",
|
||||
" float seg = 2.0 * PI / n;",
|
||||
" float poly = cos(PI / n) / cos(mod(th, seg) - PI / n);",
|
||||
" float bound = mix(1.0, poly, vShape);",
|
||||
"",
|
||||
" float cov = smoothstep(bound, bound - vAA, r);",
|
||||
" // Two offset barrel openings cut the disc from opposite sides,",
|
||||
" // which is what turns a corner bokeh into a cat's eye.",
|
||||
" float cut = vCat * vShape;",
|
||||
" cov *= smoothstep(1.0, 1.0 - vAA, length(vQ - vRadial * cut));",
|
||||
" cov *= smoothstep(1.0, 1.0 - vAA, length(vQ + vRadial * cut));",
|
||||
"",
|
||||
" gl_FragColor = vec4(vColor * (vGain * cov), 1.0);",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
var uni = {};
|
||||
var ready = false;
|
||||
var count = 0;
|
||||
|
||||
function compile(type, src) {
|
||||
var sh = gl.createShader(type);
|
||||
gl.shaderSource(sh, src);
|
||||
gl.compileShader(sh);
|
||||
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
||||
throw new Error("rack-focus shader: " + gl.getShaderInfoLog(sh));
|
||||
}
|
||||
return sh;
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
var h = String(hex).trim().replace("#", "");
|
||||
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
||||
var v = parseInt(h, 16);
|
||||
if (!isFinite(v)) return [0.02, 0.024, 0.039];
|
||||
return [((v >> 16) & 255) / 255, ((v >> 8) & 255) / 255, (v & 255) / 255];
|
||||
}
|
||||
var BG = hexToRgb(BACKDROP);
|
||||
|
||||
if (gl) {
|
||||
var prog = gl.createProgram();
|
||||
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
|
||||
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
|
||||
gl.linkProgram(prog);
|
||||
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
||||
throw new Error("rack-focus link: " + gl.getProgramInfoLog(prog));
|
||||
}
|
||||
gl.useProgram(prog);
|
||||
|
||||
// Six vertices per light: two triangles carrying the same point
|
||||
// payload and four distinct corner offsets.
|
||||
var CORNERS = [
|
||||
[-1, -1],
|
||||
[1, -1],
|
||||
[1, 1],
|
||||
[-1, -1],
|
||||
[1, 1],
|
||||
[-1, 1],
|
||||
];
|
||||
var n = P.length / 8;
|
||||
count = n * 6;
|
||||
var STRIDE = 10;
|
||||
var data = new Float32Array(count * STRIDE);
|
||||
var w = 0;
|
||||
for (var pi = 0; pi < n; pi++) {
|
||||
var o = pi * 8;
|
||||
for (var ci = 0; ci < 6; ci++) {
|
||||
data[w++] = CORNERS[ci][0];
|
||||
data[w++] = CORNERS[ci][1];
|
||||
data[w++] = P[o];
|
||||
data[w++] = P[o + 1];
|
||||
data[w++] = P[o + 2];
|
||||
data[w++] = P[o + 3];
|
||||
data[w++] = P[o + 4];
|
||||
data[w++] = P[o + 5];
|
||||
data[w++] = P[o + 6];
|
||||
data[w++] = P[o + 7];
|
||||
}
|
||||
}
|
||||
|
||||
var buf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
|
||||
var BYTES = STRIDE * 4;
|
||||
[
|
||||
["aCorner", 2, 0],
|
||||
["aPos", 3, 8],
|
||||
["aSize", 2, 20],
|
||||
["aColor", 3, 28],
|
||||
].forEach(function (spec) {
|
||||
var loc = gl.getAttribLocation(prog, spec[0]);
|
||||
gl.enableVertexAttribArray(loc);
|
||||
gl.vertexAttribPointer(loc, spec[1], gl.FLOAT, false, BYTES, spec[2]);
|
||||
});
|
||||
|
||||
[
|
||||
"uRes",
|
||||
"uPxPerMm",
|
||||
"uFocal",
|
||||
"uFocus",
|
||||
"uFstop",
|
||||
"uCoCmm",
|
||||
"uMaxR",
|
||||
"uMinR",
|
||||
"uCatsEye",
|
||||
"uBlades",
|
||||
"uBokeh",
|
||||
].forEach(function (nm) {
|
||||
uni[nm] = gl.getUniformLocation(prog, nm);
|
||||
});
|
||||
|
||||
gl.viewport(0, 0, W, H);
|
||||
gl.uniform2f(uni.uRes, W, H);
|
||||
gl.uniform1f(uni.uPxPerMm, PX_PER_MM);
|
||||
gl.uniform1f(uni.uFocal, FOCAL);
|
||||
gl.uniform1f(uni.uFstop, FSTOP);
|
||||
gl.uniform1f(uni.uCoCmm, COC_MM);
|
||||
gl.uniform1f(uni.uMaxR, MAX_COC_R_PX);
|
||||
gl.uniform1f(uni.uMinR, MIN_R_PX);
|
||||
gl.uniform1f(uni.uCatsEye, CATSEYE);
|
||||
gl.uniform1f(uni.uBlades, BLADES);
|
||||
gl.uniform1f(uni.uBokeh, BOKEH);
|
||||
|
||||
// Light adds to light. Overlapping bokeh discs are brighter where
|
||||
// they cross, which is the whole texture of a bokeh field.
|
||||
gl.disable(gl.DEPTH_TEST);
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.ONE, gl.ONE);
|
||||
ready = true;
|
||||
}
|
||||
|
||||
// Every frame is computed from t alone: the focal distance is a
|
||||
// closed-form function of t, and each sprite's circle of confusion
|
||||
// falls out of its own static depth and that distance.
|
||||
function draw(t) {
|
||||
if (!ready) return;
|
||||
gl.uniform1f(uni.uFocus, focusAt(t));
|
||||
gl.clearColor(BG[0], BG[1], BG[2], 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, count);
|
||||
gl.flush();
|
||||
}
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
// The canvas is repainted from a property SETTER, not from onUpdate:
|
||||
// gsap's seek(t) suppresses events by default, so an onUpdate callback
|
||||
// silently never fires on a scrub and the canvas freezes on frame 0.
|
||||
// Tweened values are always written during render, suppressed or not,
|
||||
// so this fires on every seek, and hands us the frame time directly.
|
||||
var driver = { _t: 0 };
|
||||
Object.defineProperty(driver, "t", {
|
||||
get: function () {
|
||||
return this._t;
|
||||
},
|
||||
set: function (v) {
|
||||
this._t = v;
|
||||
draw(v);
|
||||
},
|
||||
});
|
||||
tl.to(driver, { t: DUR, duration: DUR, ease: "none" }, 0);
|
||||
window.__timelines["rack-focus"] = tl;
|
||||
|
||||
draw(0);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
</VariablesExplorer>
|
||||
|
||||
## Install
|
||||
|
||||
<InstallCommand command="npx hyperframes add rack-focus" />
|
||||
|
||||
That writes one file: `compositions/rack-focus.html`.
|
||||
|
||||
## Add it to your video
|
||||
|
||||
It runs for 6 seconds at 1920×1080. Paste this into your composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="rack-focus"
|
||||
data-composition-src="compositions/rack-focus.html"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
Move it in time with `data-start`. Put it on a different timeline row with
|
||||
`data-track-index`. See [data attributes](/concepts/data-attributes) for the rest.
|
||||
|
||||
## Change how it looks
|
||||
|
||||
Set these CSS variables on the block:
|
||||
|
||||
- `--nearfocus` — Near focal distance in metres (where the pull starts and the near subject sits). Defaults to `1.2`.
|
||||
- `--farfocus` — Far focal distance in metres (where the pull ends and the far subject sits). Defaults to `80`.
|
||||
- `--focallength` — Focal length in mm (longer lens = shallower depth of field). Defaults to `85`.
|
||||
- `--aperture` — Aperture f-number (lower = bigger bokeh). Defaults to `1.8`.
|
||||
- `--blades` — Aperture blade count, the bokeh polygon's sides. Defaults to `6`.
|
||||
- `--catseye` — Cat's-eye clipping at the frame corners (mechanical vignetting). Defaults to `0.62`.
|
||||
- `--pullstart` — Seconds held on the near subject before the pull starts. Defaults to `1.2`.
|
||||
- `--pullduration` — Pull duration in seconds. Defaults to `3.2`.
|
||||
- `--pullease` — Pull easing (any GSAP ease name). Defaults to `power2.inOut`.
|
||||
- `--bokeh` — Bokeh exposure. Defaults to `1`.
|
||||
- `--backdrop` — Backdrop. Defaults to `#05060a`.
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `webgl` `shader` `camera` `depth` `cinematic` `showcase`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
||||
- [Build a richer composition](/go-further)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user