diff --git a/docs/public/catalog-index.json b/docs/public/catalog-index.json index 764193bcf..fa4d31679 100644 --- a/docs/public/catalog-index.json +++ b/docs/public/catalog-index.json @@ -544,6 +544,22 @@ "href": "/catalog/components/shimmer-sweep", "preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/shimmer-sweep.png" }, + { + "name": "spain-map", + "type": "block", + "title": "Spain Map", + "description": "Animated Spain choropleth by autonomous community with staggered reveals and gradient legend — D3 conic conformal projection", + "tags": [ + "data", + "map", + "geography", + "spain", + "europe", + "choropleth" + ], + "href": "/catalog/blocks/spain-map", + "preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/spain-map.png" + }, { "name": "spotify-card", "type": "block", @@ -777,6 +793,70 @@ "href": "/catalog/blocks/ui-3d-reveal", "preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/ui-3d-reveal.png" }, + { + "name": "us-map", + "type": "block", + "title": "US Map", + "description": "Animated US choropleth map with staggered state reveals, value labels, and gradient legend — pure inline SVG with GSAP", + "tags": [ + "data", + "map", + "geography", + "usa", + "choropleth" + ], + "href": "/catalog/blocks/us-map", + "preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/us-map.png" + }, + { + "name": "us-map-bubble", + "type": "block", + "title": "US Bubble Map", + "description": "Animated US bubble map with proportional city markers, value callouts, and connection lines — composable with us-map", + "tags": [ + "data", + "map", + "geography", + "usa", + "bubble", + "cities" + ], + "href": "/catalog/blocks/us-map-bubble", + "preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/us-map-bubble.png" + }, + { + "name": "us-map-flow", + "type": "block", + "title": "US Flow Map", + "description": "Animated connection arcs between US cities over a base map — composable origin-destination flow visualization", + "tags": [ + "data", + "map", + "geography", + "usa", + "flow", + "connections", + "arcs" + ], + "href": "/catalog/blocks/us-map-flow", + "preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/us-map-flow.png" + }, + { + "name": "us-map-hex", + "type": "block", + "title": "US Hex Grid Map", + "description": "Animated hexagonal tile grid map — each state as an equal-weight hex with data fill and abbreviation label", + "tags": [ + "data", + "map", + "geography", + "usa", + "hexgrid", + "tilegrid" + ], + "href": "/catalog/blocks/us-map-hex", + "preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/us-map-hex.png" + }, { "name": "vfx-iphone-device", "type": "block", @@ -911,6 +991,21 @@ "href": "/catalog/blocks/whip-pan", "preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/whip-pan.png" }, + { + "name": "world-map", + "type": "block", + "title": "World Map", + "description": "Animated world choropleth with country-by-country reveal, tooltip labels, and rotating globe inset — D3 Natural Earth projection", + "tags": [ + "data", + "map", + "geography", + "world", + "choropleth" + ], + "href": "/catalog/blocks/world-map", + "preview": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/world-map.png" + }, { "name": "x-post", "type": "block", diff --git a/packages/core/src/compiler/htmlBundler.ts b/packages/core/src/compiler/htmlBundler.ts index a17050dc9..3bae137bf 100644 --- a/packages/core/src/compiler/htmlBundler.ts +++ b/packages/core/src/compiler/htmlBundler.ts @@ -368,7 +368,7 @@ function parseHostVariableValues(host: Element): Record { return parsed as Record; } -const FLATTENED_INNER_ROOT_STRIP_ATTRS = [ +export const FLATTENED_INNER_ROOT_STRIP_ATTRS = [ "data-composition-id", "data-composition-file", "data-start", @@ -381,7 +381,7 @@ const FLATTENED_INNER_ROOT_STRIP_ATTRS = [ "data-hf-authored-end", ]; -function prepareFlattenedInnerRoot(innerRoot: Element): Element { +export function prepareFlattenedInnerRoot(innerRoot: Element): Element { const prepared = innerRoot.cloneNode(true) as Element; const authoredRootId = prepared.getAttribute("id")?.trim(); for (const attrName of FLATTENED_INNER_ROOT_STRIP_ATTRS) { diff --git a/packages/core/src/compiler/index.ts b/packages/core/src/compiler/index.ts index 85d66afea..e173ba890 100644 --- a/packages/core/src/compiler/index.ts +++ b/packages/core/src/compiler/index.ts @@ -15,7 +15,12 @@ export { export { compileHtml, type MediaDurationProber } from "./htmlCompiler"; // HTML bundler (Node.js — requires fs, linkedom, esbuild) -export { bundleToSingleHtml, type BundleOptions } from "./htmlBundler"; +export { + bundleToSingleHtml, + type BundleOptions, + prepareFlattenedInnerRoot, + FLATTENED_INNER_ROOT_STRIP_ATTRS, +} from "./htmlBundler"; export { RUNTIME_BOOTSTRAP_ATTR, diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index a57de988a..01036dc84 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -964,7 +964,7 @@ export function initSandboxRuntimeModular(): void { return true; }; - (window as any).__hfForceTimelineRebind = () => { + (window as Window & { __hfForceTimelineRebind?: () => void }).__hfForceTimelineRebind = () => { childrenBound = false; bindRootTimelineIfAvailable(); }; diff --git a/packages/engine/src/services/frameCapture.ts b/packages/engine/src/services/frameCapture.ts index f15a30dc8..06561ac1a 100644 --- a/packages/engine/src/services/frameCapture.ts +++ b/packages/engine/src/services/frameCapture.ts @@ -682,11 +682,25 @@ export async function initializeSession(session: CaptureSession): Promise await applyVideoMetadataHints(page, session.options.videoMetadataHints); // Same readyState contract as the screenshot path above (>= 2 / HAVE_CURRENT_DATA). - await pollVideosReady( + const bfVideosReady = await pollVideosReady( page, session.options.skipReadinessVideoIds ?? [], session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout, ); + if (!bfVideosReady) { + const failedVideos = await page.evaluate((skipIdList: readonly string[]) => { + const skip = new Set(skipIdList); + return Array.from(document.querySelectorAll("video")) + .filter((v) => !skip.has(v.id)) + .filter((v) => (v as HTMLVideoElement).readyState < 2 && !(v as HTMLVideoElement).error) + .map((v) => (v as HTMLVideoElement).src || v.getAttribute("src") || "(no src)") + .join(", "); + }, session.options.skipReadinessVideoIds ?? []); + console.warn( + `[FrameCapture] Some video elements did not decode within ${pageReadyTimeout}ms: ${failedVideos}. ` + + `Continuing render — affected videos will appear as blank/black frames.`, + ); + } // Font check (no rAF dependency — uses fonts.ready API directly) await page.evaluate(`document.fonts?.ready`); diff --git a/packages/engine/src/services/videoFrameExtractor.ts b/packages/engine/src/services/videoFrameExtractor.ts index 70979f1a8..ef32c2ef2 100644 --- a/packages/engine/src/services/videoFrameExtractor.ts +++ b/packages/engine/src/services/videoFrameExtractor.ts @@ -530,7 +530,7 @@ export function resolveProjectRelativeSrc( // then strip any remaining leading `..` segments. Stripping `..` from the // raw input would leave dangling siblings (`assets/../../assets/foo` // would become `assets/assets/foo` instead of `assets/foo`). - const normalized = posix.normalize(src.replace(/\\/g, "/")); + const normalized = posix.normalize(cleanSrc.replace(/\\/g, "/")); const stripped = normalized.replace(/^(\.\.\/)+/, ""); if (stripped && stripped !== src && !stripped.startsWith("..")) { if (compiledDir) candidates.push(join(compiledDir, stripped)); diff --git a/packages/producer/src/services/htmlCompiler.ts b/packages/producer/src/services/htmlCompiler.ts index 2203b764e..d8b2c6862 100644 --- a/packages/producer/src/services/htmlCompiler.ts +++ b/packages/producer/src/services/htmlCompiler.ts @@ -21,7 +21,10 @@ import { type ResolvedDuration, type UnresolvedElement, } from "@hyperframes/core"; -import { inlineSubCompositions as inlineSubCompositionsShared } from "@hyperframes/core/compiler"; +import { + inlineSubCompositions as inlineSubCompositionsShared, + prepareFlattenedInnerRoot, +} from "@hyperframes/core/compiler"; import { extractMediaMetadata, extractAudioMetadata } from "../utils/ffprobe.js"; import { isPathInside, toExternalAssetKey } from "../utils/paths.js"; import { @@ -559,19 +562,6 @@ function inlineSubCompositions( if (!hosts.length) return html; - const FLATTENED_INNER_ROOT_STRIP_ATTRS = [ - "data-composition-id", - "data-composition-file", - "data-start", - "data-duration", - "data-end", - "data-track-index", - "data-track", - "data-composition-src", - "data-hf-authored-duration", - "data-hf-authored-end", - ]; - const result = inlineSubCompositionsShared( document as unknown as Document, hosts as unknown as Element[], @@ -587,19 +577,7 @@ function inlineSubCompositions( return compHtml; }, parseHtml: (htmlStr: string) => parseHTML(htmlStr).document as unknown as Document, - flattenInnerRoot: (innerRoot: Element): Element => { - const prepared = innerRoot.cloneNode(true) as Element; - const authoredRootId = prepared.getAttribute("id")?.trim(); - for (const attrName of FLATTENED_INNER_ROOT_STRIP_ATTRS) { - prepared.removeAttribute(attrName); - } - if (authoredRootId) { - prepared.removeAttribute("id"); - prepared.setAttribute("data-hf-authored-id", authoredRootId); - } - prepared.setAttribute("data-hf-inner-root", "true"); - return prepared; - }, + flattenInnerRoot: prepareFlattenedInnerRoot as (innerRoot: Element) => Element, scriptErrorLabel: "[Compiler] Composition script failed", }, );