feat: add Stronkter catalog blocks (#570)

## Problem

The Catalog did not include the four prompt-matched Stronkter one-shot HyperFrames projects, and registry metadata only supported a plain author string, so there was no structured way to show creator attribution or the original generation prompt on generated catalog pages.

## What this fixes

- Adds four Catalog blocks matching the provided prompts, in order:
  - `north-korea-locked-down`
  - `apple-money-count`
  - `nyc-paris-flight`
  - `goonvpn-youtube-spot`
- Attributes each block to [Stronkter](https://x.com/Stronkter).
- Stores and renders the original source prompt for each generated catalog page.
- Adds a local realistic map plate for the North Korea block so rendering does not depend on live map tile requests.
- Extends registry item metadata/schema with `authorUrl` and `sourcePrompt`.
- Updates catalog page generation to read items from `registry/registry.json`, keeping generated docs aligned to the public registry manifest.
- Ignores normal browser media preload `net::ERR_ABORTED` request failures for media assets during `hyperframes validate`, while preserving failures for real missing assets.

## Root cause

The imported projects are Catalog-ready compositions, but the registry/docs pipeline did not have first-class source-prompt or linked-author fields to expose creator credit on generated MDX pages. The audio-backed compositions also surfaced a validation edge case: Chrome can report aborted media preload requests as `net::ERR_ABORTED` even when the audio file exists and playback is valid.

## Verification

### Local

- `bun run --filter @hyperframes/cli test src/commands/validate.test.ts`
- `bun run --filter @hyperframes/core test src/registry/types.test.ts`
- `bun run sync-schemas:check`
- `bunx oxlint packages/cli/src/commands/validate.ts packages/cli/src/commands/validate.test.ts packages/core/src/registry/types.ts packages/core/src/registry/types.test.ts scripts/generate-catalog-pages.ts`
- `bunx oxfmt --check ...` on changed source, registry, docs, and composition files
- `git diff --check`
- `bun packages/cli/src/cli.ts lint` and `validate` against temp installed projects for all four blocks
- Lefthook pre-commit: lint/format/typecheck on the initial commit, plus format on the amend
- Lefthook commit-msg: commitlint

### Browser

- Exercised all four blocks through HyperFrames preview routes with `agent-browser`.
- Captured playback screenshots and WebM recordings for:
  - `north-korea-locked-down`
  - `apple-money-count`
  - `nyc-paris-flight`
  - `goonvpn-youtube-spot`

## Notes

- The zip also contained unrelated project directories, but this PR intentionally includes only the four prompt-matched Catalog blocks requested here.
- The imported one-shot compositions may trigger the existing large-composition lint warning, but there are no lint errors and runtime validation passes.
This commit is contained in:
Miguel Ángel
2026-04-29 22:59:19 +02:00
committed by GitHub
parent ea3b708b12
commit 4d05b475f0
27 changed files with 2637 additions and 23 deletions
+53
View File
@@ -0,0 +1,53 @@
---
title: "Apple Money Count"
description: "Apple-style finance counter that counts from $0 to $10,000, flashes green, and bursts money icons with sound."
---
# Apple Money Count
Apple-style finance counter that counts from $0 to $10,000, flashes green, and bursts money icons with sound.
`showcase` `finance` `kinetic` `youtube` `sfx`
Created by [Stronkter](https://x.com/Stronkter).
## Source Prompt
```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 />
## Install
<CodeGroup>
```bash Terminal
npx hyperframes add apple-money-count
```
</CodeGroup>
## Details
| Property | Value |
| --- | --- |
| Type | Block |
| Dimensions | 1920×1080 |
| Duration | 5s |
## 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 |
## 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>
```
@@ -0,0 +1,53 @@
---
title: "GoonVPN YouTube Spot"
description: "Snappy Apple-style YouTube insert showing a phone finding and installing a fictional VPN app with sound effects."
---
# GoonVPN YouTube Spot
Snappy Apple-style YouTube insert showing a phone finding and installing a fictional VPN app with sound effects.
`app` `showcase` `youtube` `sfx`
Created by [Stronkter](https://x.com/Stronkter).
## Source Prompt
```text
📷HyperFrames by HeyGen make me a 7s video apple style bold font and styling a phone scrolling in a appstore clicking on a vpn app called goonVPN and installing it the app installs the phone snaps down and fades to a white backround make it snappy and pop its for a youtube video also add soundfx and make it 60fps and 1920x1080
```
<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/goonvpn-youtube-spot.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/goonvpn-youtube-spot.png" autoPlay muted loop playsInline />
## Install
<CodeGroup>
```bash Terminal
npx hyperframes add goonvpn-youtube-spot
```
</CodeGroup>
## Details
| Property | Value |
| --- | --- |
| Type | Block |
| Dimensions | 1920×1080 |
| Duration | 7s |
## Files
| File | Target | Type |
| --- | --- | --- |
| `goonvpn-youtube-spot.html` | `compositions/goonvpn-youtube-spot.html` | hyperframes:composition |
| `assets/goonvpn-sfx.wav` | `assets/goonvpn-sfx.wav` | hyperframes:asset |
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="goonvpn-youtube-spot" data-composition-src="compositions/goonvpn-youtube-spot.html" data-start="0" data-duration="7" data-track-index="1" data-width="1920" data-height="1080"></div>
```
@@ -0,0 +1,53 @@
---
title: "North Korea Locked Down"
description: "Realistic map zoom into North Korea with a red scribble circle, locked-down pop-up label, and reddish editorial wash."
---
# North Korea Locked Down
Realistic map zoom into North Korea with a red scribble circle, locked-down pop-up label, and reddish editorial wash.
`showcase` `map` `annotation` `youtube` `kinetic`
Created by [Stronkter](https://x.com/Stronkter).
## Source Prompt
```text
use 📷HyperFrames by HeyGen and Image Gen if you need it for assets or like png images of assets without backround to make a youtube style camera moving in out and other things that are in youtube videos, to make a video of a map zooms in on north korea and a scribble style circle circles the country and a text pops up above it saying locked down when the text apears the video turns a bit redish make the video 7 seconds long id like the map to look realistic and accurate to real lfe
```
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/north-korea-locked-down.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/north-korea-locked-down.png" autoPlay muted loop playsInline />
## Install
<CodeGroup>
```bash Terminal
npx hyperframes add north-korea-locked-down
```
</CodeGroup>
## Details
| Property | Value |
| --- | --- |
| Type | Block |
| Dimensions | 1920×1080 |
| Duration | 7s |
## Files
| File | Target | Type |
| --- | --- | --- |
| `north-korea-locked-down.html` | `compositions/north-korea-locked-down.html` | hyperframes:composition |
| `assets/korea-map.png` | `assets/korea-map.png` | hyperframes:asset |
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="north-korea-locked-down" data-composition-src="compositions/north-korea-locked-down.html" data-start="0" data-duration="7" data-track-index="1" data-width="1920" data-height="1080"></div>
```
+54
View File
@@ -0,0 +1,54 @@
---
title: "NYC Paris Flight"
description: "Apple-style realistic map animation with a plane flying from New York to Paris, marker circle, landing pop, and sound effects."
---
# NYC Paris Flight
Apple-style realistic map animation with a plane flying from New York to Paris, marker circle, landing pop, and sound effects.
`showcase` `travel` `map` `youtube` `sfx`
Created by [Stronkter](https://x.com/Stronkter).
## Source Prompt
```text
📷HyperFrames by HeyGen Make a six-second Apple-style font bold video of a plane going from New York to Paris. A map animation, and then it shows the plane going from New York to Paris. Make the fonts Apple-style bold and make the map actual realistic, actually realistic. Before it lands in Paris, do a doodle circle in red around Paris, and then it lands in the doodle circle, and the video basically cuts to a white screen at the end. Also add sound effects for everything as well. Every nice motion, every the plane for pop-ups, bubble, pop-up effect, everything you want.
```
<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/nyc-paris-flight.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/nyc-paris-flight.png" autoPlay muted loop playsInline />
## Install
<CodeGroup>
```bash Terminal
npx hyperframes add nyc-paris-flight
```
</CodeGroup>
## Details
| Property | Value |
| --- | --- |
| Type | Block |
| Dimensions | 1920×1080 |
| Duration | 6s |
## Files
| File | Target | Type |
| --- | --- | --- |
| `nyc-paris-flight.html` | `compositions/nyc-paris-flight.html` | hyperframes:composition |
| `assets/map-nyc-paris.png` | `assets/map-nyc-paris.png` | hyperframes:asset |
| `assets/sfx-mix.wav` | `assets/sfx-mix.wav` | hyperframes:asset |
## Usage
After installing, add the block to your host composition:
```html
<div data-composition-id="nyc-paris-flight" data-composition-src="compositions/nyc-paris-flight.html" data-start="0" data-duration="6" data-track-index="1" data-width="1920" data-height="1080"></div>
```
+4
View File
@@ -141,6 +141,10 @@
"group": "Showcases", "group": "Showcases",
"pages": [ "pages": [
"catalog/blocks/app-showcase", "catalog/blocks/app-showcase",
"catalog/blocks/apple-money-count",
"catalog/blocks/goonvpn-youtube-spot",
"catalog/blocks/north-korea-locked-down",
"catalog/blocks/nyc-paris-flight",
"catalog/blocks/ui-3d-reveal" "catalog/blocks/ui-3d-reveal"
] ]
}, },
+59
View File
@@ -12,6 +12,21 @@
"href": "/catalog/blocks/app-showcase", "href": "/catalog/blocks/app-showcase",
"preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/app-showcase.png" "preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/app-showcase.png"
}, },
{
"name": "apple-money-count",
"type": "block",
"title": "Apple Money Count",
"description": "Apple-style finance counter that counts from $0 to $10,000, flashes green, and bursts money icons with sound.",
"tags": [
"showcase",
"finance",
"kinetic",
"youtube",
"sfx"
],
"href": "/catalog/blocks/apple-money-count",
"preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/apple-money-count.png"
},
{ {
"name": "chromatic-radial-split", "name": "chromatic-radial-split",
"type": "block", "type": "block",
@@ -110,6 +125,20 @@
"href": "/catalog/blocks/glitch", "href": "/catalog/blocks/glitch",
"preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/glitch.png" "preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/glitch.png"
}, },
{
"name": "goonvpn-youtube-spot",
"type": "block",
"title": "GoonVPN YouTube Spot",
"description": "Snappy Apple-style YouTube insert showing a phone finding and installing a fictional VPN app with sound effects.",
"tags": [
"app",
"showcase",
"youtube",
"sfx"
],
"href": "/catalog/blocks/goonvpn-youtube-spot",
"preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/goonvpn-youtube-spot.png"
},
{ {
"name": "grain-overlay", "name": "grain-overlay",
"type": "component", "type": "component",
@@ -201,6 +230,36 @@
"href": "/catalog/blocks/macos-notification", "href": "/catalog/blocks/macos-notification",
"preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/macos-notification.png" "preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/macos-notification.png"
}, },
{
"name": "north-korea-locked-down",
"type": "block",
"title": "North Korea Locked Down",
"description": "Realistic map zoom into North Korea with a red scribble circle, locked-down pop-up label, and reddish editorial wash.",
"tags": [
"showcase",
"map",
"annotation",
"youtube",
"kinetic"
],
"href": "/catalog/blocks/north-korea-locked-down",
"preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/north-korea-locked-down.png"
},
{
"name": "nyc-paris-flight",
"type": "block",
"title": "NYC Paris Flight",
"description": "Apple-style realistic map animation with a plane flying from New York to Paris, marker circle, landing pop, and sound effects.",
"tags": [
"showcase",
"travel",
"map",
"youtube",
"sfx"
],
"href": "/catalog/blocks/nyc-paris-flight",
"preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/nyc-paris-flight.png"
},
{ {
"name": "reddit-post", "name": "reddit-post",
"type": "block", "type": "block",
+8
View File
@@ -35,6 +35,14 @@
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
}, },
"authorUrl": {
"type": "string",
"format": "uri"
},
"sourcePrompt": {
"type": "string",
"minLength": 1
},
"license": { "license": {
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { shouldIgnoreRequestFailure } from "./validate.js";
describe("shouldIgnoreRequestFailure", () => {
it("ignores aborted media preload requests", () => {
expect(
shouldIgnoreRequestFailure("http://127.0.0.1:3000/assets/sfx.wav", "net::ERR_ABORTED"),
).toBe(true);
expect(shouldIgnoreRequestFailure("http://127.0.0.1:3000/video.mp4", "net::ERR_ABORTED")).toBe(
true,
);
});
it("keeps non-media and non-aborted failures reportable", () => {
expect(
shouldIgnoreRequestFailure("http://127.0.0.1:3000/assets/map.png", "net::ERR_ABORTED"),
).toBe(false);
expect(
shouldIgnoreRequestFailure("http://127.0.0.1:3000/assets/sfx.wav", "net::ERR_FAILED"),
).toBe(false);
});
});
+13 -1
View File
@@ -29,6 +29,16 @@ interface ContrastEntry {
const CONTRAST_SAMPLES = 5; const CONTRAST_SAMPLES = 5;
const SEEK_SETTLE_MS = 150; const SEEK_SETTLE_MS = 150;
const MEDIA_EXTENSIONS = /\.(aac|flac|m4a|mov|mp3|mp4|oga|ogg|wav|webm)$/i;
export function shouldIgnoreRequestFailure(url: string, errorText: string | undefined): boolean {
if (errorText !== "net::ERR_ABORTED") return false;
try {
return MEDIA_EXTENSIONS.test(new URL(url).pathname);
} catch {
return false;
}
}
async function getCompositionDuration(page: import("puppeteer-core").Page): Promise<number> { async function getCompositionDuration(page: import("puppeteer-core").Page): Promise<number> {
return page.evaluate(() => { return page.evaluate(() => {
@@ -183,10 +193,12 @@ async function validateInBrowser(
page.on("requestfailed", (req) => { page.on("requestfailed", (req) => {
const url = req.url(); const url = req.url();
if (url.includes("favicon") || url.startsWith("data:")) return; if (url.includes("favicon") || url.startsWith("data:")) return;
const failureText = req.failure()?.errorText;
if (shouldIgnoreRequestFailure(url, failureText)) return;
const path = decodeURIComponent(new URL(url).pathname).replace(/^\//, ""); const path = decodeURIComponent(new URL(url).pathname).replace(/^\//, "");
errors.push({ errors.push({
level: "error", level: "error",
text: `Failed to load ${path}: ${req.failure()?.errorText ?? "net::ERR_FAILED"}`, text: `Failed to load ${path}: ${failureText ?? "net::ERR_FAILED"}`,
url, url,
}); });
}); });
+8
View File
@@ -35,6 +35,14 @@
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
}, },
"authorUrl": {
"type": "string",
"format": "uri"
},
"sourcePrompt": {
"type": "string",
"minLength": 1
},
"license": { "license": {
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
+5 -1
View File
@@ -212,13 +212,15 @@ describe("registry types", () => {
expect(true).toBe(true); expect(true).toBe(true);
}); });
it("optional metadata fields are accepted (author, license, deprecated, minCliVersion)", () => { it("optional metadata fields are accepted", () => {
const item: ComponentItem = { const item: ComponentItem = {
name: "shader-wipe", name: "shader-wipe",
type: "hyperframes:component", type: "hyperframes:component",
title: "Shader Wipe", title: "Shader Wipe",
description: "d", description: "d",
author: "heygen", author: "heygen",
authorUrl: "https://example.com/heygen",
sourcePrompt: "Create a shader wipe.",
license: "Apache-2.0", license: "Apache-2.0",
minCliVersion: "0.4.0", minCliVersion: "0.4.0",
deprecated: "Use `shader-wipe-v2` instead.", deprecated: "Use `shader-wipe-v2` instead.",
@@ -231,6 +233,8 @@ describe("registry types", () => {
], ],
}; };
expect(item.author).toBe("heygen"); expect(item.author).toBe("heygen");
expect(item.authorUrl).toBe("https://example.com/heygen");
expect(item.sourcePrompt).toBe("Create a shader wipe.");
}); });
}); });
}); });
+4
View File
@@ -48,6 +48,10 @@ interface RegistryItemBase {
tags?: string[]; tags?: string[];
/** Item author / maintainer. */ /** Item author / maintainer. */
author?: string; author?: string;
/** URL for the author / creator credit. */
authorUrl?: string;
/** Original prompt used to create or inspire the item. */
sourcePrompt?: string;
/** SPDX license identifier. */ /** SPDX license identifier. */
license?: string; license?: string;
/** Minimum `hyperframes` CLI version required to install this item (semver). */ /** Minimum `hyperframes` CLI version required to install this item (semver). */
+308
View File
@@ -0,0 +1,308 @@
<!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: #fdfefe;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #fdfefe;
color: #111315;
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
}
.green-flash {
position: absolute;
inset: 0;
z-index: 1;
background: #30d158;
opacity: 0;
}
.stage-content {
position: relative;
z-index: 5;
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
padding: 120px 160px;
}
.amount-wrap {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-width: 1080px;
min-height: 260px;
}
.amount {
position: relative;
color: #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 rgba(255, 255, 255, 0.58),
0 18px 36px rgba(17, 19, 21, 0.14),
0 42px 92px 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 rgba(7, 84, 31, 0.2));
will-change: transform, opacity, filter;
}
.money-icon.bill {
width: 96px;
height: 52px;
border-radius: 10px;
background: #30d158;
box-shadow:
inset 0 0 0 3px rgba(7, 84, 31, 0.2),
inset 0 -10px 18px rgba(7, 84, 31, 0.1),
0 16px 26px rgba(7, 84, 31, 0.2),
0 34px 64px rgba(7, 84, 31, 0.16);
}
.money-icon.bill::before {
position: absolute;
inset: 9px 16px;
border: 2px solid 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%, #fff7a6 0%, #ffd54f 40%, #d9a514 100%);
box-shadow:
inset 0 0 0 4px rgba(111, 76, 0, 0.12),
inset 0 -8px 14px rgba(111, 76, 0, 0.1),
0 16px 30px rgba(111, 76, 0, 0.18),
0 34px 58px rgba(111, 76, 0, 0.12);
}
.money-icon .mark {
position: relative;
z-index: 1;
color: #07541f;
font-size: 30px;
font-weight: 900;
line-height: 1;
}
.money-icon.coin .mark {
color: #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: "#30d158",
textShadow:
"0 3px 0 rgba(255,255,255,0.52), 0 18px 40px rgba(48,209,88,0.3), 0 46px 96px 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>
@@ -0,0 +1,28 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "apple-money-count",
"type": "hyperframes:block",
"title": "Apple Money Count",
"description": "Apple-style finance counter that counts from $0 to $10,000, flashes green, and bursts money icons with sound.",
"tags": ["showcase", "finance", "kinetic", "youtube", "sfx"],
"author": "Stronkter",
"authorUrl": "https://x.com/Stronkter",
"sourcePrompt": "\ud83d\udcf7HyperFrames 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.",
"dimensions": {
"width": 1920,
"height": 1080
},
"duration": 5,
"files": [
{
"path": "apple-money-count.html",
"target": "compositions/apple-money-count.html",
"type": "hyperframes:composition"
},
{
"path": "assets/sfx-production.wav",
"target": "assets/sfx-production.wav",
"type": "hyperframes:asset"
}
]
}
@@ -0,0 +1,987 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #ffffff;
font-family:
-apple-system, BlinkMacSystemFont, "SF Pro Display", Inter, "Helvetica Neue", Arial,
sans-serif;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
color: #111111;
background:
radial-gradient(circle at 20% 16%, rgba(0, 122, 255, 0.13), transparent 24%),
radial-gradient(circle at 82% 22%, rgba(52, 199, 89, 0.14), transparent 22%),
linear-gradient(180deg, #ffffff 0%, #f5f5f7 100%);
}
.stage {
position: absolute;
inset: 0;
display: grid;
place-items: center;
padding: 70px 120px;
}
.headline {
position: absolute;
left: 118px;
top: 104px;
width: 580px;
display: flex;
flex-direction: column;
gap: 2px;
z-index: 2;
pointer-events: none;
}
.headline span {
display: block;
font-size: 92px;
line-height: 0.92;
font-weight: 900;
letter-spacing: 0;
}
.headline .blue {
color: #007aff;
}
.headline .sub {
margin-top: 20px;
width: 410px;
color: #6e6e73;
font-size: 27px;
line-height: 1.18;
font-weight: 700;
}
.ambient-word {
position: absolute;
right: 78px;
bottom: 64px;
color: rgba(17, 17, 17, 0.05);
font-size: 208px;
line-height: 0.84;
font-weight: 900;
letter-spacing: 0;
pointer-events: none;
}
.ambient-line {
position: absolute;
height: 2px;
width: 360px;
border-radius: 999px;
background: rgba(0, 122, 255, 0.2);
transform-origin: center;
pointer-events: none;
}
.line-a {
left: 198px;
bottom: 230px;
}
.line-b {
right: 208px;
top: 196px;
width: 248px;
background: rgba(52, 199, 89, 0.2);
}
.snap-shadow {
position: absolute;
left: 50%;
bottom: 132px;
width: 560px;
height: 52px;
border-radius: 50%;
transform: translateX(-50%);
background: rgba(17, 17, 17, 0.14);
filter: blur(24px);
opacity: 0;
}
.phone {
position: relative;
width: 462px;
height: 952px;
border-radius: 72px;
padding: 16px;
background: linear-gradient(145deg, #1f2025 0%, #07070a 100%);
box-shadow:
0 46px 120px rgba(17, 17, 17, 0.22),
inset 0 0 0 2px rgba(255, 255, 255, 0.12);
z-index: 4;
will-change: transform;
}
.phone::before {
content: "";
position: absolute;
inset: 8px;
border-radius: 64px;
border: 2px solid rgba(255, 255, 255, 0.07);
pointer-events: none;
}
.screen {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 56px;
background: #f5f5f7;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.04);
}
.island {
position: absolute;
left: 50%;
top: 15px;
width: 138px;
height: 38px;
border-radius: 999px;
transform: translateX(-50%);
background: #07070a;
z-index: 30;
}
.store-view,
.detail-view,
.white-wipe {
position: absolute;
inset: 0;
}
.store-view {
padding: 70px 24px 26px;
z-index: 2;
}
.store-top {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 16px;
}
.store-title {
font-size: 38px;
line-height: 1;
font-weight: 900;
letter-spacing: 0;
}
.avatar {
width: 38px;
height: 38px;
border-radius: 50%;
background:
radial-gradient(circle at 62% 38%, rgba(255, 255, 255, 0.9) 0 10%, transparent 11%),
linear-gradient(135deg, #007aff, #34c759);
box-shadow: 0 8px 18px rgba(0, 122, 255, 0.25);
}
.search-pill {
display: flex;
align-items: center;
gap: 9px;
height: 45px;
padding: 0 16px;
border-radius: 15px;
background: #e9e9ed;
color: #6e6e73;
font-size: 18px;
font-weight: 700;
margin-bottom: 16px;
}
.search-icon {
width: 16px;
height: 16px;
border: 3px solid #8e8e93;
border-radius: 50%;
position: relative;
}
.search-icon::after {
content: "";
position: absolute;
right: -8px;
bottom: -5px;
width: 9px;
height: 3px;
border-radius: 999px;
background: #8e8e93;
transform: rotate(45deg);
}
.screen-window {
height: 720px;
overflow: hidden;
border-radius: 26px;
}
.scroll-content {
display: flex;
flex-direction: column;
gap: 12px;
will-change: transform;
}
.store-row {
display: grid;
grid-template-columns: 72px 1fr 78px;
align-items: center;
gap: 13px;
min-height: 104px;
padding: 13px 12px;
border-radius: 23px;
background: rgba(255, 255, 255, 0.88);
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.035);
}
.app-icon {
width: 72px;
height: 72px;
border-radius: 18px;
position: relative;
overflow: hidden;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.38);
}
.icon-cloud {
background: linear-gradient(135deg, #70d7ff, #007aff);
}
.icon-photo {
background:
radial-gradient(circle at 32% 35%, #ffffff 0 10%, transparent 11%),
linear-gradient(135deg, #ff9f0a, #ff375f);
}
.icon-music {
background: linear-gradient(135deg, #ff2d55, #5856d6);
}
.icon-bank {
background: linear-gradient(135deg, #111111, #8e8e93);
}
.icon-map {
background: linear-gradient(135deg, #34c759, #ffd60a);
}
.icon-goon {
background:
radial-gradient(circle at 72% 16%, rgba(255, 255, 255, 0.62) 0 12%, transparent 13%),
linear-gradient(145deg, #101828 0%, #007aff 64%, #34c759 100%);
}
.icon-goon::before {
content: "";
position: absolute;
left: 21px;
top: 16px;
width: 30px;
height: 38px;
border-radius: 16px 16px 18px 18px;
background: rgba(255, 255, 255, 0.94);
clip-path: polygon(50% 0, 88% 13%, 88% 46%, 50% 100%, 12% 46%, 12% 13%);
}
.icon-goon::after {
content: "";
position: absolute;
left: 31px;
top: 29px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #007aff;
}
.row-copy {
min-width: 0;
}
.row-title {
color: #111111;
font-size: 18px;
line-height: 1.24;
font-weight: 800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row-sub {
margin-top: 4px;
color: #6e6e73;
font-size: 12px;
line-height: 1.28;
font-weight: 600;
}
.stars {
margin-top: 6px;
color: #8e8e93;
font-size: 12px;
line-height: 1.25;
font-weight: 800;
}
.get-button,
.install-button,
.open-button {
display: grid;
place-items: center;
height: 34px;
border-radius: 999px;
font-size: 14px;
line-height: 1;
font-weight: 900;
letter-spacing: 0;
}
.get-button,
.open-button {
width: 72px;
color: #007aff;
background: #ececf1;
}
#goon-row {
position: relative;
transform-origin: center;
outline: 0 solid rgba(0, 122, 255, 0);
}
.tap-dot {
position: absolute;
right: 47px;
top: 52px;
width: 42px;
height: 42px;
border-radius: 50%;
border: 4px solid rgba(0, 122, 255, 0.8);
background: rgba(0, 122, 255, 0.18);
opacity: 0;
z-index: 9;
}
.detail-view {
padding: 78px 24px 26px;
background: #f5f5f7;
opacity: 0;
z-index: 8;
}
.detail-card {
height: 100%;
border-radius: 36px;
padding: 28px 22px 24px;
background: #ffffff;
box-shadow: 0 26px 70px rgba(17, 17, 17, 0.12);
overflow: hidden;
}
.detail-hero {
display: grid;
grid-template-columns: 108px 1fr;
gap: 16px;
align-items: center;
}
.detail-icon {
width: 108px;
height: 108px;
border-radius: 26px;
}
.detail-title {
font-size: 31px;
line-height: 1;
font-weight: 900;
}
.detail-sub {
margin-top: 7px;
color: #6e6e73;
font-size: 14px;
line-height: 1.2;
font-weight: 700;
}
.install-button {
position: relative;
width: 94px;
margin-top: 15px;
color: #ffffff;
background: #007aff;
overflow: hidden;
}
.install-label,
.open-label,
.ring {
position: absolute;
}
.install-label,
.open-label {
inset: 0;
display: grid;
place-items: center;
}
.open-label,
.ring {
opacity: 0;
}
.ring {
inset: 4px auto auto 34px;
width: 26px;
height: 26px;
transform: rotate(-90deg);
}
.ring .track {
fill: none;
stroke: rgba(255, 255, 255, 0.36);
stroke-width: 4;
}
.ring .progress {
fill: none;
stroke: #ffffff;
stroke-width: 4;
stroke-linecap: round;
stroke-dasharray: 69;
stroke-dashoffset: 69;
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin: 26px 0;
}
.stat {
min-height: 72px;
border-radius: 22px;
padding: 12px 8px;
background: #f5f5f7;
text-align: center;
}
.stat-label {
color: #8e8e93;
font-size: 11px;
font-weight: 800;
}
.stat-value {
margin-top: 7px;
color: #111111;
font-size: 22px;
line-height: 1;
font-weight: 900;
}
.progress-panel {
position: relative;
display: grid;
place-items: center;
height: 260px;
border-radius: 34px;
background:
radial-gradient(circle at 50% 40%, rgba(0, 122, 255, 0.12), transparent 48%), #f5f5f7;
overflow: hidden;
}
.progress-halo {
position: absolute;
width: 196px;
height: 196px;
border-radius: 50%;
border: 24px solid rgba(0, 122, 255, 0.1);
}
.progress-big {
width: 190px;
height: 190px;
transform: rotate(-90deg);
}
.big-track,
.big-progress {
fill: none;
stroke-width: 18;
}
.big-track {
stroke: rgba(0, 122, 255, 0.12);
}
.big-progress {
stroke: #007aff;
stroke-linecap: round;
stroke-dasharray: 440;
stroke-dashoffset: 440;
}
.percent {
position: absolute;
color: #111111;
font-size: 48px;
line-height: 1;
font-weight: 900;
}
.shield-status {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-top: 18px;
color: #34c759;
font-size: 20px;
font-weight: 900;
opacity: 0;
}
.check {
width: 30px;
height: 30px;
border-radius: 50%;
display: grid;
place-items: center;
color: #ffffff;
background: #34c759;
}
.speed-lines {
position: absolute;
inset: 0;
opacity: 0;
z-index: 20;
pointer-events: none;
}
.speed-lines i {
position: absolute;
left: 50%;
width: 4px;
height: 210px;
border-radius: 999px;
background: rgba(17, 17, 17, 0.18);
transform: translateX(-50%);
}
.speed-lines i:nth-child(1) {
top: 70px;
margin-left: -310px;
}
.speed-lines i:nth-child(2) {
top: 130px;
margin-left: 304px;
height: 240px;
}
.speed-lines i:nth-child(3) {
top: 260px;
margin-left: -232px;
height: 300px;
}
.speed-lines i:nth-child(4) {
top: 220px;
margin-left: 228px;
height: 320px;
}
.white-wipe {
background: #ffffff;
opacity: 0;
z-index: 40;
pointer-events: none;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="goonvpn-youtube-spot"
data-start="0"
data-duration="7"
data-width="1920"
data-height="1080"
>
<audio
id="sfx"
data-start="0"
data-duration="7"
data-track-index="1"
src="assets/goonvpn-sfx.wav"
data-volume="0.95"
></audio>
<div class="stage">
<div class="headline" id="stageHeadline">
<span>SECURE</span>
<span class="blue">INSTANTLY</span>
<div class="sub">One tap, installed, ready.</div>
</div>
<div class="ambient-word">VPN</div>
<div class="ambient-line line-a"></div>
<div class="ambient-line line-b"></div>
<div class="snap-shadow"></div>
<div class="phone">
<div class="screen">
<div class="island"></div>
<section class="store-view">
<div class="store-top">
<div class="store-title">Search</div>
<div class="avatar"></div>
</div>
<div class="search-pill">
<div class="search-icon"></div>
<div>vpn apps</div>
</div>
<div class="screen-window" data-layout-allow-overflow>
<div class="scroll-content">
<div class="store-row">
<div class="app-icon icon-cloud"></div>
<div class="row-copy">
<div class="row-title">CloudPocket</div>
<div class="row-sub">Files and backups</div>
<div class="stars">4.6 ★★★★★</div>
</div>
<div class="get-button">GET</div>
</div>
<div class="store-row">
<div class="app-icon icon-photo"></div>
<div class="row-copy">
<div class="row-title">LensLab</div>
<div class="row-sub">Photo editor</div>
<div class="stars">4.8 ★★★★★</div>
</div>
<div class="get-button">GET</div>
</div>
<div class="store-row">
<div class="app-icon icon-music"></div>
<div class="row-copy">
<div class="row-title">BeatShelf</div>
<div class="row-sub">Music discovery</div>
<div class="stars">4.4 ★★★★☆</div>
</div>
<div class="get-button">GET</div>
</div>
<div class="store-row">
<div class="app-icon icon-bank"></div>
<div class="row-copy">
<div class="row-title">VaultPay</div>
<div class="row-sub">Budget tracker</div>
<div class="stars">4.7 ★★★★★</div>
</div>
<div class="get-button">GET</div>
</div>
<div class="store-row">
<div class="app-icon icon-map"></div>
<div class="row-copy">
<div class="row-title">TrailPing</div>
<div class="row-sub">Maps for trips</div>
<div class="stars">4.5 ★★★★☆</div>
</div>
<div class="get-button">GET</div>
</div>
<div class="store-row" id="goon-row">
<div class="app-icon icon-goon"></div>
<div class="row-copy">
<div class="row-title">goonVPN</div>
<div class="row-sub">Fast private browsing</div>
<div class="stars">4.9 ★★★★★</div>
</div>
<div class="get-button">GET</div>
<div class="tap-dot"></div>
</div>
<div class="store-row">
<div class="app-icon icon-cloud"></div>
<div class="row-copy">
<div class="row-title">QuickTunnel</div>
<div class="row-sub">Network tools</div>
<div class="stars">4.2 ★★★★☆</div>
</div>
<div class="get-button">GET</div>
</div>
<div class="store-row">
<div class="app-icon icon-photo"></div>
<div class="row-copy">
<div class="row-title">PrivateFrame</div>
<div class="row-sub">Photo lockers</div>
<div class="stars">4.3 ★★★★☆</div>
</div>
<div class="get-button">GET</div>
</div>
</div>
</div>
</section>
<section class="detail-view">
<div class="detail-card">
<div class="detail-hero">
<div class="app-icon icon-goon detail-icon"></div>
<div>
<div class="detail-title">goonVPN</div>
<div class="detail-sub">Private, fast VPN for everyday browsing.</div>
<div class="install-button">
<span class="install-label">GET</span>
<svg class="ring" viewBox="0 0 28 28" aria-hidden="true">
<circle class="track" cx="14" cy="14" r="11"></circle>
<circle class="progress" cx="14" cy="14" r="11"></circle>
</svg>
<span class="open-label"></span>
</div>
</div>
</div>
<div class="stats">
<div class="stat">
<div class="stat-label">RATING</div>
<div class="stat-value">4.9</div>
</div>
<div class="stat">
<div class="stat-label">SPEED</div>
<div class="stat-value">Fast</div>
</div>
<div class="stat">
<div class="stat-label">AGE</div>
<div class="stat-value">4+</div>
</div>
</div>
<div class="progress-panel">
<div class="progress-halo"></div>
<svg class="progress-big" viewBox="0 0 160 160" aria-hidden="true">
<circle class="big-track" cx="80" cy="80" r="70"></circle>
<circle id="progressArc" class="big-progress" cx="80" cy="80" r="70"></circle>
</svg>
<div class="percent" id="progressText">0%</div>
</div>
<div class="shield-status">
<div class="check"></div>
<div>Installed</div>
</div>
</div>
</section>
</div>
</div>
</div>
<div class="speed-lines">
<i></i>
<i></i>
<i></i>
<i></i>
</div>
<div class="white-wipe"></div>
</div>
<script>
window.__timelines = window.__timelines || {};
gsap.defaults({ force3D: true });
const tl = gsap.timeline({
paused: true,
defaults: { duration: 0.45, ease: "power3.out" },
});
const breatheRepeats = Math.ceil(7 / 2.4) - 1;
tl.from(
".headline span",
{ y: 46, opacity: 0, stagger: 0.055, duration: 0.38, ease: "back.out(1.35)" },
0.05,
)
.from(".headline .sub", { y: 20, opacity: 0, duration: 0.28 }, 0.2)
.from(".ambient-word", { scale: 0.92, opacity: 0, duration: 0.5 }, 0.08)
.from(".ambient-line", { scaleX: 0, opacity: 0, stagger: 0.08, duration: 0.42 }, 0.16)
.to(
".ambient-line",
{
scaleX: 1.08,
opacity: 0.55,
duration: 2.4,
yoyo: true,
repeat: breatheRepeats,
ease: "sine.inOut",
},
0.55,
)
.from(
".phone",
{
y: 760,
scale: 0.82,
rotation: -2.5,
opacity: 0,
duration: 0.47,
ease: "back.out(1.6)",
},
0,
)
.from(".store-title", { y: 26, opacity: 0, duration: 0.28 }, 0.28)
.from(".avatar", { scale: 0.4, opacity: 0, duration: 0.24, ease: "back.out(1.8)" }, 0.34)
.from(".search-pill", { y: 18, opacity: 0, duration: 0.26 }, 0.4)
.from(
".store-row",
{ y: 44, opacity: 0, stagger: 0.035, duration: 0.36, ease: "power3.out" },
0.48,
);
tl.to(".scroll-content", { y: -456, duration: 1.2, ease: "power4.out" }, 0.62)
.to(
".screen-window",
{ scale: 1.012, duration: 0.18, yoyo: true, repeat: 1, ease: "power2.out" },
0.92,
)
.to(
".screen-window",
{ scale: 1.014, duration: 0.14, yoyo: true, repeat: 1, ease: "power2.out" },
1.25,
)
.to(
"#goon-row",
{
scale: 1.035,
outlineWidth: 3,
outlineColor: "rgba(0,122,255,0.34)",
duration: 0.2,
ease: "back.out(1.7)",
},
1.72,
)
.to("#goon-row", { scale: 1, duration: 0.22 }, 1.93)
.fromTo(
".tap-dot",
{ scale: 0.25, opacity: 0.95 },
{ scale: 2.15, opacity: 0, duration: 0.42, ease: "power2.out" },
2.02,
)
.to("#goon-row .get-button", { scale: 0.82, duration: 0.1, ease: "power3.in" }, 2.04)
.to("#goon-row .get-button", { scale: 1, duration: 0.14, ease: "back.out(2)" }, 2.14);
tl.fromTo(
".detail-view",
{ y: 620, scale: 0.92, opacity: 0 },
{ y: 0, scale: 1, opacity: 1, duration: 0.46, ease: "power4.out" },
2.32,
)
.from(".detail-card", { y: 56, scale: 0.96, duration: 0.42, ease: "back.out(1.2)" }, 2.34)
.from(
".detail-icon",
{ scale: 0.55, rotation: -8, opacity: 0, duration: 0.32, ease: "back.out(1.8)" },
2.47,
)
.from(".detail-title", { y: 20, opacity: 0, duration: 0.28 }, 2.54)
.from(".detail-sub", { y: 14, opacity: 0, duration: 0.24 }, 2.6)
.from(
".install-button",
{ scale: 0.72, opacity: 0, duration: 0.24, ease: "back.out(2)" },
2.66,
)
.from(".stat", { y: 28, opacity: 0, stagger: 0.05, duration: 0.3 }, 2.78)
.from(".progress-panel", { y: 42, scale: 0.96, opacity: 0, duration: 0.38 }, 2.92);
tl.to(".install-label", { y: -18, opacity: 0, duration: 0.16 }, 2.86)
.set(".install-label", { textContent: "" }, 3.02)
.to(".ring", { opacity: 1, duration: 0.16 }, 2.96)
.fromTo(
".ring .progress",
{ strokeDashoffset: 69 },
{ strokeDashoffset: 0, duration: 1.2, ease: "power2.inOut" },
3.04,
)
.fromTo(
"#progressArc",
{ strokeDashoffset: 440 },
{ strokeDashoffset: 0, duration: 1.42, ease: "power2.inOut" },
3.02,
)
.set("#progressText", { textContent: "18%" }, 3.18)
.to(".progress-panel", { scale: 1.025, duration: 0.12, yoyo: true, repeat: 1 }, 3.18)
.set("#progressText", { textContent: "52%" }, 3.56)
.to(".progress-panel", { scale: 1.03, duration: 0.12, yoyo: true, repeat: 1 }, 3.56)
.set("#progressText", { textContent: "87%" }, 3.9)
.to(".progress-panel", { scale: 1.035, duration: 0.12, yoyo: true, repeat: 1 }, 3.9)
.set("#progressText", { textContent: "100%" }, 4.28)
.to(
".progress-panel",
{ scale: 1.04, duration: 0.12, yoyo: true, repeat: 1, ease: "back.out(1.7)" },
4.28,
)
.to(".ring", { scale: 0.35, opacity: 0, duration: 0.16 }, 4.44)
.to(".install-button", { backgroundColor: "#34c759", width: 94, duration: 0.18 }, 4.5)
.set(".open-label", { textContent: "OPEN" }, 4.52)
.to(".open-label", { opacity: 1, duration: 0.16 }, 4.56)
.fromTo(
".shield-status",
{ y: 22, scale: 0.9, opacity: 0 },
{ y: 0, scale: 1, opacity: 1, duration: 0.34, ease: "back.out(1.8)" },
4.62,
)
.to(
".phone",
{ scale: 1.025, duration: 0.18, yoyo: true, repeat: 1, ease: "back.out(1.5)" },
4.7,
);
tl.to(".headline", { y: -18, opacity: 0.35, duration: 0.22 }, 5.18)
.to(".ambient-word", { opacity: 0.018, duration: 0.22 }, 5.18)
.to(".snap-shadow", { opacity: 1, scaleX: 0.8, duration: 0.16 }, 5.35)
.fromTo(
".speed-lines",
{ y: -220, opacity: 0 },
{ y: 420, opacity: 1, duration: 0.26, ease: "power4.in" },
5.42,
)
.to(
".phone",
{ y: 1050, scale: 0.78, rotation: -3.5, duration: 0.48, ease: "power4.in" },
5.44,
)
.to(".snap-shadow", { opacity: 0, scaleX: 1.6, duration: 0.28 }, 5.62)
.to(".speed-lines", { opacity: 0, duration: 0.2 }, 5.74)
.to(".white-wipe", { opacity: 1, duration: 0.42, ease: "power2.out" }, 5.86);
window.__timelines["goonvpn-youtube-spot"] = tl;
</script>
</body>
</html>
@@ -0,0 +1,28 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "goonvpn-youtube-spot",
"type": "hyperframes:block",
"title": "GoonVPN YouTube Spot",
"description": "Snappy Apple-style YouTube insert showing a phone finding and installing a fictional VPN app with sound effects.",
"tags": ["app", "showcase", "youtube", "sfx"],
"author": "Stronkter",
"authorUrl": "https://x.com/Stronkter",
"sourcePrompt": "\ud83d\udcf7HyperFrames by HeyGen make me a 7s video apple style bold font and styling a phone scrolling in a appstore clicking on a vpn app called goonVPN and installing it the app installs the phone snaps down and fades to a white backround make it snappy and pop its for a youtube video also add soundfx and make it 60fps and 1920x1080",
"dimensions": {
"width": 1920,
"height": 1080
},
"duration": 7,
"files": [
{
"path": "goonvpn-youtube-spot.html",
"target": "compositions/goonvpn-youtube-spot.html",
"type": "hyperframes:composition"
},
{
"path": "assets/goonvpn-sfx.wav",
"target": "assets/goonvpn-sfx.wav",
"type": "hyperframes:asset"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

@@ -0,0 +1,305 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #eef3f4;
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #eef3f4;
color: #151515;
}
.camera {
position: absolute;
inset: 0;
z-index: 0;
transform-origin: 50% 51%;
will-change: transform, filter;
}
.map {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.paper-depth {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 52% 50%, rgba(255, 255, 255, 0.08), transparent 28%),
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(10, 20, 24, 0.12));
mix-blend-mode: multiply;
pointer-events: none;
}
.annotation {
position: absolute;
left: 652px;
top: 235px;
z-index: 4;
width: 560px;
height: 392px;
overflow: visible;
transform-origin: center;
will-change: transform, opacity;
}
.scribble {
position: absolute;
inset: 0;
width: 560px;
height: 392px;
overflow: visible;
}
.scribble path {
fill: none;
stroke: #e21d2f;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 8px 12px rgba(226, 29, 47, 0.24));
}
#circle-a {
stroke-width: 12;
stroke-dasharray: 980;
stroke-dashoffset: 980;
}
#circle-b {
stroke-width: 6;
opacity: 0.82;
stroke-dasharray: 910;
stroke-dashoffset: 910;
}
.label {
position: absolute;
left: 50%;
top: -146px;
transform: translateX(-50%) scale(0.82);
transform-origin: 50% 100%;
min-width: 370px;
padding: 18px 26px 20px;
border-radius: 18px;
color: #ffffff;
background: #111111;
box-shadow:
0 22px 54px rgba(25, 4, 7, 0.24),
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
font-size: 60px;
line-height: 0.9;
font-weight: 900;
letter-spacing: 0;
text-align: center;
text-transform: uppercase;
opacity: 0;
}
.label::after {
content: "";
position: absolute;
left: 50%;
bottom: -16px;
width: 32px;
height: 32px;
background: #111111;
transform: translateX(-50%) rotate(45deg);
}
.red-wash {
position: absolute;
inset: 0;
z-index: 2;
background:
radial-gradient(
circle at 50% 48%,
rgba(255, 59, 48, 0.3),
rgba(255, 59, 48, 0.06) 42%,
rgba(79, 0, 8, 0.22) 100%
),
linear-gradient(90deg, rgba(99, 0, 12, 0.12), rgba(255, 59, 48, 0.2));
opacity: 0;
mix-blend-mode: multiply;
pointer-events: none;
}
.scanline {
position: absolute;
inset: 0;
z-index: 3;
opacity: 0;
background: repeating-linear-gradient(
0deg,
rgba(65, 0, 0, 0.12) 0,
rgba(65, 0, 0, 0.12) 1px,
transparent 1px,
transparent 7px
);
mix-blend-mode: multiply;
pointer-events: none;
}
.corner {
position: absolute;
z-index: 6;
width: 84px;
height: 84px;
border-color: rgba(17, 17, 17, 0.62);
opacity: 0;
}
.corner.tl {
left: 86px;
top: 78px;
border-top: 5px solid;
border-left: 5px solid;
}
.corner.tr {
right: 86px;
top: 78px;
border-top: 5px solid;
border-right: 5px solid;
}
.corner.bl {
left: 86px;
bottom: 78px;
border-bottom: 5px solid;
border-left: 5px solid;
}
.corner.br {
right: 86px;
bottom: 78px;
border-bottom: 5px solid;
border-right: 5px solid;
}
.attribution {
position: absolute;
right: 28px;
bottom: 22px;
z-index: 7;
color: rgba(17, 17, 17, 0.44);
font-size: 19px;
font-weight: 650;
opacity: 0.7;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="north-korea-locked-down"
data-root="true"
data-start="0"
data-duration="7"
data-width="1920"
data-height="1080"
>
<div id="camera" class="camera">
<img class="map" src="assets/korea-map.png" alt="" />
<div class="paper-depth"></div>
</div>
<div id="annotation" class="annotation">
<div id="label" class="label">LOCKED<br />DOWN</div>
<svg class="scribble" viewBox="0 0 560 392" aria-hidden="true" data-layout-ignore>
<path
id="circle-a"
d="M 74 211 C 42 119 118 42 254 36 C 410 30 524 111 508 222 C 494 320 350 377 208 346 C 94 322 37 278 74 211"
/>
<path
id="circle-b"
d="M 92 231 C 33 150 105 55 243 44 C 408 31 529 126 501 244 C 480 334 335 370 197 342 C 83 319 47 269 92 231"
/>
</svg>
</div>
<div id="redWash" class="red-wash"></div>
<div id="scanline" class="scanline"></div>
<div class="corner tl"></div>
<div class="corner tr"></div>
<div class="corner bl"></div>
<div class="corner br"></div>
<div class="attribution">© OpenStreetMap contributors © CARTO</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({
paused: true,
defaults: { overwrite: "auto" },
});
tl.fromTo(
"#camera",
{ scale: 0.92, x: -70, y: 18, filter: "saturate(0.92) contrast(0.98)" },
{
scale: 2.12,
x: -56,
y: -22,
filter: "saturate(1.08) contrast(1.04)",
duration: 3.18,
ease: "expo.inOut",
},
0,
);
tl.to("#camera", { scale: 2.2, x: -44, y: -34, duration: 0.42, ease: "back.out(1.5)" }, 3.18);
tl.from(
".corner",
{ opacity: 0, scale: 0.72, duration: 0.28, stagger: 0.035, ease: "power3.out" },
0.42,
);
tl.to(".corner", { opacity: 0.72, duration: 0.18, ease: "power2.out" }, 1.2);
tl.fromTo(
"#annotation",
{ opacity: 0, scale: 0.92, x: 4, y: -4 },
{ opacity: 1, scale: 1, x: 0, y: 0, duration: 0.16, ease: "power2.out" },
3.12,
);
tl.to("#circle-a", { strokeDashoffset: 0, duration: 0.62, ease: "power2.out" }, 3.24);
tl.to("#circle-b", { strokeDashoffset: 0, duration: 0.44, ease: "power3.out" }, 3.46);
tl.to("#label", { opacity: 1, scale: 1, duration: 0.28, ease: "back.out(2.1)" }, 3.78);
tl.to("#redWash", { opacity: 1, duration: 0.24, ease: "power2.out" }, 3.82);
tl.to("#scanline", { opacity: 0.44, duration: 0.18, ease: "none" }, 3.86);
tl.to(
"#annotation",
{ scale: 1.045, duration: 0.14, yoyo: true, repeat: 1, ease: "sine.inOut" },
4.1,
);
tl.to("#camera", { scale: 2.06, x: -82, y: -8, duration: 2.35, ease: "sine.inOut" }, 4.2);
tl.to("#redWash", { opacity: 0.82, duration: 2.35, ease: "sine.inOut" }, 4.2);
tl.to(".corner", { opacity: 0.38, duration: 0.72, ease: "sine.inOut" }, 5.82);
tl.to("#label", { y: -6, duration: 0.4, yoyo: true, repeat: 1, ease: "sine.inOut" }, 5.94);
window.__timelines["north-korea-locked-down"] = tl;
</script>
</body>
</html>
@@ -0,0 +1,28 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "north-korea-locked-down",
"type": "hyperframes:block",
"title": "North Korea Locked Down",
"description": "Realistic map zoom into North Korea with a red scribble circle, locked-down pop-up label, and reddish editorial wash.",
"tags": ["showcase", "map", "annotation", "youtube", "kinetic"],
"author": "Stronkter",
"authorUrl": "https://x.com/Stronkter",
"sourcePrompt": "use \ud83d\udcf7HyperFrames by HeyGen and Image Gen if you need it for assets or like png images of assets without backround to make a youtube style camera moving in out and other things that are in youtube videos, to make a video of a map zooms in on north korea and a scribble style circle circles the country and a text pops up above it saying locked down when the text apears the video turns a bit redish make the video 7 seconds long id like the map to look realistic and accurate to real lfe",
"dimensions": {
"width": 1920,
"height": 1080
},
"duration": 7,
"files": [
{
"path": "north-korea-locked-down.html",
"target": "compositions/north-korea-locked-down.html",
"type": "hyperframes:composition"
},
{
"path": "assets/korea-map.png",
"target": "assets/korea-map.png",
"type": "hyperframes:asset"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

Binary file not shown.
+525
View File
@@ -0,0 +1,525 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
width: 1920px;
height: 1080px;
overflow: hidden;
background: #f5f5f7;
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
}
#root {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #f5f5f7;
color: #1d1d1f;
}
.stage {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #f5f5f7;
}
.map-plane {
position: absolute;
inset: 0;
transform-origin: 52% 48%;
will-change: transform, opacity;
}
.map-image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.soft-ocean {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 50% 24%, rgba(0, 113, 227, 0.18), rgba(0, 113, 227, 0) 34%),
linear-gradient(180deg, rgba(245, 245, 247, 0.05), rgba(245, 245, 247, 0.3));
mix-blend-mode: multiply;
opacity: 0.72;
}
.route-svg,
.doodle-svg {
position: absolute;
inset: 0;
width: 1920px;
height: 1080px;
overflow: visible;
}
.route-glow {
fill: none;
stroke: rgba(0, 113, 227, 0.23);
stroke-width: 24;
stroke-linecap: round;
stroke-dasharray: 2000;
stroke-dashoffset: 2000;
filter: blur(4px);
}
.route-path {
fill: none;
stroke: #0071e3;
stroke-width: 9;
stroke-linecap: round;
stroke-dasharray: 2000;
stroke-dashoffset: 2000;
filter: drop-shadow(0 10px 20px rgba(0, 113, 227, 0.22));
}
.route-thread {
fill: none;
stroke: rgba(255, 255, 255, 0.86);
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 2000;
stroke-dashoffset: 2000;
}
.route-tick {
position: absolute;
width: 14px;
height: 14px;
margin: -7px 0 0 -7px;
border-radius: 999px;
background: #ffffff;
border: 3px solid #0071e3;
box-shadow: 0 8px 26px rgba(0, 113, 227, 0.32);
opacity: 0;
}
.pin {
position: absolute;
width: 26px;
height: 26px;
border-radius: 999px;
background: #ffffff;
border: 7px solid #0071e3;
box-shadow:
0 15px 34px rgba(0, 0, 0, 0.18),
0 0 0 10px rgba(0, 113, 227, 0.12);
transform: translate(-50%, -50%);
transform-origin: center;
will-change: transform, opacity;
}
.pin::after {
content: "";
position: absolute;
inset: -20px;
border-radius: 999px;
border: 2px solid rgba(0, 113, 227, 0.25);
}
.pin-label {
position: absolute;
top: 32px;
min-width: 142px;
padding: 12px 16px 13px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(255, 255, 255, 0.88);
color: #1d1d1f;
font-size: 22px;
font-weight: 800;
line-height: 1;
letter-spacing: 0;
box-shadow: 0 18px 38px rgba(29, 29, 31, 0.16);
backdrop-filter: blur(18px);
white-space: nowrap;
}
.pin-label span {
display: block;
margin-top: 7px;
color: #6e6e73;
font-size: 13px;
font-weight: 700;
}
#nyc-pin {
left: 308.41px;
top: 584.16px;
}
#nyc-pin .pin-label {
left: 28px;
}
#paris-pin {
left: 1611.59px;
top: 387.84px;
}
#paris-pin .pin-label {
right: 28px;
}
.plane {
position: absolute;
left: 0;
top: 0;
width: 112px;
height: 112px;
offset-path: path("M 308.41 584.158 C 650 520 1120 245 1611.59 387.842");
offset-distance: 0%;
offset-rotate: auto;
offset-anchor: 50% 50%;
transform-origin: center;
filter: drop-shadow(0 22px 22px rgba(0, 0, 0, 0.24));
opacity: 1;
will-change: offset-distance, transform, opacity;
}
.plane-core {
position: absolute;
inset: 0;
transform-origin: center;
will-change: transform;
}
.plane svg {
width: 100%;
height: 100%;
overflow: visible;
}
.doodle-line {
fill: none;
stroke: #ff3b30;
stroke-width: 13;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 900;
stroke-dashoffset: 900;
filter: drop-shadow(0 10px 14px rgba(255, 59, 48, 0.18));
}
.doodle-line.secondary {
stroke-width: 6;
opacity: 0.82;
}
.paris-pulse {
position: absolute;
left: 1611.59px;
top: 387.84px;
width: 126px;
height: 126px;
margin: -63px 0 0 -63px;
border-radius: 999px;
border: 4px solid rgba(255, 59, 48, 0.34);
opacity: 0;
}
.landing-badge {
position: absolute;
left: 1500px;
top: 502px;
padding: 15px 20px 16px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.9);
color: #d70015;
font-size: 24px;
font-weight: 900;
line-height: 1;
letter-spacing: 0;
box-shadow: 0 20px 44px rgba(29, 29, 31, 0.14);
opacity: 0;
transform: scale(0.78);
transform-origin: center;
}
.title-stack {
position: absolute;
left: 82px;
top: 70px;
z-index: 6;
max-width: 780px;
padding: 30px 34px 33px;
border-radius: 30px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(255, 255, 255, 0.86);
box-shadow: 0 28px 70px rgba(29, 29, 31, 0.12);
backdrop-filter: blur(22px);
}
.eyebrow {
color: #6e6e73;
font-size: 19px;
font-weight: 800;
letter-spacing: 0;
}
.headline {
margin-top: 11px;
color: #1d1d1f;
font-size: 84px;
font-weight: 900;
line-height: 0.92;
letter-spacing: 0;
}
.headline span {
color: #0071e3;
}
.route-meta {
display: flex;
align-items: center;
gap: 14px;
margin-top: 18px;
color: #6e6e73;
font-size: 25px;
font-weight: 800;
}
.route-meta i {
display: block;
width: 54px;
height: 6px;
border-radius: 999px;
background: #0071e3;
}
.attribution {
position: absolute;
right: 28px;
bottom: 24px;
z-index: 5;
padding: 8px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.68);
color: rgba(29, 29, 31, 0.76);
font-size: 14px;
font-weight: 700;
line-height: 1;
}
.white-screen {
position: absolute;
inset: 0;
z-index: 30;
background: #ffffff;
opacity: 0;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="nyc-paris-flight"
data-start="0"
data-duration="6"
data-width="1920"
data-height="1080"
>
<div
id="flight-scene"
class="clip stage"
data-start="0"
data-duration="6"
data-track-index="0"
>
<div id="map-plane" class="map-plane" data-layout-allow-overflow>
<img class="map-image" src="assets/map-nyc-paris.png" alt="" />
<div class="soft-ocean" data-layout-ignore></div>
<svg class="route-svg" viewBox="0 0 1920 1080" aria-hidden="true" data-layout-ignore>
<path
id="route-glow"
class="route-glow"
d="M 308.41 584.158 C 650 520 1120 245 1611.59 387.842"
/>
<path
id="route-path"
class="route-path"
d="M 308.41 584.158 C 650 520 1120 245 1611.59 387.842"
/>
<path
id="route-thread"
class="route-thread"
d="M 308.41 584.158 C 650 520 1120 245 1611.59 387.842"
/>
</svg>
<div class="route-tick" style="left: 481.8px; top: 539.7px"></div>
<div class="route-tick" style="left: 647.6px; top: 486.5px"></div>
<div class="route-tick" style="left: 824.8px; top: 430.6px"></div>
<div class="route-tick" style="left: 1011.5px; top: 382.4px"></div>
<div class="route-tick" style="left: 1206.1px; top: 352.3px"></div>
<div class="route-tick" style="left: 1406.7px; top: 350.7px"></div>
<div id="nyc-pin" class="pin">
<div class="pin-label">New York<span>JFK / NYC</span></div>
</div>
<div id="paris-pin" class="pin">
<div class="pin-label">Paris<span>CDG / FR</span></div>
</div>
<div id="plane" class="plane" aria-hidden="true">
<div id="plane-core" class="plane-core">
<svg viewBox="-64 -64 128 128">
<path
d="M 58 0 C 52 -10 37 -15 15 -16 L -18 -56 C -23 -62 -34 -58 -32 -49 L -21 -15 L -48 -12 C -56 -11 -60 -6 -60 0 C -60 6 -56 11 -48 12 L -21 15 L -32 49 C -34 58 -23 62 -18 56 L 15 16 C 37 15 52 10 58 0 Z"
fill="#ffffff"
/>
<path
d="M 54 0 C 46 -6 33 -9 14 -10 L -19 -50 C -21 -53 -25 -52 -25 -47 L -15 -10 L -49 -6 C -53 -5 -55 -2 -55 0 C -55 2 -53 5 -49 6 L -15 10 L -25 47 C -25 52 -21 53 -19 50 L 14 10 C 33 9 46 6 54 0 Z"
fill="#0071e3"
/>
<circle cx="18" cy="0" r="6" fill="#f5f5f7" opacity="0.95" />
</svg>
</div>
</div>
<svg class="doodle-svg" viewBox="0 0 1920 1080" aria-hidden="true" data-layout-ignore>
<path
id="doodle-a"
class="doodle-line"
d="M 1510 382 C 1505 323 1560 287 1627 300 C 1700 315 1740 358 1720 416 C 1705 468 1624 486 1556 462 C 1518 449 1494 417 1510 382"
/>
<path
id="doodle-b"
class="doodle-line secondary"
d="M 1518 396 C 1488 356 1528 307 1605 296 C 1688 284 1748 338 1730 397 C 1713 453 1641 480 1570 463 C 1526 453 1504 427 1518 396"
/>
</svg>
<div id="paris-pulse" class="paris-pulse"></div>
<div id="landing-badge" class="landing-badge">ARRIVED</div>
</div>
<div class="title-stack">
<div class="eyebrow">Six-second transatlantic hop</div>
<div class="headline">NEW YORK <span>TO</span> PARIS</div>
<div class="route-meta">
<i></i>
<p>5,837 km compressed</p>
</div>
</div>
<div class="attribution">© OpenStreetMap contributors © CARTO</div>
<div id="white-screen" class="white-screen"></div>
</div>
<audio
id="sfx"
data-start="0"
data-duration="6"
data-track-index="1"
src="assets/sfx-mix.wav"
data-volume="0.95"
></audio>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({
paused: true,
defaults: { overwrite: "auto" },
});
tl.from(
"#map-plane",
{ scale: 1.1, x: -38, y: 20, opacity: 0.94, duration: 1.05, ease: "power2.out" },
0.16,
);
tl.from(
".title-stack",
{ y: 28, scale: 0.97, opacity: 0, duration: 0.52, ease: "expo.out" },
0.24,
);
tl.from(".eyebrow", { x: -22, opacity: 0, duration: 0.36, ease: "power3.out" }, 0.32);
tl.from(".headline", { y: 34, opacity: 0, duration: 0.5, ease: "back.out(1.45)" }, 0.4);
tl.from(".route-meta", { x: -26, opacity: 0, duration: 0.38, ease: "circ.out" }, 0.58);
tl.from("#nyc-pin", { scale: 0.12, opacity: 0, duration: 0.34, ease: "back.out(2.8)" }, 0.7);
tl.from(
"#paris-pin",
{ scale: 0.12, opacity: 0, duration: 0.34, ease: "elastic.out(1, 0.5)" },
0.86,
);
tl.from("#plane", { scale: 0.48, opacity: 0, duration: 0.3, ease: "back.out(2.2)" }, 1.08);
tl.to("#route-glow", { strokeDashoffset: 0, duration: 3.1, ease: "power1.inOut" }, 1.16);
tl.to("#route-path", { strokeDashoffset: 0, duration: 3.05, ease: "power2.inOut" }, 1.2);
tl.to("#route-thread", { strokeDashoffset: 0, duration: 2.92, ease: "sine.inOut" }, 1.26);
tl.to("#plane", { offsetDistance: "100%", duration: 3.25, ease: "sine.inOut" }, 1.17);
tl.to(
"#plane-core",
{ scale: 1.06, duration: 0.2, repeat: 12, yoyo: true, ease: "sine.inOut" },
1.22,
);
tl.to("#map-plane", { scale: 1.025, duration: 3.2, ease: "sine.inOut" }, 1.28);
gsap.utils.toArray(".route-tick").forEach((tick, index) => {
tl.fromTo(
tick,
{ scale: 0.2, opacity: 0 },
{ scale: 1, opacity: 1, duration: 0.16, ease: "back.out(3)" },
1.34 + index * 0.44,
);
tl.to(
tick,
{ scale: 0.6, opacity: 0.18, duration: 0.34, ease: "power2.out" },
1.48 + index * 0.44,
);
});
tl.from(
"#paris-pulse",
{ scale: 0.52, opacity: 0, duration: 0.26, ease: "back.out(2.4)" },
4.06,
);
tl.to("#doodle-a", { strokeDashoffset: 0, duration: 0.54, ease: "power2.out" }, 4.18);
tl.to("#doodle-b", { strokeDashoffset: 0, duration: 0.36, ease: "power3.out" }, 4.36);
tl.to("#paris-pulse", { scale: 1.5, opacity: 0, duration: 0.58, ease: "power3.out" }, 4.64);
tl.to("#plane-core", { scale: 1.23, duration: 0.16, ease: "back.out(3)" }, 4.96);
tl.to("#plane-core", { scale: 0.9, duration: 0.12, ease: "power2.in" }, 5.12);
tl.to("#plane-core", { scale: 1, duration: 0.22, ease: "elastic.out(1, 0.45)" }, 5.24);
tl.to(
"#landing-badge",
{ scale: 1, opacity: 1, duration: 0.28, ease: "back.out(2.4)" },
5.02,
);
tl.to(
"#landing-badge",
{ scale: 1.06, duration: 0.16, yoyo: true, repeat: 1, ease: "sine.inOut" },
5.3,
);
tl.set("#white-screen", { opacity: 1 }, 5.5);
window.__timelines["nyc-paris-flight"] = tl;
</script>
</body>
</html>
@@ -0,0 +1,33 @@
{
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
"name": "nyc-paris-flight",
"type": "hyperframes:block",
"title": "NYC Paris Flight",
"description": "Apple-style realistic map animation with a plane flying from New York to Paris, marker circle, landing pop, and sound effects.",
"tags": ["showcase", "travel", "map", "youtube", "sfx"],
"author": "Stronkter",
"authorUrl": "https://x.com/Stronkter",
"sourcePrompt": "\ud83d\udcf7HyperFrames by HeyGen Make a six-second Apple-style font bold video of a plane going from New York to Paris. A map animation, and then it shows the plane going from New York to Paris. Make the fonts Apple-style bold and make the map actual realistic, actually realistic. Before it lands in Paris, do a doodle circle in red around Paris, and then it lands in the doodle circle, and the video basically cuts to a white screen at the end. Also add sound effects for everything as well. Every nice motion, every the plane for pop-ups, bubble, pop-up effect, everything you want.",
"dimensions": {
"width": 1920,
"height": 1080
},
"duration": 6,
"files": [
{
"path": "nyc-paris-flight.html",
"target": "compositions/nyc-paris-flight.html",
"type": "hyperframes:composition"
},
{
"path": "assets/map-nyc-paris.png",
"target": "assets/map-nyc-paris.png",
"type": "hyperframes:asset"
},
{
"path": "assets/sfx-mix.wav",
"target": "assets/sfx-mix.wav",
"type": "hyperframes:asset"
}
]
}
+16
View File
@@ -91,6 +91,22 @@
"name": "app-showcase", "name": "app-showcase",
"type": "hyperframes:block" "type": "hyperframes:block"
}, },
{
"name": "north-korea-locked-down",
"type": "hyperframes:block"
},
{
"name": "apple-money-count",
"type": "hyperframes:block"
},
{
"name": "goonvpn-youtube-spot",
"type": "hyperframes:block"
},
{
"name": "nyc-paris-flight",
"type": "hyperframes:block"
},
{ {
"name": "ui-3d-reveal", "name": "ui-3d-reveal",
"type": "hyperframes:block" "type": "hyperframes:block"
+43 -21
View File
@@ -13,7 +13,7 @@
* npx tsx scripts/generate-catalog-pages.ts * npx tsx scripts/generate-catalog-pages.ts
*/ */
import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
import { join, resolve, dirname } from "node:path"; import { join, resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
// Import from source — bun workspace linking doesn't resolve for scripts outside packages/. // Import from source — bun workspace linking doesn't resolve for scripts outside packages/.
@@ -27,11 +27,17 @@ const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, ".."); const repoRoot = resolve(scriptDir, "..");
const registryDir = resolve(repoRoot, "registry"); const registryDir = resolve(repoRoot, "registry");
const docsDir = resolve(repoRoot, "docs"); const docsDir = resolve(repoRoot, "docs");
const catalogImageBase = "https://static.heygen.ai/hyperframes-oss/docs/images/catalog";
// ── Types ────────────────────────────────────────────────────────────────── // ── Types ──────────────────────────────────────────────────────────────────
type ItemKind = "block" | "component"; type ItemKind = "block" | "component";
interface SourceMetadata {
authorUrl?: string;
sourcePrompt?: string;
}
interface CatalogEntry { interface CatalogEntry {
name: string; name: string;
type: ItemKind; type: ItemKind;
@@ -48,28 +54,34 @@ interface CatalogEntry {
function discoverItems(): { kind: ItemKind; manifest: RegistryItem }[] { function discoverItems(): { kind: ItemKind; manifest: RegistryItem }[] {
const items: { kind: ItemKind; manifest: RegistryItem }[] = []; const items: { kind: ItemKind; manifest: RegistryItem }[] = [];
const registryManifest = JSON.parse(
readFileSync(join(registryDir, "registry.json"), "utf-8"),
) as { items?: { name: string; type: string }[] };
const dirs: { kind: ItemKind; dir: string }[] = [ for (const item of registryManifest.items ?? []) {
{ kind: "block", dir: join(registryDir, "blocks") }, const kind =
{ kind: "component", dir: join(registryDir, "components") }, item.type === "hyperframes:block"
]; ? "block"
: item.type === "hyperframes:component"
? "component"
: null;
for (const { kind, dir } of dirs) { if (!kind) continue;
if (!existsSync(dir)) continue;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const manifestPath = join(dir, entry.name, "registry-item.json");
if (!existsSync(manifestPath)) continue;
let manifest: RegistryItem; const manifestPath = join(registryDir, typeDir(kind), item.name, "registry-item.json");
try { if (!existsSync(manifestPath)) {
manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as RegistryItem; console.warn(` ⚠ Skipping ${item.name}: missing ${manifestPath}`);
} catch (err) { continue;
console.warn(` ⚠ Skipping ${manifestPath}: ${(err as Error).message}`);
continue;
}
items.push({ kind, manifest });
} }
let manifest: RegistryItem;
try {
manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as RegistryItem;
} catch (err) {
console.warn(` ⚠ Skipping ${manifestPath}: ${(err as Error).message}`);
continue;
}
items.push({ kind, manifest });
} }
return items.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name)); return items.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
@@ -89,6 +101,7 @@ function generateItemMdx(kind: ItemKind, manifest: RegistryItem): string {
const tags = manifest.tags ?? []; const tags = manifest.tags ?? [];
const tagBadges = tags.map((t) => `\`${t}\``).join(" "); const tagBadges = tags.map((t) => `\`${t}\``).join(" ");
const installCmd = `npx hyperframes add ${manifest.name}`; const installCmd = `npx hyperframes add ${manifest.name}`;
const source = manifest as RegistryItem & SourceMetadata;
const lines: string[] = [ const lines: string[] = [
"---", "---",
@@ -106,8 +119,17 @@ function generateItemMdx(kind: ItemKind, manifest: RegistryItem): string {
lines.push(tagBadges, ""); lines.push(tagBadges, "");
} }
if (manifest.author) {
const author = source.authorUrl ? `[${manifest.author}](${source.authorUrl})` : manifest.author;
lines.push(`Created by ${author}.`, "");
}
if (source.sourcePrompt) {
lines.push("## Source Prompt", "", "```text", source.sourcePrompt, "```", "");
}
// Preview video with poster — muted loop, no autoPlay (matches examples page). // Preview video with poster — muted loop, no autoPlay (matches examples page).
const previewPath = `/images/catalog/${typeDir(kind)}/${manifest.name}`; const previewPath = `${catalogImageBase}/${typeDir(kind)}/${manifest.name}`;
lines.push( lines.push(
`<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="${previewPath}.mp4" poster="${previewPath}.png" autoPlay muted loop playsInline />`, `<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="${previewPath}.mp4" poster="${previewPath}.png" autoPlay muted loop playsInline />`,
"", "",
@@ -227,7 +249,7 @@ function main(): void {
description: manifest.description, description: manifest.description,
tags: manifest.tags ?? [], tags: manifest.tags ?? [],
href: `/catalog/${dir}/${manifest.name}`, href: `/catalog/${dir}/${manifest.name}`,
preview: `/images/catalog/${dir}/${manifest.name}.png`, preview: `${catalogImageBase}/${dir}/${manifest.name}.png`,
}); });
} }