mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-15 13:58:20 +00:00
merge: pull in the fallow-audit fixes
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "hyperframes",
|
||||
"description": "HyperFrames by HeyGen. Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames.",
|
||||
"version": "0.7.91",
|
||||
"version": "0.7.107",
|
||||
"author": {
|
||||
"name": "HeyGen",
|
||||
"email": "hyperframes@heygen.com",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "hyperframes",
|
||||
"description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames.",
|
||||
"version": "0.7.91",
|
||||
"version": "0.7.107",
|
||||
"author": {
|
||||
"name": "HeyGen",
|
||||
"email": "hyperframes@heygen.com",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "hyperframes",
|
||||
"displayName": "HyperFrames by HeyGen",
|
||||
"description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames.",
|
||||
"version": "0.7.91",
|
||||
"version": "0.7.107",
|
||||
"author": {
|
||||
"name": "HeyGen",
|
||||
"email": "hyperframes@heygen.com"
|
||||
|
||||
@@ -199,6 +199,17 @@
|
||||
"produceDrawElementFrame",
|
||||
],
|
||||
},
|
||||
// External-conflict persistence is the #2990 stack boundary. The coordinator
|
||||
// consumes these exports in child PR #2991; keep the primitive independently reviewable.
|
||||
{
|
||||
"file": "packages/studio/src/utils/externalConflictStorage.ts",
|
||||
"exports": [
|
||||
"persistExternalConflictSnapshot",
|
||||
"persistExternalFailureSnapshot",
|
||||
"loadExternalConflictSnapshot",
|
||||
"deleteExternalConflictSnapshot",
|
||||
],
|
||||
},
|
||||
// CLI command files: every command exports a const `examples` per the
|
||||
// convention documented in CLAUDE.md. This is a namespace barrel, not a
|
||||
// collision.
|
||||
@@ -570,6 +581,12 @@
|
||||
// this PR only teaches the server scan to prefer its reported PID, but that
|
||||
// line shift makes fallow re-flag the inherited probe clones.
|
||||
"packages/cli/src/server/portUtils.ts",
|
||||
// iframe.test.ts: the remaining clone groups are pre-existing per-case arrange
|
||||
// blocks in the selection and draft-loop suites (build an adapter, wire a spy,
|
||||
// act). Appending the paint-query suite shifts their line numbers and re-flags
|
||||
// them; each block states its own setup on purpose, which a shared fixture
|
||||
// would hide.
|
||||
"packages/sdk/src/adapters/iframe.test.ts",
|
||||
// gsapParserAcorn.motionEval.test.ts: parallel arrange/act/assert cases for
|
||||
// the staggered-collection honesty pass (.from reveal vs .to landing on the
|
||||
// rest pose). Each asserts a distinct keyframe shape; collapsing the shared
|
||||
@@ -660,6 +677,28 @@
|
||||
// inherited clone; consistent with the norm above of leaving parallel
|
||||
// command-test cases unabstracted.
|
||||
"packages/cli/src/commands/check.test.ts",
|
||||
// canary.test.ts: rawFnv is a deliberate independent re-implementation of
|
||||
// canary.ts's fnv1a32, not copy-paste — its own docstring explains why:
|
||||
// the test re-derives the hash so it can cross-check canaryBucket against
|
||||
// a copy that owes it nothing, rather than importing the function under
|
||||
// test. Importing fnv1a32 here would defeat the point of the assertion.
|
||||
"packages/core/src/canary.test.ts",
|
||||
// The audio FX property-panel test files (and the two siblings outside
|
||||
// this stack, propertyPanelFlatMotionSection.test.tsx and
|
||||
// propertyPanelFlatEffectsSection.test.tsx) share a `renderInto` React
|
||||
// mount helper — pre-existing across nine files, not introduced here.
|
||||
// The FxSection/FlatTextSection `mount()` wrappers additionally look
|
||||
// alike because both build a props-with-overrides harness for their own
|
||||
// component; the prop shapes differ per component, so a shared mount
|
||||
// abstraction would obscure more than it dedupes (same rationale as
|
||||
// slideshowPanelHelpers.ts above).
|
||||
"packages/studio/src/components/editor/PropertyPanelEmptyState.test.tsx",
|
||||
"packages/studio/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx",
|
||||
"packages/studio/src/components/editor/propertyPanelFlatLayoutSection.test.tsx",
|
||||
"packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx",
|
||||
"packages/studio/src/components/editor/propertyPanelFlatTextSection.test.tsx",
|
||||
"packages/studio/src/components/editor/propertyPanelFlatToggle.test.tsx",
|
||||
"packages/studio/src/components/editor/propertyPanelFxSection.test.tsx",
|
||||
],
|
||||
},
|
||||
"health": {
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Talk with the HyperFrames team
|
||||
url: https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ2cSpKoDgmcmRrgekrnrgqmvPT8W6F2Zg6e7MY7IJqaZKwpn_I0NdTHkN390iguMepE_NVg8ezb?gv=true
|
||||
about: Share what you are building and help us improve the developer experience in a casual 30-minute conversation.
|
||||
|
||||
@@ -58,16 +58,41 @@ jobs:
|
||||
with:
|
||||
chrome-version: stable
|
||||
|
||||
# The renderer shells out to ffmpeg for both halves of a preview: the
|
||||
# poster transcode and the web encode of the mp4. Neither ran here before
|
||||
# (`--skip-video` skipped the encode, and the poster copy was a plain
|
||||
# file copy), so the job never needed it and ubuntu-latest does not ship
|
||||
# it.
|
||||
- name: Install ffmpeg
|
||||
run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends ffmpeg
|
||||
|
||||
- name: Render changed block/component previews
|
||||
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)
|
||||
|
||||
# A renderer change reaches every item, so it cannot be trusted to a
|
||||
# PR that happens to also touch a block. Two canaries cover the two
|
||||
# shapes the renderer has to tell apart: a block whose scene lives in
|
||||
# a <template> (mounted through a wrapper) and one that registers its
|
||||
# timeline at body level (rendered directly). Getting that wrong is
|
||||
# silent — the wrong-shaped block renders blank, not red.
|
||||
RENDERER_CHANGED=$(git diff --name-only --diff-filter=ACMR "$BASE_SHA"...HEAD \
|
||||
-- scripts/generate-catalog-previews.ts scripts/registry-target-paths.mjs)
|
||||
if [ -n "$RENDERER_CHANGED" ]; then
|
||||
CHANGED_ITEMS=$(printf '%s\n' $CHANGED_ITEMS \
|
||||
code-snippet-visual-studio-dark code-snippet-apple-terminal-pro | sort -u)
|
||||
fi
|
||||
|
||||
if [ -z "$CHANGED_ITEMS" ]; then
|
||||
echo "No block/component changes detected."
|
||||
exit 0
|
||||
|
||||
+49
-15
@@ -34,6 +34,7 @@ jobs:
|
||||
code: ${{ steps.filter.outputs.code }}
|
||||
cli: ${{ steps.filter.outputs.cli }}
|
||||
skills: ${{ steps.filter.outputs.skills }}
|
||||
catalog_index: ${{ steps.filter.outputs.catalog_index }}
|
||||
codex_plugin: ${{ steps.filter.outputs.codex_plugin }}
|
||||
gcp_beginframe: ${{ steps.filter.outputs.gcp_beginframe }}
|
||||
studio: ${{ steps.filter.outputs.studio }}
|
||||
@@ -44,11 +45,18 @@ jobs:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Reject accidental file deletions
|
||||
if: github.event_name == 'pull_request'
|
||||
run: node scripts/check-no-main-deletions.mjs --base origin/main
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
|
||||
id: filter
|
||||
with:
|
||||
token: ""
|
||||
filters: |
|
||||
catalog_index:
|
||||
- "registry/registry.json"
|
||||
- "registry/catalog-artifact/**"
|
||||
- "scripts/catalog/check-artifact-coverage.ts"
|
||||
code:
|
||||
- "packages/**"
|
||||
- "scripts/**"
|
||||
@@ -129,7 +137,7 @@ jobs:
|
||||
- run: corepack enable
|
||||
- run: corepack prepare pnpm@10.17.1 --activate
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
- run: bun run build
|
||||
- run: bun run verify:packed-manifests
|
||||
|
||||
@@ -148,7 +156,7 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
- run: bun run lint
|
||||
|
||||
# `fallow audit` runs dead-code + complexity + duplication analysis scoped to
|
||||
@@ -182,7 +190,7 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
- name: Run fallow audit
|
||||
id: audit
|
||||
# `bun install` above made `bunx fallow` resolve from node_modules, so
|
||||
@@ -238,7 +246,7 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
- run: bun run format:check
|
||||
|
||||
typecheck:
|
||||
@@ -256,9 +264,10 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
- run: bun run build
|
||||
- run: bun run --filter '*' typecheck
|
||||
- run: bun run typecheck:scripts
|
||||
|
||||
test:
|
||||
name: Test
|
||||
@@ -283,11 +292,14 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun run test:scripts
|
||||
- run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
- 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:
|
||||
@@ -314,7 +326,7 @@ jobs:
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends ffmpeg
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
- run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
|
||||
- run: bun run --cwd packages/core build
|
||||
- run: bun run --filter @hyperframes/engine build
|
||||
@@ -359,6 +371,21 @@ jobs:
|
||||
# `gen:skills-manifest --check`, which compares per-skill content hashes; the
|
||||
# manifest carries no version/timestamp, so it only fails on real content
|
||||
# drift. bun runs the TS script directly, no install needed.
|
||||
catalog-index-coverage:
|
||||
name: "Catalog: search index covers the registry"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.catalog_index == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
# Comparing two name lists needs neither the embedding model nor a
|
||||
# network call, so this stays a seconds-long gate. Regenerating the
|
||||
# vectors does need the model, which is why CI checks rather than fixes.
|
||||
- name: Verify every searchable registry item has a vector
|
||||
run: bun scripts/catalog/check-artifact-coverage.ts
|
||||
|
||||
skills-manifest:
|
||||
name: "Skills: manifest in sync"
|
||||
needs: changes
|
||||
@@ -422,7 +449,7 @@ jobs:
|
||||
node-version: 22
|
||||
- name: Install dependencies
|
||||
if: runner.os != 'Windows'
|
||||
run: bun install --frozen-lockfile --ignore-scripts
|
||||
run: bash scripts/ci/install-workspace-dependencies.sh --ignore-scripts
|
||||
- name: Install dependencies
|
||||
if: runner.os == 'Windows'
|
||||
run: bun install --frozen-lockfile --ignore-scripts --linker=hoisted
|
||||
@@ -437,7 +464,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
# Build workspace deps so the sdk's @hyperframes/parsers + core subpath
|
||||
# imports resolve via the "node" export condition (dist) under vitest.
|
||||
- run: bun run --filter '@hyperframes/parsers' build
|
||||
@@ -459,7 +486,7 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
# Runtime coverage now imports core modules that consume workspace
|
||||
# subpaths. Build their dist exports before Vitest resolves them.
|
||||
- run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
|
||||
@@ -480,7 +507,7 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
# Build workspace deps so the studio vite.config.ts (loaded by Node) can
|
||||
# resolve @hyperframes/core and @hyperframes/studio-server via the "node"
|
||||
# export condition (dist).
|
||||
@@ -523,7 +550,7 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
# Same reason as studio-load-smoke: vite.config.ts is loaded by Node and
|
||||
# resolves the workspace packages through their "node" export condition.
|
||||
- run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
|
||||
@@ -634,7 +661,7 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
- run: bun run build
|
||||
|
||||
# Pack the CLI as a tarball (simulates what `npm publish` produces)
|
||||
@@ -711,7 +738,7 @@ jobs:
|
||||
sudo apt-get install -y ffmpeg
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
run: bash scripts/ci/install-workspace-dependencies.sh
|
||||
- name: Build monorepo
|
||||
run: bun run build
|
||||
|
||||
@@ -755,6 +782,13 @@ jobs:
|
||||
|
||||
test -s /tmp/hf-cli-inside/renders/inside.mp4
|
||||
|
||||
# Belongs in this job because it needs the BUILT CLI: `shouldTrack()`
|
||||
# short-circuits whenever `isDevMode()` is true, which it is for any `.ts`
|
||||
# entry, so under vitest a real event and no event look identical. Sends
|
||||
# nothing to PostHog — the script wraps fetch and captures the batch.
|
||||
- name: Smoke-test `add` telemetry end to end
|
||||
run: node scripts/ci/cli-telemetry-e2e.mjs
|
||||
|
||||
- name: Assert page.goto completes under 5s budget
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -66,7 +66,7 @@ packages/
|
||||
player/ → Embeddable <hyperframes-player> web component
|
||||
producer/ → Full rendering pipeline (capture + encode + audio mix)
|
||||
shader-transitions/ → WebGL shader transitions for compositions
|
||||
studio/ → Browser-based composition editor UI
|
||||
studio/ → Browser-based composition editor UI (read packages/studio/AGENTS.md first)
|
||||
registry/
|
||||
blocks/ → Installable sub-composition scenes (50+)
|
||||
components/ → Installable effects and snippets
|
||||
|
||||
@@ -97,7 +97,7 @@ packages/
|
||||
player/ → Embeddable <hyperframes-player> web component
|
||||
producer/ → Full rendering pipeline (capture + encode + audio mix)
|
||||
shader-transitions/ → WebGL shader transitions for compositions
|
||||
studio/ → Browser-based composition editor UI
|
||||
studio/ → Browser-based composition editor UI (read packages/studio/AGENTS.md first)
|
||||
registry/
|
||||
blocks/ → Installable sub-composition scenes (50+)
|
||||
components/ → Installable effects and snippets
|
||||
|
||||
+29
-4
@@ -91,11 +91,36 @@ Blocks don't need `demo.html` — they are already standalone compositions.
|
||||
|
||||
### Checklist for new items
|
||||
|
||||
**Anyone can add an item.** Nothing here needs commit access, and the two steps
|
||||
that do need something a contributor may not have are handled by a maintainer
|
||||
before merge, listed at the end.
|
||||
|
||||
1. Create `registry/<blocks|components>/<name>/registry-item.json` following the [schema](packages/core/schemas/registry-item.json)
|
||||
2. Add the item to `registry/registry.json`
|
||||
3. For components: include a `demo.html`
|
||||
4. Run `npx hyperframes lint` and `npx hyperframes validate` on your HTML
|
||||
5. Test the install flow: `hyperframes add <name> --dir /tmp/test-project`
|
||||
2. For components: include a `demo.html`
|
||||
3. Run `npx hyperframes lint` and `npx hyperframes validate` on your HTML
|
||||
4. Test the install flow: `hyperframes add <name> --dir /tmp/test-project`
|
||||
5. Regenerate the manifest: `npx tsx scripts/generate-registry-items.ts`
|
||||
|
||||
`registry/registry.json` is generated from the item directories, so edit it with
|
||||
that script rather than by hand. An entry added by hand survives until the next
|
||||
regeneration and then disappears; entries left behind for directories that no
|
||||
longer exist are worse, because `hyperframes add <name>` resolves the name and
|
||||
then fails on missing files.
|
||||
|
||||
### What a maintainer finishes for you
|
||||
|
||||
Two things need assets an outside contributor is not expected to install. Open
|
||||
the pull request without them and say so; neither blocks review.
|
||||
|
||||
| Thing | If you have it | If you do not |
|
||||
| ----------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| The search index (`registry/catalog-artifact/`) | The pre-commit hook rebuilds and stages it | The hook skips, CI names the gap, a maintainer regenerates before merge |
|
||||
| The catalog preview image | Internal contributors run `scripts/upload-docs-images.sh` | Attach the preview MP4 to the PR instead |
|
||||
|
||||
The search index needs a 32 MB embedding model, which is an opt-in for catalog
|
||||
search rather than a build dependency. Until it is regenerated your item is
|
||||
findable by word search and not by meaning, which is the same state as any item
|
||||
published since a user last refreshed their copy.
|
||||
|
||||
### Auto-generated docs
|
||||
|
||||
|
||||
+1
-2
@@ -91,8 +91,7 @@ After CLI commands, show what the user should see:
|
||||
|
||||
```bash
|
||||
npx hyperframes preview
|
||||
# ✓ Server running at http://localhost:3000
|
||||
# ✓ Watching for changes...
|
||||
# Studio running at http://localhost:3002
|
||||
```
|
||||
|
||||
### Use CodeGroup for multi-platform commands
|
||||
|
||||
@@ -313,6 +313,7 @@ HyperFrames is used in production at [HeyGen](https://www.heygen.com), with comm
|
||||
|
||||
- Questions and ideas: [Discord](https://discord.gg/EbK98HBPdk)
|
||||
- Bugs and feature requests: [GitHub Issues](https://github.com/heygen-com/hyperframes/issues)
|
||||
- User research: [Book a casual 30-minute conversation with the HyperFrames team](https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ2cSpKoDgmcmRrgekrnrgqmvPT8W6F2Zg6e7MY7IJqaZKwpn_I0NdTHkN390iguMepE_NVg8ezb?gv=true) — no preparation or sales pitch
|
||||
- Security reports: [SECURITY.md](SECURITY.md)
|
||||
- Contributions: [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
|
||||
@@ -18,12 +18,13 @@
|
||||
"oxlint": "^1.56.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^3.2.4",
|
||||
"yaml": "^2.9.0",
|
||||
},
|
||||
},
|
||||
"packages/aws-lambda": {
|
||||
"name": "@hyperframes/aws-lambda",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.105",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.700.0",
|
||||
"@aws-sdk/client-sfn": "^3.700.0",
|
||||
@@ -56,7 +57,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@hyperframes/cli",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.105",
|
||||
"bin": {
|
||||
"hyperframes": "./bin/hyperframes.mjs",
|
||||
},
|
||||
@@ -106,7 +107,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@hyperframes/core",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.105",
|
||||
"dependencies": {
|
||||
"@chenglou/pretext": "^0.0.5",
|
||||
"@hyperframes/lint": "workspace:*",
|
||||
@@ -131,7 +132,7 @@
|
||||
},
|
||||
"packages/engine": {
|
||||
"name": "@hyperframes/engine",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.105",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^2.0.5",
|
||||
"@hyperframes/core": "workspace:^",
|
||||
@@ -150,7 +151,7 @@
|
||||
},
|
||||
"packages/gcp-cloud-run": {
|
||||
"name": "@hyperframes/gcp-cloud-run",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.105",
|
||||
"dependencies": {
|
||||
"@google-cloud/storage": "^7.14.0",
|
||||
"@google-cloud/workflows": "^4.2.0",
|
||||
@@ -171,7 +172,7 @@
|
||||
},
|
||||
"packages/lint": {
|
||||
"name": "@hyperframes/lint",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.105",
|
||||
"dependencies": {
|
||||
"@hyperframes/parsers": "workspace:*",
|
||||
"htmlparser2": "^10.1.0",
|
||||
@@ -189,7 +190,7 @@
|
||||
},
|
||||
"packages/parsers": {
|
||||
"name": "@hyperframes/parsers",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.105",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.0",
|
||||
"acorn": "^8.17.0",
|
||||
@@ -209,7 +210,7 @@
|
||||
},
|
||||
"packages/player": {
|
||||
"name": "@hyperframes/player",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.105",
|
||||
"dependencies": {
|
||||
"@hyperframes/core": "workspace:*",
|
||||
},
|
||||
@@ -224,7 +225,7 @@
|
||||
},
|
||||
"packages/producer": {
|
||||
"name": "@hyperframes/producer",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.105",
|
||||
"dependencies": {
|
||||
"@fontsource/archivo-black": "^5.2.8",
|
||||
"@fontsource/eb-garamond": "^5.2.7",
|
||||
@@ -269,7 +270,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@hyperframes/sdk",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.105",
|
||||
"dependencies": {
|
||||
"@hyperframes/core": "workspace:*",
|
||||
"@hyperframes/parsers": "workspace:*",
|
||||
@@ -299,7 +300,7 @@
|
||||
},
|
||||
"packages/shader-transitions": {
|
||||
"name": "@hyperframes/shader-transitions",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.105",
|
||||
"dependencies": {
|
||||
"html2canvas": "^1.4.1",
|
||||
},
|
||||
@@ -311,7 +312,7 @@
|
||||
},
|
||||
"packages/studio": {
|
||||
"name": "@hyperframes/studio",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.105",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.20.1",
|
||||
"@codemirror/commands": "^6.10.3",
|
||||
@@ -343,6 +344,8 @@
|
||||
"@types/react-dom": "19",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"chokidar": "^4.0.3",
|
||||
"fake-indexeddb": "^6.2.5",
|
||||
"postcss": "^8.4.0",
|
||||
"puppeteer-core": "^25.2.1",
|
||||
"tailwindcss": "^3.4.0",
|
||||
@@ -360,7 +363,7 @@
|
||||
},
|
||||
"packages/studio-server": {
|
||||
"name": "@hyperframes/studio-server",
|
||||
"version": "0.7.78",
|
||||
"version": "0.7.105",
|
||||
"dependencies": {
|
||||
"@hyperframes/core": "workspace:*",
|
||||
"@hyperframes/parsers": "workspace:*",
|
||||
@@ -1487,6 +1490,8 @@
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fake-indexeddb": ["fake-indexeddb@6.2.5", "", {}, "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w=="],
|
||||
|
||||
"fallow": ["fallow@2.75.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@fallow-cli/darwin-arm64": "2.75.0", "@fallow-cli/darwin-x64": "2.75.0", "@fallow-cli/linux-arm64-gnu": "2.75.0", "@fallow-cli/linux-arm64-musl": "2.75.0", "@fallow-cli/linux-x64-gnu": "2.75.0", "@fallow-cli/linux-x64-musl": "2.75.0", "@fallow-cli/win32-arm64-msvc": "2.75.0", "@fallow-cli/win32-x64-msvc": "2.75.0" }, "bin": { "fallow": "bin/fallow", "fallow-lsp": "bin/fallow-lsp", "fallow-mcp": "bin/fallow-mcp" } }, "sha512-0/2cquNI/cDLP/LzcCbkwI4hMzkX4tE0VY3/69n3PBBeqFpbM2oai+2Cb0sB8dXB8MDUGPVoPJjDW5GiUo7a1A=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,57 +3,371 @@ title: "Apple Money Count"
|
||||
description: "Apple-style finance counter that counts from $0 to $10,000, flashes green, and bursts money icons with sound."
|
||||
---
|
||||
|
||||
`showcase` `finance` `kinetic` `youtube` `sfx`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="apple-money-count preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/apple-money-count.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/apple-money-count.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Install
|
||||
|
||||
<InstallCommand command="npx hyperframes add apple-money-count" item="apple-money-count" />
|
||||
|
||||
That writes `compositions/apple-money-count.html`, plus 1 supporting file under `assets/`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`apple-money-count.html`}>
|
||||
|
||||
```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-root="true"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
</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 */}
|
||||
|
||||
Tagged `showcase` `finance` `kinetic` `youtube` `sfx`.
|
||||
|
||||
Created by [Stronkter](https://x.com/Stronkter).
|
||||
|
||||
## Source Prompt
|
||||
<Accordion title="The prompt this was built from">
|
||||
|
||||
```text
|
||||
📷HyperFrames by HeyGen Make me a five-second video of, on a white background, of a Apple-style bold font counting from $0 to $10,000. Once it counts to $10,000, it changes to a green color and the screen also flashes green for a second, and then money icons come out of the $10,000 amount all over the screen and then disappear.
|
||||
```
|
||||
|
||||
<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 />
|
||||
|
||||
## Add it to a project
|
||||
|
||||
### Ask your agent
|
||||
|
||||
```text
|
||||
Add the Apple Money Count block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
|
||||
```bash
|
||||
npx hyperframes add apple-money-count
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 5s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `apple-money-count.html` | `compositions/apple-money-count.html` | hyperframes:composition |
|
||||
| `assets/sfx-production.wav` | `assets/sfx-production.wav` | hyperframes:asset |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
|
||||
```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>
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
## Related topics
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,49 +3,939 @@ title: "Beat Freeze Cut"
|
||||
description: "A beat-driven speed ramp, freeze-frame hit, and hard-cut sequence for music-led promos and montages."
|
||||
---
|
||||
|
||||
`transition` `music` `beat-driven` `freeze-frame`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="beat-freeze-cut preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/beat-freeze-cut.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/beat-freeze-cut.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add beat-freeze-cut" item="beat-freeze-cut" />
|
||||
|
||||
```text
|
||||
Add the Beat Freeze Cut block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/beat-freeze-cut.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="beat-freeze-cut"
|
||||
data-composition-src="compositions/beat-freeze-cut.html"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
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.
|
||||
|
||||
```bash
|
||||
npx hyperframes add beat-freeze-cut
|
||||
```
|
||||
## Change the colors
|
||||
|
||||
## Details
|
||||
Set these CSS variables on the block:
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 6s |
|
||||
- `--bg-color` — Background. Defaults to `#0B0D10`.
|
||||
- `--accent-color` — Accent. Defaults to `#00E5C7`.
|
||||
|
||||
**Installed files**
|
||||
## Source
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `beat-freeze-cut.html` | `compositions/beat-freeze-cut.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`beat-freeze-cut.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="beat-freeze-cut" data-composition-src="compositions/beat-freeze-cut.html" data-start="0" data-duration="6" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,49 +3,216 @@ title: "Camcorder HUD"
|
||||
description: "Responsive REC, battery, editable date placeholder, and timeline-driven counter overlay for creator-camera and camcorder treatments"
|
||||
---
|
||||
|
||||
`camcorder` `camera` `recording` `hud` `overlay` `creator` `media-treatment-overlay`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="camcorder-hud preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/camcorder-hud.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/camcorder-hud.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add camcorder-hud" item="camcorder-hud" />
|
||||
|
||||
```text
|
||||
Add the Camcorder HUD block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/camcorder-hud.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="camcorder-hud"
|
||||
data-composition-src="compositions/camcorder-hud.html"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
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.
|
||||
|
||||
```bash
|
||||
npx hyperframes add camcorder-hud
|
||||
```
|
||||
## Source
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `camcorder-hud.html` | `compositions/camcorder-hud.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`camcorder-hud.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="camcorder-hud" data-composition-src="compositions/camcorder-hud.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -0,0 +1,794 @@
|
||||
---
|
||||
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/blocks/camera-dolly-zoom.json"
|
||||
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" item="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.
|
||||
|
||||
## Variables
|
||||
|
||||
Every one of these has a default, so the piece works untouched. Set the ones you
|
||||
want to change on the element:
|
||||
|
||||
| Variable | Default | Accepts | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| `direction` | `out` | `out`, `in` | |
|
||||
| `strength` | `2` | 1.1 to 4, step 0.1 | |
|
||||
| `subjectDistance` | `1400` | 600px to 3000px, step 50px | |
|
||||
| `easing` | `power2.inOut` | `none`, `sine.inOut`, `power1.inOut`, `power2.inOut`, `power3.inOut` | |
|
||||
|
||||
Set them with `data-variable-values` on the element that mounts it. These are the
|
||||
defaults, so this behaves exactly like the preview above until you change one:
|
||||
|
||||
```html wrap
|
||||
<div
|
||||
data-composition-id="camera-dolly-zoom"
|
||||
data-composition-src="compositions/camera-dolly-zoom.html"
|
||||
data-variable-values='{"direction":"out","strength":2,"subjectDistance":1400,"easing":"power2.inOut"}'
|
||||
></div>
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`camera-dolly-zoom.html`}>
|
||||
|
||||
```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>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* 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,49 +3,408 @@ title: "Chromatic Radial Split"
|
||||
description: "Shader transition with chromatic aberration radial split"
|
||||
---
|
||||
|
||||
`transition` `shader`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="chromatic-radial-split preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/chromatic-radial-split.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/chromatic-radial-split.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add chromatic-radial-split" item="chromatic-radial-split" />
|
||||
|
||||
```text
|
||||
Add the Chromatic Radial Split block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/chromatic-radial-split.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`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-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add chromatic-radial-split
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `chromatic-radial-split.html` | `compositions/chromatic-radial-split.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `transition` `shader`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,408 @@ title: "Cinematic Zoom"
|
||||
description: "Shader transition with dramatic zoom blur"
|
||||
---
|
||||
|
||||
`transition` `shader`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="cinematic-zoom preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/cinematic-zoom.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/cinematic-zoom.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add cinematic-zoom" item="cinematic-zoom" />
|
||||
|
||||
```text
|
||||
Add the Cinematic Zoom block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/cinematic-zoom.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`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-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add cinematic-zoom
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `cinematic-zoom.html` | `compositions/cinematic-zoom.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `transition` `shader`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,655 @@ 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."
|
||||
---
|
||||
|
||||
`code` `code-animation` `3d` `webgl` `developer`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-3d-extrude preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-3d-extrude.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-3d-extrude.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-3d-extrude" item="code-3d-extrude" />
|
||||
|
||||
```text
|
||||
Add the Code 3D Extrude block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/code-3d-extrude.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`code-3d-extrude.html`}>
|
||||
|
||||
```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-start="0"
|
||||
data-duration="8"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-3d-extrude
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-3d-extrude.html` | `compositions/code-3d-extrude.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `code` `code-animation` `3d` `webgl` `developer`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,858 @@ title: "Code Diff"
|
||||
description: "An edit shown as a colored diff — removed lines collapse in red, added lines expand in green."
|
||||
---
|
||||
|
||||
`code` `code-animation` `diff` `developer`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-diff preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-diff.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-diff.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-diff" item="code-diff" />
|
||||
|
||||
```text
|
||||
Add the Code Diff block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/code-diff.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`code-diff.html`}>
|
||||
|
||||
```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-start="0"
|
||||
data-duration="6"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="bg-field" data-layout-ignore></div>
|
||||
<div class="bg-grid" data-layout-ignore></div>
|
||||
<div class="bg-glow a" data-layout-ignore></div>
|
||||
<div class="bg-glow b" data-layout-ignore></div>
|
||||
<div class="editor" id="editor" data-layout-ignore>
|
||||
<div class="titlebar">
|
||||
<div class="dots">
|
||||
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
|
||||
</div>
|
||||
<span class="filename"><span class="accent">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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-diff
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 6s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-diff.html` | `compositions/code-diff.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `code` `code-animation` `diff` `developer`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -1,51 +1,789 @@
|
||||
---
|
||||
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."
|
||||
---
|
||||
|
||||
`code` `code-animation` `highlight` `developer`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-highlight preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-highlight.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-highlight.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-highlight" item="code-highlight" />
|
||||
|
||||
```text
|
||||
Add the Code Highlight Sweep block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/code-highlight.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`code-highlight.html`}>
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Code Highlight Sweep</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #05070b;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
color: #e6edf3;
|
||||
}
|
||||
.bg-field {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(1200px 700px at 50% 18%, #0e1726 0%, #070b12 55%, #05070b 100%);
|
||||
}
|
||||
.bg-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(88, 166, 255, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(88, 166, 255, 0.05) 1px, transparent 1px);
|
||||
background-size: 64px 64px;
|
||||
mask-image: radial-gradient(1100px 620px at 50% 42%, #000 0%, transparent 78%);
|
||||
}
|
||||
.bg-glow {
|
||||
position: absolute;
|
||||
width: 900px;
|
||||
height: 900px;
|
||||
border-radius: 50%;
|
||||
filter: blur(120px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.bg-glow.a {
|
||||
background: #1f6feb55;
|
||||
left: -180px;
|
||||
top: -200px;
|
||||
}
|
||||
.bg-glow.b {
|
||||
background: #2ea04355;
|
||||
right: -200px;
|
||||
bottom: -260px;
|
||||
}
|
||||
.editor {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 1380px;
|
||||
height: 800px;
|
||||
background: #0b0f17;
|
||||
border: 1px solid #1d2733;
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 40px 120px rgba(0, 0, 0, 0.6),
|
||||
0 2px 0 rgba(255, 255, 255, 0.03) inset;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.titlebar {
|
||||
height: 56px;
|
||||
flex: 0 0 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 0 22px;
|
||||
background: linear-gradient(#11161f, #0c111a);
|
||||
border-bottom: 1px solid #1b2430;
|
||||
}
|
||||
.dots {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.dot.r {
|
||||
background: #ff5f57;
|
||||
}
|
||||
.dot.y {
|
||||
background: #febc2e;
|
||||
}
|
||||
.dot.g {
|
||||
background: #28c840;
|
||||
}
|
||||
.filename {
|
||||
font-size: 19px;
|
||||
color: #8b98a9;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
.filename .accent {
|
||||
color: #d6e2f0;
|
||||
}
|
||||
.code-surface {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
will-change: opacity;
|
||||
}
|
||||
.code {
|
||||
position: absolute;
|
||||
top: 38px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0 44px 0 104px;
|
||||
font-size: 30px;
|
||||
line-height: 46px;
|
||||
font-variant-ligatures: none;
|
||||
tab-size: 2;
|
||||
}
|
||||
.gutter {
|
||||
position: absolute;
|
||||
top: 38px;
|
||||
left: 0;
|
||||
width: 84px;
|
||||
padding-right: 18px;
|
||||
font-size: 30px;
|
||||
line-height: 46px;
|
||||
text-align: right;
|
||||
color: #828c9b;
|
||||
user-select: none;
|
||||
}
|
||||
.ln {
|
||||
display: block;
|
||||
height: 46px;
|
||||
}
|
||||
.line {
|
||||
display: block;
|
||||
min-height: 46px;
|
||||
white-space: pre;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.tok {
|
||||
display: inline-block;
|
||||
white-space: pre;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.caret {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 32px;
|
||||
margin-top: 6px;
|
||||
background: #58a6ff;
|
||||
border-radius: 1px;
|
||||
z-index: 3;
|
||||
}
|
||||
.hl-box {
|
||||
position: absolute;
|
||||
background: rgba(88, 166, 255, 0.16);
|
||||
border-left: 3px solid #58a6ff;
|
||||
border-radius: 6px;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.diff-line {
|
||||
display: block;
|
||||
min-height: 0;
|
||||
line-height: 46px;
|
||||
white-space: pre;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding-left: 14px;
|
||||
}
|
||||
.diff-sign {
|
||||
display: inline-block;
|
||||
width: 22px;
|
||||
color: #41506220;
|
||||
}
|
||||
.diff-del {
|
||||
background: rgba(248, 81, 73, 0.12);
|
||||
box-shadow: inset 3px 0 #f85149;
|
||||
}
|
||||
.diff-add {
|
||||
background: rgba(63, 185, 80, 0.12);
|
||||
box-shadow: inset 3px 0 #3fb950;
|
||||
}
|
||||
.diff-del .diff-sign {
|
||||
color: #f85149;
|
||||
}
|
||||
.diff-add .diff-sign {
|
||||
color: #3fb950;
|
||||
}
|
||||
.flight-block {
|
||||
position: absolute;
|
||||
white-space: pre;
|
||||
font-size: 27px;
|
||||
line-height: 40px;
|
||||
padding: 11px 22px;
|
||||
border-radius: 11px;
|
||||
background: #0f1722;
|
||||
border: 1px solid #233044;
|
||||
box-shadow: 0 16px 44px rgba(0, 0, 0, 0.5);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.flight-target {
|
||||
position: absolute;
|
||||
border: 1.5px dashed #2a3a4f;
|
||||
border-radius: 14px;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="code-highlight"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="bg-field" data-layout-ignore></div>
|
||||
<div class="bg-grid" data-layout-ignore></div>
|
||||
<div class="bg-glow a" data-layout-ignore></div>
|
||||
<div class="bg-glow b" data-layout-ignore></div>
|
||||
<div class="editor" id="editor" data-layout-ignore>
|
||||
<div class="titlebar">
|
||||
<div class="dots">
|
||||
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
|
||||
</div>
|
||||
<span class="filename"
|
||||
><span class="accent">loadConfig.js</span> — Code Highlight Sweep</span
|
||||
>
|
||||
</div>
|
||||
<div class="code-surface" id="surface"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__TOKENS = {
|
||||
feature: {
|
||||
lang: "js",
|
||||
theme: "github-dark",
|
||||
bg: "#24292e",
|
||||
fg: "#e1e4e8",
|
||||
states: [
|
||||
{
|
||||
code: 'async function loadConfig(path) {\n const raw = await readFile(path, "utf8")\n const config = JSON.parse(raw)\n return validate(config)\n}',
|
||||
tokens: [
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-0",
|
||||
content: "async",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-1",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-2",
|
||||
content: "function",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-3",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-4",
|
||||
content: "loadConfig",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-5",
|
||||
content: "(",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-6",
|
||||
content: "path",
|
||||
color: "#FFAB70",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-7",
|
||||
content: ") {",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-8",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-9",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-10",
|
||||
content: "const",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-11",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-12",
|
||||
content: "raw",
|
||||
color: "#79B8FF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-13",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-14",
|
||||
content: "=",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-15",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-16",
|
||||
content: "await",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-17",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-18",
|
||||
content: "readFile",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-19",
|
||||
content: "(path,",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-20",
|
||||
content: " ",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-21",
|
||||
content: '"utf8"',
|
||||
color: "#9ECBFF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-22",
|
||||
content: ")",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-23",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-24",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-25",
|
||||
content: "const",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-26",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-27",
|
||||
content: "config",
|
||||
color: "#79B8FF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-28",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-29",
|
||||
content: "=",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-30",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-31",
|
||||
content: "JSON",
|
||||
color: "#79B8FF",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-32",
|
||||
content: ".",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-33",
|
||||
content: "parse",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-34",
|
||||
content: "(raw)",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-35",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-36",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-37",
|
||||
content: "return",
|
||||
color: "#F97583",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-38",
|
||||
content: " ",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-39",
|
||||
content: "validate",
|
||||
color: "#B392F0",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-40",
|
||||
content: "(config)",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-41",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-42",
|
||||
content: "}",
|
||||
color: "#E1E4E8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
{
|
||||
key: "SPx9u4TyzfMqSHpbjPHnWFQU8lPVEPieF6W5Vtu-bNA-43",
|
||||
content: "\n",
|
||||
color: "#e1e4e8",
|
||||
fontStyle: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
window.__BLOCK = {
|
||||
id: "code-highlight",
|
||||
effect: "highlight",
|
||||
seq: "feature",
|
||||
line: 1,
|
||||
duration: 5,
|
||||
};
|
||||
</script>
|
||||
<script>
|
||||
// hf-code-anim.js — self-contained code-animation engine for HyperFrames registry blocks.
|
||||
//
|
||||
// Reuses Shiki Magic Move's baked output (window.__TOKENS, baked at author time) and
|
||||
// drives the animation as a PAUSED GSAP timeline the HyperFrames engine seeks per frame —
|
||||
// NOT CSS transitions (those break under non-sequential frame seeking). FLIP measurement
|
||||
// runs once inside document.fonts.ready (glyph metrics must be final). Each block sets
|
||||
// window.__BLOCK = { id, effect, seq, line?, duration } and includes this engine; the
|
||||
// engine builds one scene that fills the composition and registers window.__timelines[id].
|
||||
(function () {
|
||||
"use strict";
|
||||
var T = window.__TOKENS || {};
|
||||
|
||||
function lineDiv() {
|
||||
var d = document.createElement("div");
|
||||
d.className = "line";
|
||||
return d;
|
||||
}
|
||||
|
||||
// Render a code state's keyed tokens into codeEl as lines of inline-block .tok spans.
|
||||
function renderState(codeEl, state) {
|
||||
codeEl.textContent = "";
|
||||
var keyToEl = new Map();
|
||||
var line = lineDiv();
|
||||
codeEl.appendChild(line);
|
||||
for (var i = 0; i < state.tokens.length; i++) {
|
||||
var t = state.tokens[i];
|
||||
if (t.content === "\n") {
|
||||
line = lineDiv();
|
||||
codeEl.appendChild(line);
|
||||
continue;
|
||||
}
|
||||
var span = document.createElement("span");
|
||||
span.className = "tok";
|
||||
span.textContent = t.content;
|
||||
span.style.color = t.color;
|
||||
if (t.fontStyle & 1) span.style.fontStyle = "italic";
|
||||
if (t.fontStyle & 2) span.style.fontWeight = "700";
|
||||
line.appendChild(span);
|
||||
keyToEl.set(t.key, span);
|
||||
}
|
||||
var last = codeEl.lastChild;
|
||||
if (last && last.className === "line" && last.children.length === 0)
|
||||
codeEl.removeChild(last);
|
||||
return keyToEl;
|
||||
}
|
||||
|
||||
function rectsOf(keyToEl) {
|
||||
var m = new Map();
|
||||
keyToEl.forEach(function (el, k) {
|
||||
var r = el.getBoundingClientRect();
|
||||
m.set(k, { left: r.left, top: r.top });
|
||||
});
|
||||
return m;
|
||||
}
|
||||
|
||||
function makeScene(surface, withGutter) {
|
||||
var scene = document.createElement("div");
|
||||
scene.className = "scene";
|
||||
if (withGutter) {
|
||||
var g = document.createElement("div");
|
||||
g.className = "gutter";
|
||||
scene.appendChild(g);
|
||||
}
|
||||
var code = document.createElement("div");
|
||||
code.className = "code";
|
||||
scene.appendChild(code);
|
||||
surface.appendChild(scene);
|
||||
return { scene: scene, code: code, gutter: scene.querySelector(".gutter") };
|
||||
}
|
||||
|
||||
function fillGutter(g, n) {
|
||||
var html = "";
|
||||
for (var i = 1; i <= n; i++) html += '<span class="ln">' + i + "</span>";
|
||||
g.innerHTML = html;
|
||||
}
|
||||
|
||||
function splitLines(state) {
|
||||
var lines = [{ text: "", tokens: [] }];
|
||||
for (var i = 0; i < state.tokens.length; i++) {
|
||||
var t = state.tokens[i];
|
||||
if (t.content === "\n") {
|
||||
lines.push({ text: "", tokens: [] });
|
||||
continue;
|
||||
}
|
||||
var cur = lines[lines.length - 1];
|
||||
cur.tokens.push(t);
|
||||
cur.text += t.content;
|
||||
}
|
||||
if (lines.length && lines[lines.length - 1].tokens.length === 0) lines.pop();
|
||||
return lines;
|
||||
}
|
||||
|
||||
function lineDiff(a, b) {
|
||||
var n = a.length;
|
||||
var m = b.length;
|
||||
var dp = [];
|
||||
for (var i = 0; i <= n; i++) dp.push(new Array(m + 1).fill(0));
|
||||
for (var i = n - 1; i >= 0; i--) {
|
||||
for (var j = m - 1; j >= 0; j--) {
|
||||
dp[i][j] =
|
||||
a[i].text === b[j].text
|
||||
? dp[i + 1][j + 1] + 1
|
||||
: Math.max(dp[i + 1][j], dp[i][j + 1]);
|
||||
}
|
||||
}
|
||||
var ops = [];
|
||||
var i = 0;
|
||||
var j = 0;
|
||||
while (i < n && j < m) {
|
||||
if (a[i].text === b[j].text) {
|
||||
ops.push({ type: "same", line: b[j] });
|
||||
i++;
|
||||
j++;
|
||||
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
||||
ops.push({ type: "del", line: a[i] });
|
||||
i++;
|
||||
} else {
|
||||
ops.push({ type: "add", line: b[j] });
|
||||
j++;
|
||||
}
|
||||
}
|
||||
while (i < n) ops.push({ type: "del", line: a[i++] });
|
||||
while (j < m) ops.push({ type: "add", line: b[j++] });
|
||||
return ops;
|
||||
}
|
||||
|
||||
// ---- single-scene effect builders (fade in, play, hold to end) -------------
|
||||
var FADE = 0.45;
|
||||
|
||||
function highlight(tl, surface, state, start, targetIdx) {
|
||||
var SWEEP = 0.9;
|
||||
var parts = makeScene(surface, true);
|
||||
var code = parts.code;
|
||||
renderState(code, state);
|
||||
var lines = code.querySelectorAll(".line");
|
||||
fillGutter(parts.gutter, lines.length);
|
||||
var base = code.getBoundingClientRect();
|
||||
var idx = Math.max(0, Math.min(lines.length - 1, targetIdx));
|
||||
var lr = lines[idx].getBoundingClientRect();
|
||||
var box = document.createElement("div");
|
||||
box.className = "hl-box";
|
||||
box.style.left = lr.left - base.left - 8 + "px";
|
||||
box.style.top = lr.top - base.top - 1 + "px";
|
||||
box.style.height = lr.height + 2 + "px";
|
||||
code.insertBefore(box, code.firstChild);
|
||||
tl.set(parts.scene, { opacity: 0 }, 0);
|
||||
tl.to(parts.scene, { opacity: 1, duration: FADE, ease: "power1.out" }, start);
|
||||
var at = start + FADE + 0.2;
|
||||
tl.fromTo(
|
||||
box,
|
||||
{ width: 0, opacity: 0 },
|
||||
{ width: lr.width + 18, opacity: 1, duration: SWEEP, ease: "power2.inOut" },
|
||||
at,
|
||||
);
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (i === idx) continue;
|
||||
tl.fromTo(
|
||||
lines[i],
|
||||
{ opacity: 1 },
|
||||
{ opacity: 0.45, duration: SWEEP, ease: "power2.inOut" },
|
||||
at,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function buildEffect(tl, surface, spec) {
|
||||
var start = 0.45;
|
||||
var seq = T[spec.seq];
|
||||
if (spec.effect === "highlight")
|
||||
highlight(tl, surface, seq.states[0], start, spec.line || 0);
|
||||
}
|
||||
|
||||
function go() {
|
||||
var spec = window.__BLOCK;
|
||||
var surface = document.getElementById("surface");
|
||||
var root = document.getElementById("root");
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
// Build inside fonts.ready (glyph metrics must be final), then register.
|
||||
// Register ONLY after the timeline is fully built: the runtime's
|
||||
// sub-composition readiness gate treats "key present" as "ready" and
|
||||
// nests the child once. An empty timeline registered before this build
|
||||
// would be nested empty and never re-nested → blank render when used as
|
||||
// a sub-composition. See the contract in engine frameCapture.ts.
|
||||
document.fonts.ready.then(function () {
|
||||
tl.from("#editor", { opacity: 0, scale: 0.985, duration: 0.5, ease: "power2.out" }, 0);
|
||||
buildEffect(tl, surface, spec);
|
||||
var dur = parseFloat(root.dataset.duration) || tl.duration();
|
||||
tl.to({}, { duration: dur }, 0); // pad to full composition length
|
||||
window.__timelines["code-highlight"] = tl; // register AFTER setup completes
|
||||
if (typeof window.__hfForceTimelineRebind === "function") {
|
||||
window.__hfForceTimelineRebind(); // re-nest now that we are populated
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", go);
|
||||
else go();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-highlight
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 5s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-highlight.html` | `compositions/code-highlight.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-highlight" data-composition-src="compositions/code-highlight.html" data-start="0" data-duration="5" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `code` `code-animation` `highlight` `developer`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
+1239
-30
File diff suppressed because it is too large
Load Diff
@@ -3,49 +3,521 @@ 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."
|
||||
---
|
||||
|
||||
`code` `code-animation` `particles` `gpu` `webgl` `developer`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-particle-assemble preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-particle-assemble.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-particle-assemble.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-particle-assemble" item="code-particle-assemble" />
|
||||
|
||||
```text
|
||||
Add the Code Particle Assemble block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/code-particle-assemble.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`code-particle-assemble.html`}>
|
||||
|
||||
```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-start="0"
|
||||
data-duration="8"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-particle-assemble
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-particle-assemble.html` | `compositions/code-particle-assemble.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `code` `code-animation` `particles` `gpu` `webgl` `developer`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
+1583
-30
File diff suppressed because it is too large
Load Diff
@@ -3,49 +3,577 @@ 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."
|
||||
---
|
||||
|
||||
`code` `code-animation` `shader` `webgl` `developer`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-shader-dissolve preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-shader-dissolve.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-shader-dissolve.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-shader-dissolve" item="code-shader-dissolve" />
|
||||
|
||||
```text
|
||||
Add the Code Shader Dissolve block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/code-shader-dissolve.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`code-shader-dissolve.html`}>
|
||||
|
||||
```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-start="0"
|
||||
data-duration="7"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-shader-dissolve
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 7s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-shader-dissolve.html` | `compositions/code-shader-dissolve.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `code` `code-animation` `shader` `webgl` `developer`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,308 @@ title: "Code Snippet - Apple Terminal Basic"
|
||||
description: "Apple Terminal Basic profile: white background, black text, per-character typing animation of a shell session."
|
||||
---
|
||||
|
||||
`code` `developer` `showcase` `terminal` `apple` `apple-terminal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-snippet-apple-terminal-basic preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-basic.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-basic.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-basic" item="code-snippet-apple-terminal-basic" />
|
||||
|
||||
```text
|
||||
Add the Code Snippet - Apple Terminal Basic block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
```
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-basic.html`.
|
||||
|
||||
### Install from the terminal
|
||||
## Source
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-snippet-apple-terminal-basic
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 12s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-snippet-apple-terminal-basic.html` | `compositions/code-snippet-apple-terminal-basic.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`code-snippet-apple-terminal-basic.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-snippet-apple-terminal-basic" data-composition-src="compositions/code-snippet-apple-terminal-basic.html" data-start="0" data-duration="12" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
## 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 */}
|
||||
|
||||
Tagged `code` `developer` `showcase` `terminal` `apple` `apple-terminal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,303 @@ 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."
|
||||
---
|
||||
|
||||
`code` `developer` `showcase` `terminal` `apple` `apple-terminal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-snippet-apple-terminal-clear-dark preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-clear-dark.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-clear-dark.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-clear-dark" item="code-snippet-apple-terminal-clear-dark" />
|
||||
|
||||
```text
|
||||
Add the Code Snippet - Apple Terminal Clear Dark block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
```
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-clear-dark.html`.
|
||||
|
||||
### Install from the terminal
|
||||
## Source
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-snippet-apple-terminal-clear-dark
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 12s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-snippet-apple-terminal-clear-dark.html` | `compositions/code-snippet-apple-terminal-clear-dark.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`code-snippet-apple-terminal-clear-dark.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-snippet-apple-terminal-clear-dark" data-composition-src="compositions/code-snippet-apple-terminal-clear-dark.html" data-start="0" data-duration="12" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
## 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 */}
|
||||
|
||||
Tagged `code` `developer` `showcase` `terminal` `apple` `apple-terminal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,303 @@ 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."
|
||||
---
|
||||
|
||||
`code` `developer` `showcase` `terminal` `apple` `apple-terminal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-snippet-apple-terminal-clear-light preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-clear-light.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-clear-light.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-clear-light" item="code-snippet-apple-terminal-clear-light" />
|
||||
|
||||
```text
|
||||
Add the Code Snippet - Apple Terminal Clear Light block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
```
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-clear-light.html`.
|
||||
|
||||
### Install from the terminal
|
||||
## Source
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-snippet-apple-terminal-clear-light
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 12s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-snippet-apple-terminal-clear-light.html` | `compositions/code-snippet-apple-terminal-clear-light.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`code-snippet-apple-terminal-clear-light.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-snippet-apple-terminal-clear-light" data-composition-src="compositions/code-snippet-apple-terminal-clear-light.html" data-start="0" data-duration="12" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
## 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 */}
|
||||
|
||||
Tagged `code` `developer` `showcase` `terminal` `apple` `apple-terminal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,302 @@ title: "Code Snippet - Apple Terminal Grass"
|
||||
description: "Apple Terminal Grass profile: black background with green text, per-character typing animation of a shell session."
|
||||
---
|
||||
|
||||
`code` `developer` `showcase` `terminal` `apple` `apple-terminal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-snippet-apple-terminal-grass preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-grass.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-grass.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-grass" item="code-snippet-apple-terminal-grass" />
|
||||
|
||||
```text
|
||||
Add the Code Snippet - Apple Terminal Grass block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
```
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-grass.html`.
|
||||
|
||||
### Install from the terminal
|
||||
## Source
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-snippet-apple-terminal-grass
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 12s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-snippet-apple-terminal-grass.html` | `compositions/code-snippet-apple-terminal-grass.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`code-snippet-apple-terminal-grass.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-snippet-apple-terminal-grass" data-composition-src="compositions/code-snippet-apple-terminal-grass.html" data-start="0" data-duration="12" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
## 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 */}
|
||||
|
||||
Tagged `code` `developer` `showcase` `terminal` `apple` `apple-terminal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,307 @@ 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."
|
||||
---
|
||||
|
||||
`code` `developer` `showcase` `terminal` `apple` `apple-terminal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-snippet-apple-terminal-homebrew preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-homebrew.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-homebrew.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-homebrew" item="code-snippet-apple-terminal-homebrew" />
|
||||
|
||||
```text
|
||||
Add the Code Snippet - Apple Terminal Homebrew block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
```
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-homebrew.html`.
|
||||
|
||||
### Install from the terminal
|
||||
## Source
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-snippet-apple-terminal-homebrew
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 12s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-snippet-apple-terminal-homebrew.html` | `compositions/code-snippet-apple-terminal-homebrew.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`code-snippet-apple-terminal-homebrew.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-snippet-apple-terminal-homebrew" data-composition-src="compositions/code-snippet-apple-terminal-homebrew.html" data-start="0" data-duration="12" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
## 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 */}
|
||||
|
||||
Tagged `code` `developer` `showcase` `terminal` `apple` `apple-terminal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,307 @@ 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."
|
||||
---
|
||||
|
||||
`code` `developer` `showcase` `terminal` `apple` `apple-terminal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-snippet-apple-terminal-man-page preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-man-page.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-man-page.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-man-page" item="code-snippet-apple-terminal-man-page" />
|
||||
|
||||
```text
|
||||
Add the Code Snippet - Apple Terminal Man Page block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
```
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-man-page.html`.
|
||||
|
||||
### Install from the terminal
|
||||
## Source
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-snippet-apple-terminal-man-page
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 12s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-snippet-apple-terminal-man-page.html` | `compositions/code-snippet-apple-terminal-man-page.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`code-snippet-apple-terminal-man-page.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-snippet-apple-terminal-man-page" data-composition-src="compositions/code-snippet-apple-terminal-man-page.html" data-start="0" data-duration="12" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
## 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 */}
|
||||
|
||||
Tagged `code` `developer` `showcase` `terminal` `apple` `apple-terminal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,303 @@ 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."
|
||||
---
|
||||
|
||||
`code` `developer` `showcase` `terminal` `apple` `apple-terminal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-snippet-apple-terminal-novel preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-novel.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-novel.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-novel" item="code-snippet-apple-terminal-novel" />
|
||||
|
||||
```text
|
||||
Add the Code Snippet - Apple Terminal Novel block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
```
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-novel.html`.
|
||||
|
||||
### Install from the terminal
|
||||
## Source
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-snippet-apple-terminal-novel
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 12s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-snippet-apple-terminal-novel.html` | `compositions/code-snippet-apple-terminal-novel.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`code-snippet-apple-terminal-novel.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-snippet-apple-terminal-novel" data-composition-src="compositions/code-snippet-apple-terminal-novel.html" data-start="0" data-duration="12" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
## 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 */}
|
||||
|
||||
Tagged `code` `developer` `showcase` `terminal` `apple` `apple-terminal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,312 @@ 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."
|
||||
---
|
||||
|
||||
`code` `developer` `showcase` `terminal` `apple` `apple-terminal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-snippet-apple-terminal-ocean preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-ocean.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-ocean.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-ocean" item="code-snippet-apple-terminal-ocean" />
|
||||
|
||||
```text
|
||||
Add the Code Snippet - Apple Terminal Ocean block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
```
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-ocean.html`.
|
||||
|
||||
### Install from the terminal
|
||||
## Source
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-snippet-apple-terminal-ocean
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 12s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-snippet-apple-terminal-ocean.html` | `compositions/code-snippet-apple-terminal-ocean.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`code-snippet-apple-terminal-ocean.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-snippet-apple-terminal-ocean" data-composition-src="compositions/code-snippet-apple-terminal-ocean.html" data-start="0" data-duration="12" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
## 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 */}
|
||||
|
||||
Tagged `code` `developer` `showcase` `terminal` `apple` `apple-terminal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,306 @@ 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."
|
||||
---
|
||||
|
||||
`code` `developer` `showcase` `terminal` `apple` `apple-terminal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-snippet-apple-terminal-pro preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-pro.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-pro.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-pro" item="code-snippet-apple-terminal-pro" />
|
||||
|
||||
```text
|
||||
Add the Code Snippet - Apple Terminal Pro block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
```
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-pro.html`.
|
||||
|
||||
### Install from the terminal
|
||||
## Source
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-snippet-apple-terminal-pro
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 12s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-snippet-apple-terminal-pro.html` | `compositions/code-snippet-apple-terminal-pro.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`code-snippet-apple-terminal-pro.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-snippet-apple-terminal-pro" data-composition-src="compositions/code-snippet-apple-terminal-pro.html" data-start="0" data-duration="12" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
## 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 */}
|
||||
|
||||
Tagged `code` `developer` `showcase` `terminal` `apple` `apple-terminal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,303 @@ 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."
|
||||
---
|
||||
|
||||
`code` `developer` `showcase` `terminal` `apple` `apple-terminal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-snippet-apple-terminal-red-sands preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-red-sands.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-red-sands.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-red-sands" item="code-snippet-apple-terminal-red-sands" />
|
||||
|
||||
```text
|
||||
Add the Code Snippet - Apple Terminal Red Sands block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
```
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-red-sands.html`.
|
||||
|
||||
### Install from the terminal
|
||||
## Source
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-snippet-apple-terminal-red-sands
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 12s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-snippet-apple-terminal-red-sands.html` | `compositions/code-snippet-apple-terminal-red-sands.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`code-snippet-apple-terminal-red-sands.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-snippet-apple-terminal-red-sands" data-composition-src="compositions/code-snippet-apple-terminal-red-sands.html" data-start="0" data-duration="12" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
## 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 */}
|
||||
|
||||
Tagged `code` `developer` `showcase` `terminal` `apple` `apple-terminal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,309 @@ 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."
|
||||
---
|
||||
|
||||
`code` `developer` `showcase` `terminal` `apple` `apple-terminal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-snippet-apple-terminal-silver-aerogel preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-silver-aerogel.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-silver-aerogel.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-silver-aerogel" item="code-snippet-apple-terminal-silver-aerogel" />
|
||||
|
||||
```text
|
||||
Add the Code Snippet - Apple Terminal Silver Aerogel block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
```
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-silver-aerogel.html`.
|
||||
|
||||
### Install from the terminal
|
||||
## Source
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-snippet-apple-terminal-silver-aerogel
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 12s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-snippet-apple-terminal-silver-aerogel.html` | `compositions/code-snippet-apple-terminal-silver-aerogel.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`code-snippet-apple-terminal-silver-aerogel.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-snippet-apple-terminal-silver-aerogel" data-composition-src="compositions/code-snippet-apple-terminal-silver-aerogel.html" data-start="0" data-duration="12" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
## 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 */}
|
||||
|
||||
Tagged `code` `developer` `showcase` `terminal` `apple` `apple-terminal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,306 @@ 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."
|
||||
---
|
||||
|
||||
`code` `developer` `showcase` `terminal` `apple` `apple-terminal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-snippet-apple-terminal-solid-colors preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-apple-terminal-solid-colors.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-apple-terminal-solid-colors.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-snippet-apple-terminal-solid-colors" item="code-snippet-apple-terminal-solid-colors" />
|
||||
|
||||
```text
|
||||
Add the Code Snippet - Apple Terminal Solid Colors block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
```
|
||||
That writes one file: `compositions/code-snippet-apple-terminal-solid-colors.html`.
|
||||
|
||||
### Install from the terminal
|
||||
## Source
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-snippet-apple-terminal-solid-colors
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 12s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-snippet-apple-terminal-solid-colors.html` | `compositions/code-snippet-apple-terminal-solid-colors.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`code-snippet-apple-terminal-solid-colors.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="code-snippet-apple-terminal-solid-colors" data-composition-src="compositions/code-snippet-apple-terminal-solid-colors.html" data-start="0" data-duration="12" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
## 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 */}
|
||||
|
||||
Tagged `code` `developer` `showcase` `terminal` `apple` `apple-terminal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
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,49 +3,707 @@ title: "Code Snippet Flight"
|
||||
description: "Discrete code snippets fly in from the side and assemble into a stacked program, staggered. Block-level FLIP."
|
||||
---
|
||||
|
||||
`code` `code-animation` `flight` `developer`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-snippet-flight preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-snippet-flight.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-snippet-flight.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-snippet-flight" item="code-snippet-flight" />
|
||||
|
||||
```text
|
||||
Add the Code Snippet Flight block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/code-snippet-flight.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`code-snippet-flight.html`}>
|
||||
|
||||
```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-start="0"
|
||||
data-duration="6"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="bg-field" data-layout-ignore></div>
|
||||
<div class="bg-grid" data-layout-ignore></div>
|
||||
<div class="bg-glow a" data-layout-ignore></div>
|
||||
<div class="bg-glow b" data-layout-ignore></div>
|
||||
<div class="editor" id="editor" data-layout-ignore>
|
||||
<div class="titlebar">
|
||||
<div class="dots">
|
||||
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
|
||||
</div>
|
||||
<span class="filename"
|
||||
><span class="accent">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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-snippet-flight
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 6s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-snippet-flight.html` | `compositions/code-snippet-flight.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `code` `code-animation` `flight` `developer`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
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,49 +3,805 @@ title: "Code Typing"
|
||||
description: "Token-streamed typing reveal with a caret that tracks the frontier — deterministic, no CSS animation."
|
||||
---
|
||||
|
||||
`code` `code-animation` `typing` `developer`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="code-typing preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/code-typing.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/code-typing.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add code-typing" item="code-typing" />
|
||||
|
||||
```text
|
||||
Add the Code Typing block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/code-typing.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`code-typing.html`}>
|
||||
|
||||
```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-start="0"
|
||||
data-duration="5"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="bg-field" data-layout-ignore></div>
|
||||
<div class="bg-grid" data-layout-ignore></div>
|
||||
<div class="bg-glow a" data-layout-ignore></div>
|
||||
<div class="bg-glow b" data-layout-ignore></div>
|
||||
<div class="editor" id="editor" data-layout-ignore>
|
||||
<div class="titlebar">
|
||||
<div class="dots">
|
||||
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
|
||||
</div>
|
||||
<span class="filename"><span class="accent">loadConfig.js</span> — Code 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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add code-typing
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 5s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `code-typing.html` | `compositions/code-typing.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `code` `code-animation` `typing` `developer`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -0,0 +1,967 @@
|
||||
---
|
||||
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/blocks/cosmic-orb.json"
|
||||
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" item="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`.
|
||||
|
||||
## Variables
|
||||
|
||||
Every one of these has a default, so the piece works untouched. Set the ones you
|
||||
want to change on the element:
|
||||
|
||||
| Variable | Default | Accepts | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| `hue` | `268` | 0deg to 360deg, step 1deg | |
|
||||
| `accent` | `196` | 0deg to 360deg, step 1deg | |
|
||||
| `spin` | `0.035` | 0turns/s to 0.5turns/s, step 0.005turns/s | |
|
||||
| `stars` | `1` | 0 to 2, step 0.05 | |
|
||||
| `glow` | `1` | 0 to 2, step 0.05 | |
|
||||
| `size` | `0.86` | 0.2 to 1.2, step 0.01 | |
|
||||
| `pulse` | `0` | 0 to 1, step 0.01 | |
|
||||
| `pulseEnvelope` | `` | string | |
|
||||
| `backdrop` | `#04040a` | color | |
|
||||
|
||||
Set them with `data-variable-values` on the element that mounts it. These are the
|
||||
defaults, so this behaves exactly like the preview above until you change one:
|
||||
|
||||
```html wrap
|
||||
<div
|
||||
data-composition-id="cosmic-orb"
|
||||
data-composition-src="compositions/cosmic-orb.html"
|
||||
data-variable-values='{"hue":268,"accent":196,"spin":0.035,"stars":1,"glow":1,"size":0.86,"pulse":0,"pulseEnvelope":"","backdrop":"#04040a"}'
|
||||
></div>
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`cosmic-orb.html`}>
|
||||
|
||||
```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>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* 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,49 +3,419 @@ title: "Cross Warp Morph"
|
||||
description: "Shader transition with cross-warped morphing"
|
||||
---
|
||||
|
||||
`transition` `shader`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="cross-warp-morph preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/cross-warp-morph.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/cross-warp-morph.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add cross-warp-morph" item="cross-warp-morph" />
|
||||
|
||||
```text
|
||||
Add the Cross Warp Morph block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/cross-warp-morph.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`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-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add cross-warp-morph
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `cross-warp-morph.html` | `compositions/cross-warp-morph.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `transition` `shader`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,506 @@ title: "Data Chart"
|
||||
description: "Animated bar + line chart with staggered reveal, NYT-style typography, and value labels"
|
||||
---
|
||||
|
||||
`data` `chart` `statistics`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="data-chart preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/data-chart.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/data-chart.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add data-chart" item="data-chart" />
|
||||
|
||||
```text
|
||||
Add the Data Chart block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/data-chart.html`.
|
||||
|
||||
## Change the colors
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add data-chart
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 15s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `data-chart.html` | `compositions/data-chart.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,422 @@ title: "Domain Warp Dissolve"
|
||||
description: "Shader transition with fractal noise domain warping"
|
||||
---
|
||||
|
||||
`transition` `shader`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="domain-warp-dissolve preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/domain-warp-dissolve.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/domain-warp-dissolve.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add domain-warp-dissolve" item="domain-warp-dissolve" />
|
||||
|
||||
```text
|
||||
Add the Domain Warp Dissolve block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/domain-warp-dissolve.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`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-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add domain-warp-dissolve
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `domain-warp-dissolve.html` | `compositions/domain-warp-dissolve.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `transition` `shader`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,182 @@ title: "Editorial Flash Overlay"
|
||||
description: "Finite neutral-warm light layers for a seek-safe camera-flash cut or social reveal"
|
||||
---
|
||||
|
||||
`flash` `editorial` `social` `light` `transition` `overlay` `media-treatment-overlay`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="editorial-flash-overlay preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/editorial-flash-overlay.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/editorial-flash-overlay.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add editorial-flash-overlay" item="editorial-flash-overlay" />
|
||||
|
||||
```text
|
||||
Add the Editorial Flash Overlay block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/editorial-flash-overlay.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="editorial-flash-overlay"
|
||||
data-composition-src="compositions/editorial-flash-overlay.html"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
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.
|
||||
|
||||
```bash
|
||||
npx hyperframes add editorial-flash-overlay
|
||||
```
|
||||
## Source
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `editorial-flash-overlay.html` | `compositions/editorial-flash-overlay.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`editorial-flash-overlay.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="editorial-flash-overlay" data-composition-src="compositions/editorial-flash-overlay.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,408 @@ title: "Flash Through White"
|
||||
description: "Shader transition with white flash crossfade"
|
||||
---
|
||||
|
||||
`transition` `shader`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="flash-through-white preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/flash-through-white.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flash-through-white.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add flash-through-white" item="flash-through-white" />
|
||||
|
||||
```text
|
||||
Add the Flash Through White block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/flash-through-white.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`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-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add flash-through-white
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `flash-through-white.html` | `compositions/flash-through-white.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `transition` `shader`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,509 @@ title: "Flowchart Vertical"
|
||||
description: "Portrait animated decision tree with SVG connectors, sticky-note nodes, cursor interaction, and typing correction"
|
||||
---
|
||||
|
||||
`diagram` `flowchart` `interactive` `portrait`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="flowchart-vertical preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/flowchart-vertical.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flowchart-vertical.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add flowchart-vertical" item="flowchart-vertical" />
|
||||
|
||||
```text
|
||||
Add the Flowchart Vertical block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/flowchart-vertical.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`flowchart-vertical.html`}>
|
||||
|
||||
```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-width="1440"
|
||||
data-height="2560"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add flowchart-vertical
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1440×2560 |
|
||||
| Duration | 12s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `flowchart-vertical.html` | `compositions/flowchart-vertical.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `diagram` `flowchart` `interactive` `portrait`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,509 @@ title: "Flowchart"
|
||||
description: "Animated decision tree with SVG connectors, sticky-note nodes, cursor interaction, and typing correction"
|
||||
---
|
||||
|
||||
`diagram` `flowchart` `interactive`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="flowchart preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/flowchart.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/flowchart.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add flowchart" item="flowchart" />
|
||||
|
||||
```text
|
||||
Add the Flowchart block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/flowchart.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`flowchart.html`}>
|
||||
|
||||
```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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add flowchart
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 12s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `flowchart.html` | `compositions/flowchart.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `diagram` `flowchart` `interactive`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,173 @@ title: "Freeze-Frame Dressing"
|
||||
description: "Timeline-driven paper, tape, and flash dressing for a freeze-frame or background-removed subject"
|
||||
---
|
||||
|
||||
`freeze-frame` `cutout` `scrapbook` `paper` `social` `overlay` `media-treatment-overlay`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="freeze-frame-dressing preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/freeze-frame-dressing.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/freeze-frame-dressing.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add freeze-frame-dressing" item="freeze-frame-dressing" />
|
||||
|
||||
```text
|
||||
Add the Freeze-Frame Dressing block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/freeze-frame-dressing.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="freeze-frame-dressing"
|
||||
data-composition-src="compositions/freeze-frame-dressing.html"
|
||||
data-start="0"
|
||||
data-duration="4"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
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.
|
||||
|
||||
```bash
|
||||
npx hyperframes add freeze-frame-dressing
|
||||
```
|
||||
## Source
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `freeze-frame-dressing.html` | `compositions/freeze-frame-dressing.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`freeze-frame-dressing.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="freeze-frame-dressing" data-composition-src="compositions/freeze-frame-dressing.html" data-start="0" data-duration="4" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+389
-30
@@ -3,49 +3,408 @@ title: "Glitch"
|
||||
description: "Shader transition with digital glitch artifacts"
|
||||
---
|
||||
|
||||
`transition` `shader`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="glitch preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/glitch.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/glitch.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add glitch" item="glitch" />
|
||||
|
||||
```text
|
||||
Add the Glitch block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/glitch.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`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-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add glitch
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `glitch.html` | `compositions/glitch.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `transition` `shader`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,407 @@ title: "Gravitational Lens"
|
||||
description: "Shader transition with gravitational lensing distortion"
|
||||
---
|
||||
|
||||
`transition` `shader`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="gravitational-lens preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/gravitational-lens.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/gravitational-lens.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add gravitational-lens" item="gravitational-lens" />
|
||||
|
||||
```text
|
||||
Add the Gravitational Lens block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/gravitational-lens.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`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-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add gravitational-lens
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `gravitational-lens.html` | `compositions/gravitational-lens.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `transition` `shader`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -0,0 +1,865 @@
|
||||
---
|
||||
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/blocks/halftone-field.json"
|
||||
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" item="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`.
|
||||
|
||||
## Variables
|
||||
|
||||
Every one of these has a default, so the piece works untouched. Set the ones you
|
||||
want to change on the element:
|
||||
|
||||
| Variable | Default | Accepts | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| `frequency` | `2` | 1 to 10, step 0.1 | |
|
||||
| `speed` | `5` | 0 to 10, step 0.1 | |
|
||||
| `cellSize` | `54` | 1 to 100, step 1 | |
|
||||
| `gamma` | `12` | 1 to 20, step 0.1 | |
|
||||
| `paletteBias` | `2` | 0 to 20, step 0.5 | |
|
||||
| `palette1` | `#00AAFF` | color | |
|
||||
| `palette2` | `#C2FF67` | color | |
|
||||
| `palette3` | `#6600FF` | color | |
|
||||
| `palette4` | `#000000` | color | |
|
||||
| `background` | `#000000` | color | |
|
||||
| `quantize` | `false` | boolean | |
|
||||
|
||||
Set them with `data-variable-values` on the element that mounts it. These are the
|
||||
defaults, so this behaves exactly like the preview above until you change one:
|
||||
|
||||
```html wrap
|
||||
<div
|
||||
data-composition-id="halftone-field"
|
||||
data-composition-src="compositions/halftone-field.html"
|
||||
data-variable-values='{"frequency":2,"speed":5,"cellSize":54,"gamma":12,"paletteBias":2,"palette1":"#00AAFF","palette2":"#C2FF67","palette3":"#6600FF","palette4":"#000000","background":"#000000","quantize":false}'
|
||||
></div>
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`halftone-field.html`}>
|
||||
|
||||
```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>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* 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,50 +3,355 @@ 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)"
|
||||
---
|
||||
|
||||
`handwritten` `frame` `media`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="hw-frame preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/hw-frame.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-frame.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add hw-frame" item="hw-frame" />
|
||||
|
||||
```text
|
||||
Add the Sketched Frame block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes `compositions/hw-frame.html`, plus 1 supporting file under `assets/fonts/`.
|
||||
|
||||
## Add it to your video
|
||||
|
||||
It runs for 8 seconds at 1920×1080. Paste this into your composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="hw-frame"
|
||||
data-composition-src="compositions/hw-frame.html"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
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.
|
||||
|
||||
```bash
|
||||
npx hyperframes add hw-frame
|
||||
```
|
||||
## Source
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `hw-frame.html` | `compositions/hw-frame.html` | hyperframes:composition |
|
||||
| `assets/fonts/Caveat-700-latin.woff2` | `assets/fonts/Caveat-700-latin.woff2` | hyperframes:asset |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`hw-frame.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="hw-frame" data-composition-src="compositions/hw-frame.html" data-start="0" data-duration="8" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,50 +3,246 @@ title: "Text Along a Path"
|
||||
description: "Handwriting flowing along any SVG path with a per-character reveal; the path is the only geometry input"
|
||||
---
|
||||
|
||||
`handwritten` `typography` `path` `overlay`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="hw-path-text preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/hw-path-text.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-path-text.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add hw-path-text" item="hw-path-text" />
|
||||
|
||||
```text
|
||||
Add the Text Along a Path block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes `compositions/hw-path-text.html`, plus 1 supporting file under `assets/fonts/`.
|
||||
|
||||
## 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="hw-path-text"
|
||||
data-composition-src="compositions/hw-path-text.html"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
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.
|
||||
|
||||
```bash
|
||||
npx hyperframes add hw-path-text
|
||||
```
|
||||
## Source
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 6s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `hw-path-text.html` | `compositions/hw-path-text.html` | hyperframes:composition |
|
||||
| `assets/fonts/Caveat-700-latin.woff2` | `assets/fonts/Caveat-700-latin.woff2` | hyperframes:asset |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`hw-path-text.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="hw-path-text" data-composition-src="compositions/hw-path-text.html" data-start="0" data-duration="6" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,50 +3,370 @@ title: "Hand-Drawn Flowchart"
|
||||
description: "Wobbled boxes with handwritten labels joined by curved connectors, drawing on in sequence from a node list"
|
||||
---
|
||||
|
||||
`handwritten` `diagram` `data-viz` `overlay`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="hw-pipeline preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/hw-pipeline.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-pipeline.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add hw-pipeline" item="hw-pipeline" />
|
||||
|
||||
```text
|
||||
Add the Hand-Drawn Flowchart block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes `compositions/hw-pipeline.html`, plus 1 supporting file under `assets/fonts/`.
|
||||
|
||||
## Add it to your video
|
||||
|
||||
It runs for 7 seconds at 1920×1080. Paste this into your composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="hw-pipeline"
|
||||
data-composition-src="compositions/hw-pipeline.html"
|
||||
data-start="0"
|
||||
data-duration="7"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
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.
|
||||
|
||||
```bash
|
||||
npx hyperframes add hw-pipeline
|
||||
```
|
||||
## Source
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 7s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `hw-pipeline.html` | `compositions/hw-pipeline.html` | hyperframes:composition |
|
||||
| `assets/fonts/Caveat-700-latin.woff2` | `assets/fonts/Caveat-700-latin.woff2` | hyperframes:asset |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`hw-pipeline.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="hw-pipeline" data-composition-src="compositions/hw-pipeline.html" data-start="0" data-duration="7" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,205 @@ 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"
|
||||
---
|
||||
|
||||
`handwritten` `transition` `wipe`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="hw-scribble-transition preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/hw-scribble-transition.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-scribble-transition.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add hw-scribble-transition" item="hw-scribble-transition" />
|
||||
|
||||
```text
|
||||
Add the Scribble Transition block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/hw-scribble-transition.html`.
|
||||
|
||||
## Add it to your video
|
||||
|
||||
It runs for 2.2 seconds at 1920×1080. Paste this into your composition:
|
||||
|
||||
```html index.html
|
||||
<div
|
||||
data-composition-id="hw-scribble-transition"
|
||||
data-composition-src="compositions/hw-scribble-transition.html"
|
||||
data-start="0"
|
||||
data-duration="2.2"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
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.
|
||||
|
||||
```bash
|
||||
npx hyperframes add hw-scribble-transition
|
||||
```
|
||||
## Source
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 2.2s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `hw-scribble-transition.html` | `compositions/hw-scribble-transition.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`hw-scribble-transition.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="hw-scribble-transition" data-composition-src="compositions/hw-scribble-transition.html" data-start="0" data-duration="2.2" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,50 +3,338 @@ 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"
|
||||
---
|
||||
|
||||
`handwritten` `speech-bubble` `overlay`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="hw-text-cloud preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/hw-text-cloud.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-text-cloud.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add hw-text-cloud" item="hw-text-cloud" />
|
||||
|
||||
```text
|
||||
Add the Speech Bubble block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes `compositions/hw-text-cloud.html`, plus 1 supporting file under `assets/fonts/`.
|
||||
|
||||
## 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="hw-text-cloud"
|
||||
data-composition-src="compositions/hw-text-cloud.html"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
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.
|
||||
|
||||
```bash
|
||||
npx hyperframes add hw-text-cloud
|
||||
```
|
||||
## Source
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 6s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `hw-text-cloud.html` | `compositions/hw-text-cloud.html` | hyperframes:composition |
|
||||
| `assets/fonts/Caveat-700-latin.woff2` | `assets/fonts/Caveat-700-latin.woff2` | hyperframes:asset |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`hw-text-cloud.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="hw-text-cloud" data-composition-src="compositions/hw-text-cloud.html" data-start="0" data-duration="6" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,50 +3,310 @@ 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"
|
||||
---
|
||||
|
||||
`handwritten` `typography` `title-card` `overlay`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="hw-title preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/hw-title.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/hw-title.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add hw-title" item="hw-title" />
|
||||
|
||||
```text
|
||||
Add the Handwritten Title block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes `compositions/hw-title.html`, plus 1 supporting file under `assets/fonts/`.
|
||||
|
||||
## 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="hw-title"
|
||||
data-composition-src="compositions/hw-title.html"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
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.
|
||||
|
||||
```bash
|
||||
npx hyperframes add hw-title
|
||||
```
|
||||
## Source
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 6s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `hw-title.html` | `compositions/hw-title.html` | hyperframes:composition |
|
||||
| `assets/fonts/Caveat-700-latin.woff2` | `assets/fonts/Caveat-700-latin.woff2` | hyperframes:asset |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`hw-title.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="hw-title" data-composition-src="compositions/hw-title.html" data-start="0" data-duration="6" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,50 +3,330 @@ title: "Instagram Follow"
|
||||
description: "Animated Instagram follow overlay with profile card and follow button"
|
||||
---
|
||||
|
||||
`social` `overlay` `instagram`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="instagram-follow preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/instagram-follow.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/instagram-follow.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add instagram-follow" item="instagram-follow" />
|
||||
|
||||
```text
|
||||
Add the Instagram Follow block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes `compositions/instagram-follow.html`, plus 1 supporting file under `assets/`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`instagram-follow.html`}>
|
||||
|
||||
```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-width="1080"
|
||||
data-height="1920"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add instagram-follow
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1080×1920 |
|
||||
| Duration | 4.5s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `instagram-follow.html` | `compositions/instagram-follow.html` | hyperframes:composition |
|
||||
| `assets/avatar.jpg` | `assets/avatar.jpg` | hyperframes:asset |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `social` `overlay` `instagram`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,49 +3,407 @@ title: "Light Leak"
|
||||
description: "Shader transition with cinematic light leak overlay"
|
||||
---
|
||||
|
||||
`transition` `shader`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="light-leak preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/light-leak.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/light-leak.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add light-leak" item="light-leak" />
|
||||
|
||||
```text
|
||||
Add the Light Leak block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/light-leak.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`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-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add light-leak
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `light-leak.html` | `compositions/light-leak.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `transition` `shader`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,54 +3,671 @@ title: "Liquid Glass Context Menu"
|
||||
description: "Frosted glass context menu panel drifting over an aurora shader background"
|
||||
---
|
||||
|
||||
`html-in-canvas` `liquid-glass-html-in-canvas` `webgpu`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<Warning>
|
||||
**Requires Chrome flag.** Enable `chrome://flags/#canvas-draw-element` for live preview. Rendering via CLI enables the flag automatically. [Learn more](/guides/html-in-canvas).
|
||||
</Warning>
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="liquid-glass-context-menu preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/liquid-glass-context-menu.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-context-menu.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
<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 />
|
||||
## Install
|
||||
|
||||
## Add it to a project
|
||||
<InstallCommand command="npx hyperframes add liquid-glass-context-menu" item="liquid-glass-context-menu" />
|
||||
|
||||
### Ask your agent
|
||||
That writes `compositions/liquid-glass-context-menu.html`, plus 1 supporting file under `compositions/lib/`.
|
||||
|
||||
```text
|
||||
Add the Liquid Glass Context Menu block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
<Danger>
|
||||
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
|
||||
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>
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`liquid-glass-context-menu.html`}>
|
||||
|
||||
```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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add liquid-glass-context-menu
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `liquid-glass-context-menu.html` | `compositions/liquid-glass-context-menu.html` | hyperframes:composition |
|
||||
| `lib/liquid-glass.iife.js` | `compositions/lib/liquid-glass.iife.js` | registry:asset |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `html-in-canvas` `liquid-glass-html-in-canvas` `webgpu`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,54 +3,732 @@ title: "Liquid Glass Media Controls"
|
||||
description: "Frosted glass media control panels spreading over an aurora shader background"
|
||||
---
|
||||
|
||||
`html-in-canvas` `liquid-glass-html-in-canvas` `webgpu`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<Warning>
|
||||
**Requires Chrome flag.** Enable `chrome://flags/#canvas-draw-element` for live preview. Rendering via CLI enables the flag automatically. [Learn more](/guides/html-in-canvas).
|
||||
</Warning>
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="liquid-glass-media-controls preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/liquid-glass-media-controls.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-media-controls.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
<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 />
|
||||
## Install
|
||||
|
||||
## Add it to a project
|
||||
<InstallCommand command="npx hyperframes add liquid-glass-media-controls" item="liquid-glass-media-controls" />
|
||||
|
||||
### Ask your agent
|
||||
That writes `compositions/liquid-glass-media-controls.html`, plus 1 supporting file under `compositions/lib/`.
|
||||
|
||||
```text
|
||||
Add the Liquid Glass Media Controls block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
<Danger>
|
||||
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
|
||||
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>
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`liquid-glass-media-controls.html`}>
|
||||
|
||||
```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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add liquid-glass-media-controls
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `liquid-glass-media-controls.html` | `compositions/liquid-glass-media-controls.html` | hyperframes:composition |
|
||||
| `lib/liquid-glass.iife.js` | `compositions/lib/liquid-glass.iife.js` | registry:asset |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `html-in-canvas` `liquid-glass-html-in-canvas` `webgpu`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,55 +3,529 @@ title: "Liquid Glass Notification"
|
||||
description: "Frosted glass notification cards floating over an aurora shader background"
|
||||
---
|
||||
|
||||
`html-in-canvas` `liquid-glass-html-in-canvas` `webgpu`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<Warning>
|
||||
**Requires Chrome flag.** Enable `chrome://flags/#canvas-draw-element` for live preview. Rendering via CLI enables the flag automatically. [Learn more](/guides/html-in-canvas).
|
||||
</Warning>
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="liquid-glass-notification preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/liquid-glass-notification.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-notification.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
<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 />
|
||||
## Install
|
||||
|
||||
## Add it to a project
|
||||
<InstallCommand command="npx hyperframes add liquid-glass-notification" item="liquid-glass-notification" />
|
||||
|
||||
### Ask your agent
|
||||
That writes `compositions/liquid-glass-notification.html`, plus 2 supporting files under `compositions/assets/` and `compositions/lib/`.
|
||||
|
||||
```text
|
||||
Add the Liquid Glass Notification block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
<Danger>
|
||||
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
|
||||
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>
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`liquid-glass-notification.html`}>
|
||||
|
||||
```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-start="0"
|
||||
data-duration="8"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add liquid-glass-notification
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `liquid-glass-notification.html` | `compositions/liquid-glass-notification.html` | hyperframes:composition |
|
||||
| `lib/liquid-glass.iife.js` | `compositions/lib/liquid-glass.iife.js` | registry:asset |
|
||||
| `assets/hyperframes-logo-dark.svg` | `compositions/assets/hyperframes-logo-dark.svg` | registry:asset |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `html-in-canvas` `liquid-glass-html-in-canvas` `webgpu`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,55 +3,807 @@ title: "Liquid Glass Widgets"
|
||||
description: "Frosted glass stat cards, showcase panel and pill chips over an aurora shader background"
|
||||
---
|
||||
|
||||
`html-in-canvas` `liquid-glass-html-in-canvas` `webgpu`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<Warning>
|
||||
**Requires Chrome flag.** Enable `chrome://flags/#canvas-draw-element` for live preview. Rendering via CLI enables the flag automatically. [Learn more](/guides/html-in-canvas).
|
||||
</Warning>
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="liquid-glass-widgets preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/liquid-glass-widgets.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/liquid-glass-widgets.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
<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 />
|
||||
## Install
|
||||
|
||||
## Add it to a project
|
||||
<InstallCommand command="npx hyperframes add liquid-glass-widgets" item="liquid-glass-widgets" />
|
||||
|
||||
### Ask your agent
|
||||
That writes `compositions/liquid-glass-widgets.html`, plus 2 supporting files under `compositions/assets/` and `compositions/lib/`.
|
||||
|
||||
```text
|
||||
Add the Liquid Glass Widgets block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
<Danger>
|
||||
Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
|
||||
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>
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`liquid-glass-widgets.html`}>
|
||||
|
||||
```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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add liquid-glass-widgets
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `liquid-glass-widgets.html` | `compositions/liquid-glass-widgets.html` | hyperframes:composition |
|
||||
| `assets/hyperframes-logo-dark.svg` | `compositions/assets/hyperframes-logo-dark.svg` | registry:asset |
|
||||
| `lib/liquid-glass.iife.js` | `compositions/lib/liquid-glass.iife.js` | registry:asset |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `html-in-canvas` `liquid-glass-html-in-canvas` `webgpu`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,275 @@ title: "Logo Outro"
|
||||
description: "Cinematic logo reveal with piece-by-piece assembly, glow bloom, tagline fade-in, and URL pill"
|
||||
---
|
||||
|
||||
`branding` `outro` `logo`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="logo-outro preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/logo-outro.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/logo-outro.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add logo-outro" item="logo-outro" />
|
||||
|
||||
```text
|
||||
Add the Logo Outro block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/logo-outro.html`.
|
||||
|
||||
## Change the colors
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add logo-outro
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 6s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `logo-outro.html` | `compositions/logo-outro.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,51 +3,371 @@ 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."
|
||||
---
|
||||
|
||||
`broadcast` `lower-third` `news` `overlay`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
|
||||
|
||||
<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 />
|
||||
<VariablesExplorer
|
||||
previewSrc="/public/catalog/blocks/lower-third-bild.json"
|
||||
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"}]}
|
||||
>
|
||||
|
||||
## Add it to a project
|
||||
```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;
|
||||
}
|
||||
|
||||
### Ask your agent
|
||||
#lb-root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
```text
|
||||
Add the Lower Third — BILD Style block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
#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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</VariablesExplorer>
|
||||
|
||||
```bash
|
||||
npx hyperframes add lower-third-bild
|
||||
## Install
|
||||
|
||||
<InstallCommand command="npx hyperframes add lower-third-bild" item="lower-third-bild" />
|
||||
|
||||
That writes `compositions/lower-third-bild.html`, plus 2 supporting files under `compositions/assets/fonts/`.
|
||||
|
||||
## Variables
|
||||
|
||||
Every one of these has a default, so the piece works untouched. Set the ones you
|
||||
want to change on the element:
|
||||
|
||||
| Variable | Default | Accepts | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| `TXT_MAIN_1_Line` | `BILD EXKLUSIV` | string | |
|
||||
| `TXT_SUB` | `Sonderbericht` | string | |
|
||||
|
||||
Set them with `data-variable-values` on the element that mounts it. These are the
|
||||
defaults, so this behaves exactly like the preview above until you change one:
|
||||
|
||||
```html wrap
|
||||
<div
|
||||
data-composition-id="lower-third-bild"
|
||||
data-composition-src="compositions/lower-third-bild.html"
|
||||
data-variable-values='{"TXT_MAIN_1_Line":"BILD EXKLUSIV","TXT_SUB":"Sonderbericht"}'
|
||||
></div>
|
||||
```
|
||||
|
||||
## Details
|
||||
## Source
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 8s |
|
||||
<Accordion title={`lower-third-bild.html`}>
|
||||
|
||||
**Installed files**
|
||||
```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;
|
||||
}
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `lower-third-bild.html` | `compositions/lower-third-bild.html` | hyperframes:composition |
|
||||
| `assets/fonts/BarlowCondensed-Bold.woff2` | `compositions/assets/fonts/BarlowCondensed-Bold.woff2` | hyperframes:asset |
|
||||
| `assets/fonts/BarlowCondensed-ExtraBold.woff2` | `compositions/assets/fonts/BarlowCondensed-ExtraBold.woff2` | hyperframes:asset |
|
||||
#lb-root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
## Use it
|
||||
#lb-main-outer {
|
||||
position: absolute;
|
||||
bottom: 215px;
|
||||
left: 80px;
|
||||
padding-right: 14px;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
#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;
|
||||
}
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
#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>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Usage
|
||||
|
||||
After installing, add the block to your host composition:
|
||||
|
||||
```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>
|
||||
```
|
||||
|
||||
{/* hf:generated-footer */}
|
||||
|
||||
Tagged `broadcast` `lower-third` `news` `overlay`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,148 @@ 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"
|
||||
---
|
||||
|
||||
`lower-third` `overlay` `podcast` `interview` `minimal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="lt-accent-underline preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/lt-accent-underline.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-accent-underline.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add lt-accent-underline" item="lt-accent-underline" />
|
||||
|
||||
```text
|
||||
Add the Lower Third — Accent Underline block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/lt-accent-underline.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`lt-accent-underline.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>
|
||||
* {
|
||||
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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add lt-accent-underline
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4.8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `lt-accent-underline.html` | `compositions/lt-accent-underline.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `lower-third` `overlay` `podcast` `interview` `minimal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,150 @@ title: "Lower Third — Bold Block"
|
||||
description: "High-energy podcast lower third: solid dark block wipes in, uppercase name slams up, accent tag pops"
|
||||
---
|
||||
|
||||
`lower-third` `overlay` `podcast` `interview` `bold`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="lt-bold-block preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/lt-bold-block.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-bold-block.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add lt-bold-block" item="lt-bold-block" />
|
||||
|
||||
```text
|
||||
Add the Lower Third — Bold Block block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/lt-bold-block.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`lt-bold-block.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>
|
||||
* {
|
||||
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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add lt-bold-block
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4.8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `lt-bold-block.html` | `compositions/lt-bold-block.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `lower-third` `overlay` `podcast` `interview` `bold`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,153 @@ title: "Lower Third — Clean Bar"
|
||||
description: "Minimal white-card lower third for podcasts/interviews: accent tab, name + role, clip-wipe entrance"
|
||||
---
|
||||
|
||||
`lower-third` `overlay` `podcast` `interview`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="lt-clean-bar preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/lt-clean-bar.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-clean-bar.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add lt-clean-bar" item="lt-clean-bar" />
|
||||
|
||||
```text
|
||||
Add the Lower Third — Clean Bar block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/lt-clean-bar.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`lt-clean-bar.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>
|
||||
* {
|
||||
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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add lt-clean-bar
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4.8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `lt-clean-bar.html` | `compositions/lt-clean-bar.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `lower-third` `overlay` `podcast` `interview`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,142 @@ title: "Lower Third — Color Block"
|
||||
description: "High-energy lower third: bold accent-color block slides in with overshoot, condensed name + mono role"
|
||||
---
|
||||
|
||||
`lower-third` `overlay` `podcast` `interview` `bold`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="lt-color-block preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/lt-color-block.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-color-block.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add lt-color-block" item="lt-color-block" />
|
||||
|
||||
```text
|
||||
Add the Lower Third — Color Block block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/lt-color-block.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`lt-color-block.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>
|
||||
* {
|
||||
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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add lt-color-block
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4.8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `lt-color-block.html` | `compositions/lt-color-block.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `lower-third` `overlay` `podcast` `interview` `bold`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,152 @@ title: "Lower Third — Dark Card"
|
||||
description: "Charcoal card lower third for bright footage: name, drawn accent underline, role; slide-up entrance"
|
||||
---
|
||||
|
||||
`lower-third` `overlay` `podcast` `interview` `dark`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="lt-dark-card preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/lt-dark-card.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-dark-card.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add lt-dark-card" item="lt-dark-card" />
|
||||
|
||||
```text
|
||||
Add the Lower Third — Dark Card block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/lt-dark-card.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`lt-dark-card.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>
|
||||
* {
|
||||
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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add lt-dark-card
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4.8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `lt-dark-card.html` | `compositions/lt-dark-card.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `lower-third` `overlay` `podcast` `interview` `dark`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,155 @@ title: "Lower Third — Kicker Name"
|
||||
description: "Cardless lower third with an accent eyebrow/kicker tag, heavy name, and a drawn baseline; for footage"
|
||||
---
|
||||
|
||||
`lower-third` `overlay` `podcast` `interview` `bold`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="lt-kicker-name preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/lt-kicker-name.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-kicker-name.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add lt-kicker-name" item="lt-kicker-name" />
|
||||
|
||||
```text
|
||||
Add the Lower Third — Kicker Name block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/lt-kicker-name.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`lt-kicker-name.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>
|
||||
* {
|
||||
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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add lt-kicker-name
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4.8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `lt-kicker-name.html` | `compositions/lt-kicker-name.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `lower-third` `overlay` `podcast` `interview` `bold`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,161 @@ 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"
|
||||
---
|
||||
|
||||
`lower-third` `overlay` `podcast` `interview` `bold`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="lt-mask-reveal preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/lt-mask-reveal.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-mask-reveal.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add lt-mask-reveal" item="lt-mask-reveal" />
|
||||
|
||||
```text
|
||||
Add the Lower Third — Mask Reveal block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/lt-mask-reveal.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`lt-mask-reveal.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>
|
||||
* {
|
||||
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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add lt-mask-reveal
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4.8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `lt-mask-reveal.html` | `compositions/lt-mask-reveal.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `lower-third` `overlay` `podcast` `interview` `bold`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -0,0 +1,885 @@
|
||||
---
|
||||
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/blocks/lt-neon-border.json"
|
||||
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" item="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`.
|
||||
|
||||
## Variables
|
||||
|
||||
Every one of these has a default, so the piece works untouched. Set the ones you
|
||||
want to change on the element:
|
||||
|
||||
| Variable | Default | Accepts | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| `name` | `Dr. Maya Chen` | string | |
|
||||
| `role` | `Host · Neuroscientist` | string | |
|
||||
| `accent` | `#cc9149` | color | |
|
||||
| `movement` | `step` | `step`, `glide` | |
|
||||
| `speed` | `1.6` | 0.2corners/s to 6corners/s, step 0.1corners/s | |
|
||||
| `cornerRadius` | `26` | 0px to 90px, step 1px | |
|
||||
| `thickness` | `5` | 1px to 16px, step 1px | |
|
||||
| `arcLength` | `22` | 5% to 100%, step 1% | |
|
||||
| `glow` | `1` | 0x to 2x, step 0.05x | |
|
||||
|
||||
Set them with `data-variable-values` on the element that mounts it. These are the
|
||||
defaults, so this behaves exactly like the preview above until you change one:
|
||||
|
||||
```html wrap
|
||||
<div
|
||||
data-composition-id="lt-neon-border"
|
||||
data-composition-src="compositions/lt-neon-border.html"
|
||||
data-variable-values='{"name":"Dr. Maya Chen","role":"Host · Neuroscientist","accent":"#cc9149","movement":"step","speed":1.6,"cornerRadius":26,"thickness":5,"arcLength":22,"glow":1}'
|
||||
></div>
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`lt-neon-border.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>
|
||||
* {
|
||||
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>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
{/* 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,49 +3,155 @@ title: "Lower Third — Side Rule"
|
||||
description: "Cardless lower third with a vertical accent bar; condensed display name + mono role, text-shadowed for footage"
|
||||
---
|
||||
|
||||
`lower-third` `overlay` `podcast` `interview` `minimal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="lt-side-rule preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/lt-side-rule.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-side-rule.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add lt-side-rule" item="lt-side-rule" />
|
||||
|
||||
```text
|
||||
Add the Lower Third — Side Rule block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/lt-side-rule.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`lt-side-rule.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>
|
||||
* {
|
||||
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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add lt-side-rule
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4.8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `lt-side-rule.html` | `compositions/lt-side-rule.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `lower-third` `overlay` `podcast` `interview` `minimal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,160 @@ title: "Lower Third — Soft Pill"
|
||||
description: "Rounded white pill lower third for podcasts/interviews: status dot, name + role, scale-pop entrance"
|
||||
---
|
||||
|
||||
`lower-third` `overlay` `podcast` `interview` `minimal`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="lt-soft-pill preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/lt-soft-pill.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-soft-pill.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add lt-soft-pill" item="lt-soft-pill" />
|
||||
|
||||
```text
|
||||
Add the Lower Third — Soft Pill block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/lt-soft-pill.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`lt-soft-pill.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>
|
||||
* {
|
||||
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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add lt-soft-pill
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4.8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `lt-soft-pill.html` | `compositions/lt-soft-pill.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `lower-third` `overlay` `podcast` `interview` `minimal`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,150 @@ 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"
|
||||
---
|
||||
|
||||
`lower-third` `overlay` `podcast` `interview` `bold`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="lt-stack-bars preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/lt-stack-bars.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/lt-stack-bars.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add lt-stack-bars" item="lt-stack-bars" />
|
||||
|
||||
```text
|
||||
Add the Lower Third — Stack Bars block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/lt-stack-bars.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`lt-stack-bars.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>
|
||||
* {
|
||||
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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add lt-stack-bars
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 4.8s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `lt-stack-bars.html` | `compositions/lt-stack-bars.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `lower-third` `overlay` `podcast` `interview` `bold`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
@@ -3,49 +3,243 @@ title: "macOS Notification"
|
||||
description: "Animated macOS-style notification banner with app icon and message"
|
||||
---
|
||||
|
||||
`social` `overlay` `notification`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="macos-notification preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/macos-notification.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/macos-notification.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add macos-notification" item="macos-notification" />
|
||||
|
||||
```text
|
||||
Add the macOS Notification block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/macos-notification.html`.
|
||||
|
||||
## Source
|
||||
|
||||
<Accordion title={`macos-notification.html`}>
|
||||
|
||||
```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-width="1920"
|
||||
data-height="1080"
|
||||
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>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
npx hyperframes add macos-notification
|
||||
```
|
||||
## Usage
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 5s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `macos-notification.html` | `compositions/macos-notification.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
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 */}
|
||||
|
||||
Tagged `social` `overlay` `notification`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,49 +3,288 @@ 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"
|
||||
---
|
||||
|
||||
`background` `gradient` `minimal` `professional` `presentation`
|
||||
import { InstallCommand } from "/snippets/install-command.jsx";
|
||||
|
||||
<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 />
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
|
||||
title="mk-background preview"
|
||||
loading="lazy"
|
||||
srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/mk-background.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/mk-background.png");document.body.appendChild(p)});<\/script></body></html>`}
|
||||
/>
|
||||
|
||||
## Add it to a project
|
||||
## Install
|
||||
|
||||
### Ask your agent
|
||||
<InstallCommand command="npx hyperframes add mk-background" item="mk-background" />
|
||||
|
||||
```text
|
||||
Add the Minimal Gradient Background block from the HyperFrames Catalog to this project.
|
||||
Replace the demo content with mine and match the existing design and timing.
|
||||
That writes one file: `compositions/mk-background.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="mk-background"
|
||||
data-composition-src="compositions/mk-background.html"
|
||||
data-start="0"
|
||||
data-duration="10"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
### Install from the terminal
|
||||
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.
|
||||
|
||||
```bash
|
||||
npx hyperframes add mk-background
|
||||
```
|
||||
## Source
|
||||
|
||||
## Details
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Type | Block |
|
||||
| Dimensions | 1920×1080 |
|
||||
| Duration | 10s |
|
||||
|
||||
**Installed files**
|
||||
|
||||
| File | Target | Type |
|
||||
| --- | --- | --- |
|
||||
| `mk-background.html` | `compositions/mk-background.html` | hyperframes:composition |
|
||||
|
||||
## Use it
|
||||
|
||||
Ask your agent to replace the example content, match the project style, and place the block at the right moment in the video.
|
||||
|
||||
**Wiring it by hand.** The installed block can be added to a host composition with:
|
||||
<Accordion title={`mk-background.html`}>
|
||||
|
||||
```html
|
||||
<div data-composition-id="mk-background" data-composition-src="compositions/mk-background.html" data-start="0" data-duration="10" data-track-index="1" data-width="1920" data-height="1080"></div>
|
||||
<!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`.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Browse the complete Catalog](/catalog)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user